...
 
Commits (7)
......@@ -5,22 +5,32 @@ Try it at https://pooker.ml or https://18.223.33.251/ !
# Installation
Assuming Git, Python3 and pip are installed on a Linux-like system (switching to another OS should be similar):
``` bash
git clone https://gitlab.centralesupelec.fr/2015garder/POOker.git
cd POOker/
python3 -m pip install --user -r requirements.txt
```
Launch the server:
Launch the server locally:
``` bash
env FLASK_ENV=development FLASK_APP=server/server.py python3 -m flask run -p 5000
```
Then access it at http://localhost:5000.
To serve in production mode, `FLASK_ENV=production` should be used.
If served on the World Wide Web behind a reverse proxy, please note that you also need to forward the websocket connections.
# Usage
Connect to the webpage, choose the game mode :
- a tournament has a blind raising over time, the game starts automatically when enough players are connected and finishes when one player wins all the chips.
- a cash game has a constant blind, players can join the game along time, when they no more have money, they are replaced by waiting players.
Connect to the webpage, choose the game mode (a tournament has a blind raising over time, contrary to the cash game) and the number of players and generate a game ID.
Select the number of players and generate a game ID.
Share the game ID with your friends, enter your name and the game ID, and join the game !
If you lack friends, use different browsers (you are identified by cookies, multiple tabs will not work).
Joining the game by copying the URL will set you as spectator.
# Fonctioning
......@@ -36,16 +46,16 @@ Detailed file description:
- `server.py`: The Flask-SocketIO server
- `game.py`: The `Game` Class, consisting of a poker `Table` and a list of connected sockets
- `table/`
- `table.py`: main class representing a Poker table
- `table.py`: main class representing a Poker table and the game engine core
- `tableTimer.py`: timer for automatic actions on the table: after a player timeout or the end of a hand.
- `deck.py`: the `Deck` of `Cards`
- `cards.py`: the `Card` object
- `player/`
- `player.py`: a `Player` in the `Table`
- `player.py`: a `Player` at a `Table`
- `hand.py`: describe the poker hands formed by the `Cards`.
- `front/`
- `static/`: the static CCS/JS/Images files
- `templates`: the Jinja2 HTML templates used by Flask
- `static/`: the static CCS/JS/Images files - inspired by https://github.com/tansey/js_poker
- `templates/`: the Jinja2 HTML templates used by Flask
# Known problems
......@@ -58,3 +68,7 @@ Maybe SocketIO does not manage to use websockets and falls back to HTTP long-pol
As there is no server-side file/database storage, on a server restart or crash all the games are lost. We choosed to do so for quicker development, but it is obviously not production ready.
There is currently no logic to destroy the table when the game is finished, leading in an increased number of `Table` objects over time. The server should be restarted often to counter this.
# Code reuse
For the front end app we adapted the files from : https://github.com/tansey/js_poker
......@@ -18,9 +18,7 @@ function catchUp(tableState) {
function updateHistory(tableState) {
var state = tableState;
var txt = '';
console.log(players);
if (players.length > 0) {
console.log("test")
for (var i = 0; i < state['players'].length; i++) {
if (players[i]['state'] !== state['players'][i]['state']) {
if (state['players'][i]['state'].length > 0) {
......
......@@ -14,7 +14,7 @@
# h.getHandName()
# h.cardsStr()
# h < h2
#
import enum
......
......@@ -31,11 +31,10 @@ class Player:
def __init__(self, name):
self.__uniqueId = str(uuid4())
self.__hand = Hand()
self.__chips = int(0)
self.__chips = 0
self.__cards = []
self.__chipsOnTable = int(0)
self.__position = int(0)
self.__hasHadAction = bool(False)
self.__chipsOnTable = 0
self.__hasHadAction = False
self.__playerState = PlayerState.PLAYING
self.__name = name
self.__potId = -1
......@@ -170,12 +169,15 @@ class Player:
self.__chips += chips
def yieldBet(self):
# Moves the money that was on the table to the pot
self.__hasHadAction = False
temp = self.__chipsOnTable
self.__chipsOnTable = 0
return temp
def yieldPartialBet(self, value):
## Moves part of the money that was on the table to the pot, used for
# split pots
self.__hasHadAction = False
chips = min(value, self.__chipsOnTable)
self.__chipsOnTable -= chips
......@@ -187,10 +189,8 @@ class Player:
def giveBB(self, bigBlind):
return self.__putMoneyOnTable(bigBlind)
# Actions, called by Table.
# By calling these methods, Table authorises the transaction.
def bet(self, amount):
# amount is the money to be added
# Returns the amount actually added
......
# Game engine
## class Table
# This class is pretty much the game engine, it receives orders from the
# players connected to the front end, handles the money, controls the rules,
# and generates the json for the front end.
# One instance represents one table of poker, several instances may work in
# parallel in order to allow for more players.
#
# Tests :
# t = Table()
# t.addPlayer(Player())
# t.addPlayer(Player())
# t.startHand()
# t.call(1)
# t.call(0)
# Then repeat the last 2 at will
import time
from math import exp
......@@ -39,6 +36,7 @@ class PlayingMode(Enum):
def defaultCallBack():
# Function used as default for objects that require a callback function
print("Default CallBack function.")
return
......@@ -116,12 +114,13 @@ class Table(object):
self.__waitingPlayers.append(p)
self.__mainMutex.release()
# Return the player at required position, 0 is button
def __playerAtPos(self, pos):
# Return the player index located 'pos' sits after the button
return (self.__button + pos) % len(self.__players)
# Passes action to the next player in the hand
def __nextPos(self):
## Called when a player successfully had an action, changes the active
# player to the next one available
playerFound = False
while not playerFound:
self.__activePlayer = (self.__activePlayer + 1) % len(self.__players)
......@@ -142,6 +141,9 @@ class Table(object):
return self.__activePlayer
def startHand(self):
## Start a new hand (checks if enough players are still in the game)
# This method resets variable, passes the button, and checks if waiting
# players need to be added and/or cleans busted ones.
self.__mainMutex.acquire()
print(self.__playingMode)
if self.__playingMode == PlayingMode.TOURNOI:
......@@ -190,10 +192,14 @@ class Table(object):
self.__mainMutex.release()
def __nextStep(self):
## Method called when a turn of bet has ended, and the next step of the
# hand is to begin.
# It will check if the hand has already ended (everybody folded,
# all-ins, etc), or simply gather the money bet by the player and go on
# to the next stage of the hand.
self.__currentBet = 0
self.__minRaise = self.__blind
self.__pickUpBets()
# Check if hand has already ended
if self.__checkEndOfHand():
self.__givePot()
self.__button = self.__playerAtPos(1)
......@@ -226,6 +232,9 @@ class Table(object):
self.__nextPos()
def __pickUpBets(self):
## Gather the money that was bet by the player and gives it to the pot.
# This method is quite tricky as it must handle all-ins and side pots,
# so many turns of gathering may be required.
pickUpDone = False
minAllInValue = 0
while not pickUpDone:
......@@ -250,6 +259,9 @@ class Table(object):
return
def __checkEndOfHand(self):
## This method determines if the hand has ended, either by finishing
# the last round of bets, or premature folds/all-ins.
# Returns true if the hand has ended.
handEnded = False
if self.__gameState == GameState.RIVER or handEnded == True:
handEnded = True
......@@ -262,8 +274,16 @@ class Table(object):
return handEnded
def __givePot(self):
# Only one player left
## This method is called when the hand has ended, and the money
# gathered in the pot needs to be redistributed.
# If the hand ended before the river with all-ins, additionnal cards
# must first be drawn to complete the board cards.
# Each side pot is shared between the players allowed to, starting
# the player or players with the best hand down to the players with the
# the lesser hands (thus satisfying both situations of split and side
# pots).
if self.__nbrPlayersInHand == 1:
# Only one player left
winningPlayer = self.__players[0]
for (i, p) in enumerate(self.__players):
if p.isPlaying:
......@@ -299,7 +319,8 @@ class Table(object):
print("Player {} ({}{}) : {}".format(p.name, p.holeCards[0].cardStr(), p.holeCards[1].cardStr(), p.hand.handStr()))
remainingPlayers.sort()
remainingPlayers.reverse()
# class players by equivalent classes
# Class players by equivalent classes (all players in the class
# have a hand of identical value, thus needing a split pot)
classedPlayers = [[remainingPlayers[0]]]
for i in range(1, len(remainingPlayers)):
if not remainingPlayers[i] < classedPlayers[-1][0]:
......@@ -307,13 +328,17 @@ class Table(object):
else:
classedPlayers.append([remainingPlayers[i]])
for l in classedPlayers:
# Start with the players with the best hand
for k in range(len(self.__pots)):
# Give pots from the lesser valued on onwards
nbrSharing = 0
for i in range(len(l)):
# Count number of players that share the pot
if l[i].potId == -1 or l[i].potId >= k:
nbrSharing += 1
if nbrSharing and self.__pots[k]:
for i in range(len(l)):
# Give away the pot to the players who desserve it
if l[i].potId == -1 or l[i].potId >= k:
l[i].receiveChip(int(self.__pots[k] / nbrSharing))
print("Player {} receives {} from pot {}.".format(l[i].name, int(self.__pots[k] / nbrSharing), k))
......@@ -324,6 +349,7 @@ class Table(object):
return
def __isBettingDone(self):
## Check wether to go to the next player or next stage of the hand
nbrInHand = 0
for p in self.__players:
if p.isInHand:
......@@ -336,7 +362,9 @@ class Table(object):
return True
def playerAction(self, action, playerId, amount):
# timer management
## Methode called from the front end.
# Checks if the action is globally valid, then call the specific method
# that will check if the precise action is valid.
self.__mainMutex.acquire()
self.__timer.cancel()
p = self.__players[playerId]
......@@ -368,6 +396,9 @@ class Table(object):
self.__mainMutex.release()
return True
## Specific action functions, they check the validity of the demand then
# transfers it to the player instance and returns wether the action
# succeded or not.
def __fold(self, p):
if not p.fold():
print("Player {} cant fold, fold failed (unexpected).".format(p.name))
......@@ -412,9 +443,8 @@ class Table(object):
return True
# State Table
def state(self, userId):
# Json state for the front end.
rtn = json.dumps({
"pot": self.__pots[0],
"activePlayer": self.__activePlayer,
......@@ -442,12 +472,14 @@ class Table(object):
return rtn
def getPlayerIndex(self, userId):
# Transforms a usedId into the position in the player list.
for (i, p) in enumerate(self.__players):
if p.uniqueId == userId:
return i
return -1
def __getHoleCards(self, userId):
# Returns hole cards to be sent with the json.
rtn = []
userIndex = self.getPlayerIndex(userId)
if userIndex == -1:
......@@ -471,6 +503,7 @@ class Table(object):
return rtn
def __getBettingRange(self, userId):
# Returns betting range to be sent with the json.
userIndex = self.getPlayerIndex(userId)
if userIndex == -1:
return []
......@@ -478,6 +511,7 @@ class Table(object):
return [self.__currentBet + self.__minRaise - p.chipsOnTable, p.remainingChips]
def __availableActions(self, userId):
# Returns all of the actions that a player may do (sent with the json).
userIndex = self.getPlayerIndex(userId)
if userIndex == -1:
return []
......@@ -495,6 +529,7 @@ class Table(object):
return rslt
def __getAmountToCall(self, userId):
# Returns the amount required to call (sent with the json).
userIndex = self.getPlayerIndex(userId)
if userIndex == -1:
return 0
......@@ -502,6 +537,8 @@ class Table(object):
return self.__currentBet - p.chipsOnTable
def __defaultActionCallback(self):
## Callback for the player time-out timer, performs the best action
# available without putting money out.
activePlayerId = self.__players[self.__activePlayer].uniqueId
if "check" in self.__availableActions(activePlayerId):
self.playerAction("check", self.__activePlayer, 0)
......@@ -512,6 +549,8 @@ class Table(object):
self.__bCastCB()
def __calculateBlinds(self):
## Sets blind depending on time elapsed since the first hand
# (tournament mode only)
now = time.time()
elapsed = now - self.__startTime
val = int(exp(elapsed/60/7)+1)
......@@ -519,4 +558,4 @@ class Table(object):
if val % 2 != 0:
val += 1
self.__blind = val
self.__smallBlind = val / 2
self.__smallBlind = val / 2
\ No newline at end of file