...
 
Commits (10)
# POOker
Poker service in Python for the POOA course
Try it at http://18.223.33.251/homePage !
Try it at https://pooker.ml or https://18.223.33.251/ !
# Installation
......@@ -15,3 +15,45 @@ Launch the server:
``` bash
env FLASK_ENV=development FLASK_APP=server/server.py python3 -m flask run -p 5000
```
# Usage
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.
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
The game engine is served via a Flask server.
The interaction with the client (in the browser) is mainly done via an HTTP API.
A socketIO websocket (we did not manage to get classic websockets working alongside Flask) is used to alert all of the clients when someone finishes his turn and update the game state.
Detailed file description:
- `server/`
- `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
- `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`
- `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
# Known problems
These are known problems that we lacked time to address/fix.
If someone does not make any action during his time (30s), the server for some reason is not able to send the updates anymore via the socket.
Therefore the front-end calls /api/game/ every second to keep track of the state of the game in parallel of the websocket. The socket will only reconnect (as a new socket) on page reload.
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.
js_poker
========
A simple, minimalist scaffold for a poker web app frontend
\ No newline at end of file
......@@ -57,11 +57,9 @@ function updateGameState() {
url: '/api/game/' + gameId,
// data: id
success: function (msg) {
$("#history").text(JSON.stringify(msg));
catchUp(msg)
},
error: function (msg) {
$("#history").text(JSON.stringify(msg))
}
})
}
......@@ -204,7 +202,8 @@ function setCurrentPot(amount) {
}
function checkClicked() {
$('#history').append("\n you clicked check");
var history = document.getElementById('history');
history.innerHTML += 'You clicked CHECK <br>';
var url = '/api/game/' + gameId + "/action";
console.log(url);
$.ajax({
......@@ -215,14 +214,14 @@ function checkClicked() {
},
dataType: 'json',
success: function (msg) {
$("#history").text(JSON.stringify(msg));
catchUp(msg)
}
})
}
function foldClicked() {
$('#history').append("\n you clicked fold");
var history = document.getElementById('history');
history.innerHTML += 'You clicked FLOD <br>';
var url = '/api/game/' + gameId + "/action";
console.log(url);
$.ajax({
......@@ -233,14 +232,14 @@ function foldClicked() {
},
dataType: 'json',
success: function (msg) {
$("#history").text(JSON.stringify(msg));
catchUp(msg)
}
})
}
function callClicked() {
$('#history').append("/\n/ you clicked call");
var history = document.getElementById('history');
history.innerHTML += 'You clicked CALL <br>';
var url = '/api/game/' + gameId + "/action";
console.log(url);
$.ajax({
......@@ -251,14 +250,14 @@ function callClicked() {
},
dataType: 'json',
success: function (msg) {
$("#history").text(JSON.stringify(msg));
catchUp(msg)
}
})
}
function raiseClicked() {
$('#history').append("\n you clicked raise");
var history = document.getElementById('history');
history.innerHTML += 'You clicked RAISE <br>';
var url = '/api/game/' + gameId + "/action";
console.log(url);
var amount = document.forms[0].amount.value;
......@@ -271,7 +270,6 @@ function raiseClicked() {
},
dataType: 'json',
success: function (msg) {
$("#history").text(JSON.stringify(msg));
catchUp(msg)
}
})
......@@ -317,7 +315,6 @@ socket.on('connect', function () {
socket.on('state', function(msg) {
state = JSON.parse(msg);
$("#history").text(msg);
console.log("receive msg");
catchUp(state)
});
......@@ -16,7 +16,7 @@
<br>
<label>
<input type="radio" name="mode" value="1">
Tournoi</label>
Tournament</label>
<br>
<article>Choose number of players</article>
<select title="nbPlayers" name="nbPlayers" id="nbPlayers">
......
# A hand must be initialized with exactly 5 cards, which shall be ordered in
# the constructor (following the poker hand value rule).
# The class is fitted with a < operator, which allows hand to hand comparison.
# TODO: String description of the hand (might be useful)
## class Hand
# This class represents a poker hand (5 cards), with the associated hand value
# computation. The class is fitted with a < operator, which allows hand to hand
# comparison.
# The class may be in 2 different states (accessed by the isLoaded method):
# either useable or blank. This state may not change, as the 5 cards may only
# be given through instanciation. All of the computation is done during
# instatiation, which may fail if the given card list is neither empty nor
# contains exactly 5 cards.
#
# Tests :
# h = hand.Hand([Card(1,3), Card(1, 6), Card(1, 6), Card(1, 8), Card(2, 8)])
......@@ -10,8 +15,6 @@
# h.cardsStr()
# h < h2
#
# Errors detected :
#
import enum
......@@ -36,7 +39,6 @@ class Hand:
if len(self.__cards) == 5:
self.__cards.sort()
self.__cards.reverse()
# print(self.cardsStr())
self.__readHand()
def getHandName(self):
......@@ -51,36 +53,46 @@ class Hand:
return len(self.__cards) == 5
def handStr(self):
# String description of the hand, to be exported to front end
args = [self.__cards[0].valueStr(), self.__cards[1].valueStr(), self.__cards[2].valueStr(), self.__cards[3].valueStr(), self.__cards[4].valueStr()]
if not self.isLoaded():
return 'Not a hand'
if self.__handName == HandName.HIGH_CARD:
return 'High card {}, {} + {} + {} + {} kicker'.format(self.__cards[0].valueStr(), self.__cards[1].valueStr(), self.__cards[2].valueStr(), self.__cards[3].valueStr(), self.__cards[4].valueStr())
return 'High card {}, {} + {} + {} + {} kicker'.format(*args)
if self.__handName == HandName.PAIR:
return 'Pair of {0}s, {2} + {3} + {4} kicker'.format(self.__cards[0].valueStr(), self.__cards[1].valueStr(), self.__cards[2].valueStr(), self.__cards[3].valueStr(), self.__cards[4].valueStr())
return 'Pair of {0}s, {2} + {3} + {4} kicker'.format(*args)
if self.__handName == HandName.TWO_PAIRS:
return 'Pairs of {0}s and {2}s, {4} kicker'.format(self.__cards[0].valueStr(), self.__cards[1].valueStr(), self.__cards[2].valueStr(), self.__cards[3].valueStr(), self.__cards[4].valueStr())
return 'Pairs of {0}s and {2}s, {4} kicker'.format(*args)
if self.__handName == HandName.THREE_OF_A_KIND:
return 'Three {0}s, {3} + {4} kicker'.format(self.__cards[0].valueStr(), self.__cards[1].valueStr(), self.__cards[2].valueStr(), self.__cards[3].valueStr(), self.__cards[4].valueStr())
return 'Three {0}s, {3} + {4} kicker'.format(*args)
if self.__handName == HandName.STRAIGHT:
return 'Straight {4} to {0}'.format(self.__cards[0].valueStr(), self.__cards[1].valueStr(), self.__cards[2].valueStr(), self.__cards[3].valueStr(), self.__cards[4].valueStr())
return 'Straight {4} to {0}'.format(*args)
if self.__handName == HandName.FLUSH:
return '{} High flush, {} + {} + {} + {} kicker'.format(self.__cards[0].valueStr(), self.__cards[1].valueStr(), self.__cards[2].valueStr(), self.__cards[3].valueStr(), self.__cards[4].valueStr())
return '{} High flush, {} + {} + {} + {} kicker'.format(*args)
if self.__handName == HandName.FULL_HOUSE:
return '{0}s full of {3}s'.format(self.__cards[0].valueStr(), self.__cards[1].valueStr(), self.__cards[2].valueStr(), self.__cards[3].valueStr(), self.__cards[4].valueStr())
return '{0}s full of {3}s'.format(*args)
if self.__handName == HandName.FOUR_OF_A_KIND:
return 'Four {0}s, {4} kicker'.format(self.__cards[0].valueStr(), self.__cards[1].valueStr(), self.__cards[2].valueStr(), self.__cards[3].valueStr(), self.__cards[4].valueStr())
return 'Four {0}s, {4} kicker'.format(*args)
if self.__handName == HandName.STRAIGHT_FLUSH:
return 'Straight flush {4} to {0}'.format(self.__cards[0].valueStr(), self.__cards[1].valueStr(), self.__cards[2].valueStr(), self.__cards[3].valueStr(), self.__cards[4].valueStr())
return 'Straight flush {4} to {0}'.format(*args)
print('Internal error : Unexpected state {}.'.format(self.__handName))
return 'Hand Internal Error'
def cardsStr(self):
# Debug string description
return '{0}{1}{2}{3}{4}'.format(self.__cards[0].cardStr(), self.__cards[1].cardStr(), self.__cards[2].cardStr(), self.__cards[3].cardStr(), self.__cards[4].cardStr())
def __sendToBack(self, nbr):
# Internal methode used to reorder cards when reading the hand
self.__cards.append(self.__cards[nbr])
del self.__cards[nbr]
def __readHand(self):
## Extract hand type and reorders the card from the most to least
# important.
# This method assumes that the card list has been previously sorted in
# decreasing order, and should be exclusively called by the __init__.
#
counter = []
for i in range(15):
counter.append(0)
......@@ -156,6 +168,7 @@ class Hand:
self.__handName = HandName.STRAIGHT
def __lt__(self, hand):
# Compare 2 hands based on hand value
assert self.isLoaded()
assert hand.isLoaded()
rslt = False
......
## class Player
# This class handles the status of a player, without much responsibility.
# It takes care of:
# - unique ID, name
# - player status (playing, busted, all-in, etc)
# - player's chips (until they are sent to the pot)
# - related pot (when side-pots occur)
# - hand handling (receives its holecards, computes its hand with boardcards)
# - comparison operator with another player (based and hand value)
#
from uuid import uuid4
from enum import Enum
from player.hand import Hand
class PlayerState(Enum):
PLAYING = 0
FOLDED = 1
ALL_IN = 2
BUSTED = 3
CALLED = 4
CHECKED = 5
RAISED = 6
class Player:
......@@ -29,11 +44,11 @@ class Player:
# GETTERS & SETTERS
@property
def isPlaying(self):
return self.__playerState == PlayerState.PLAYING
return self.__playerState == PlayerState.PLAYING or self.__playerState == PlayerState.CHECKED or self.__playerState == PlayerState.CALLED or self.__playerState == PlayerState.RAISED
@property
def isInHand(self):
return self.__playerState == PlayerState.PLAYING or self.__playerState == PlayerState.ALL_IN
return self.__playerState == PlayerState.PLAYING or self.__playerState == PlayerState.CHECKED or self.__playerState == PlayerState.CALLED or self.__playerState == PlayerState.RAISED or self.__playerState == PlayerState.ALL_IN
@property
def isAllIn(self):
......@@ -92,6 +107,13 @@ class Player:
# PRIVATE METHODS
def __putMoneyOnTable(self, amount):
## called by 'call' or 'bet', puts money from the player's stack on the
# table, but not yet on the pot.
# This methode is in charge of putting the player all-in if necessary,
# and makes sure that the player's stack is nto negative.
# Receives the expected amount to be extracted from the stack, and
# returns the amount that was actually extracted.
#
amountBet = 0
if self.isAllIn:
print("Player {} is already All In".format(self.__name))
......@@ -111,6 +133,7 @@ class Player:
# PUBLIC METHODS
def newHand(self):
# Reset status for the next hand.
self.__hasHadAction = False
if self.__chips:
self.__playerState = PlayerState.PLAYING
......@@ -119,12 +142,15 @@ class Player:
self.__hand = Hand()
self.__potId = -1
self.__won = False
# self.__position = pos # Players are caracterized by uid, no need ATM
def computeHand(self, boardCards):
## Towards the end of the hand, give the player the board cards and
# let him find out his hand (the best combination of 5 cards from the 5
# cards from the board and its 2 hole cards).
# Returns the computed best hand.
#
assert len(boardCards) == 5
cards = boardCards + self.__cards
# print("Hole cards {}, cards {}".format(len(self.__cards), len(cards)))
self.__hand = Hand(boardCards)
for i in range(len(cards)):
for j in range(i+1, len(cards)):
......@@ -132,7 +158,6 @@ class Player:
for k in range(len(cards)):
if k != i and k != j:
newHandCards += [cards[k]]
# print("({0}, {1})/{2} : {3}".format(i, j, len(cards), len(newHandCards)))
newHand = Hand(newHandCards)
if self.__hand < newHand:
self.__hand = newHand
......@@ -167,46 +192,57 @@ class Player:
def giveBB(self, bigBlind):
return self.__putMoneyOnTable(bigBlind)
# Actions, called by Table.
# By calling these methodes, Table authorises the transaction.
# By calling these methods, Table authorises the transaction.
# amount is the money added
# Returns the amount added
def bet(self, amount):
# amount is the money to be added
# Returns the amount actually added
self.__hasHadAction = True
self.__playerState = PlayerState.RAISED
return self.__putMoneyOnTable(amount)
# Returns True if succeeds
def check(self):
# Returns True if succeeds
self.__hasHadAction = True
self.__playerState = PlayerState.CHECKED
return True
# Returns the amount added
def call(self, amountToCall):
# Returns the amount added
self.__hasHadAction = True
self.__playerState = PlayerState.CALLED
return self.__putMoneyOnTable(amountToCall - self.__chipsOnTable)
# Returns True if succeeds
def fold(self):
# Returns True if succeeds
self.__hasHadAction = True
self.__playerState = PlayerState.FOLDED
return True
# Timeout, shouldn't be needed (just check/fold called from Table)
def yieldAction(self):
# Timeout, shouldn't be needed (just check/fold called from Table)
self.__hasHadAction = True
# returns player state (all in, playing, folded)
def state(self):
# returns player state (all in, playing, folded)
if self.__playerState == PlayerState.FOLDED:
return "FOLDED"
return "FOLD"
if self.__playerState == PlayerState.ALL_IN:
return "ALL IN"
if self.__playerState == PlayerState.BUSTED:
return "BUSTED"
if self.__playerState == PlayerState.CALLED:
return "CALL"
if self.__playerState == PlayerState.CHECKED:
return "CHECK"
if self.__playerState == PlayerState.RAISED:
return "RAISE"
return ""
def __lt__(self, player):
# Compare 2 players based on the value of their hand
assert self.isComparable()
assert player.isComparable()
return self.hand < player.hand
from flask_socketio import SocketIO
import threading
class Game:
def __init__(self, table, server):
......@@ -7,12 +7,26 @@ class Game:
self.__sockets = []
self.__userIds = []
self.__socketio = server
self.__mutex = threading.Lock()
def bCastState(self):
print("BROADCASTING")
for (i,socketId) in enumerate(self.__sockets):
self.__socketio.emit('state', self.table.state(self.__userIds[i]), room=socketId)
with self.__mutex:
# Sends a personalized message (thus not really broadcasting) to each connected socket
# print("BROADCASTING")
for (i,socketId) in enumerate(self.__sockets):
self.__socketio.emit('state', self.table.state(self.__userIds[i]), room=socketId)
def addSocket(self, socket, userId):
self.__userIds.append(userId)
self.__sockets.append(socket)
with self.__mutex:
self.__userIds.append(userId)
self.__sockets.append(socket)
def removeSocket(self, socket):
with self.__mutex:
# Remove the socket if it exists (the socket disconnected)
for (i, socketId) in enumerate(self.__sockets):
if socket == socketId:
# Remove the socket and the userID
del self.__sockets[i]
del self.__userIds[i]
return
......@@ -93,6 +93,7 @@ def game():
p = Player(name)
if gameId in games.keys():
currentTable = games[gameId].table
# Players new players are added to the waiting line, they will be admitted to the game when a player is busted
if currentTable.playingMode == PlayingMode.CASHGAME:
currentTable.addPlayer(p)
if len(currentTable.waitingPlayers) == currentTable.maxPlayers and not len(currentTable.players):
......@@ -104,6 +105,7 @@ def game():
resp.set_cookie("userId", str(p.uniqueId))
games.get(gameId).bCastState()
return resp
# No more player than the maximum number of players can be admitted, players are eliminated until 1 last remains
elif currentTable.playingMode == PlayingMode.TOURNOI:
if len(currentTable.waitingPlayers) + len(currentTable.players) < currentTable.maxPlayers:
currentTable.addPlayer(p)
......@@ -160,6 +162,9 @@ def connect():
@socketio.on('disconnect')
def disconnect():
print('Websocket disconnected')
# Remove the saved socket for its game
for game in games.values():
game.removeSocket(request.sid)
if __name__ == '__main__':
......
## class Card
# This class represents a single card, with its suit and value.
# These parameters cannot be changed after instanciation, which must succeed
# (overwise an exception shall be raised), so an instance of card is always
# valid.
# The class is fitted with a < operator, which compares cards depending on their
# value alone, and a sameAs() methode which takes both value and suit into account.
# The class also handles the json export.
#
# Values are stored as integers, with the following equivalence :
# T = 10
# J = 11
# Q = 12
# K = 13
# A = 14
#
from enum import Enum
class Color(Enum):
HEARTS = 1
SPADES = 2
......@@ -15,6 +27,8 @@ class Color(Enum):
class Card:
def __init__(self, color, value):
## The card must be instanciated with its suit and value, which won't
# change.
assert value >= 2 and value <= 14
self.__color = color
self.__value = value
......@@ -38,9 +52,11 @@ class Card:
return self.__json
def cardStr(self):
# Debug print
return '({0}:{1})'.format(self.__value, self.__color)
def valueStr(self):
# Returns the value of a card as astring, for json export.
if self.__value == 11:
return "Jack"
if self.__value == 12:
......@@ -54,7 +70,9 @@ class Card:
return str(self.__value)
def __lt__(self, card):
# Compares 2 cards solely on value (for hand evaluation).
return self.__value < card.value
def sameAs(self, card):
# Check if both cards are exactly the same (suit and value)
return self.__color == card.color and self.__value == card.value
# A single deck of cards, shuffled when instanciated.
## class Deck
# A single deck of 52 cards.
# An instance of Deck is always available until no cards are left.
# The reset method generates a clean new deck, similar to a newly instanciated
# one.
# The size of the deck is decreased by one for each drawn card, until none are
# left (drawCard will then fail).
#
from table import card
......
......@@ -8,8 +8,7 @@
# t.call(1)
# t.call(0)
# Then repeat the last 2 at will
#
# TODO: fix bug with wrong player action at the start of the second hand.
import time
from math import exp
from uuid import uuid4
......@@ -199,8 +198,11 @@ class Table(object):
self.__givePot()
self.__button = self.__playerAtPos(1)
self.__bCastCB()
print("hallahhhhhhh")
self.__endOfHandTimer = threading.Timer(20, self.startHand)
if self.__gameState == GameState.HAND_ENDED_NO_SHOW:
t = 5
else:
t = 10
self.__endOfHandTimer = threading.Timer(t, self.startHand)
self.__endOfHandTimer.start()
else:
if self.__gameState == GameState.PREFLOP:
......@@ -262,11 +264,9 @@ class Table(object):
def __givePot(self):
# Only one player left
if self.__nbrPlayersInHand == 1:
winner = 0
winningPlayer = self.__players[0]
for (i, p) in enumerate(self.__players):
if p.isPlaying:
winner = i
winningPlayer = p
print("Hand ended, player {} won {}.".format(winningPlayer.name, self.__pots[0]))
winningPlayer.won = True
......@@ -371,7 +371,7 @@ class Table(object):
def __fold(self, p):
if not p.fold():
print("Player {} cant fold, fold failed (unexpected).".format(p.name))
return False # fold could not happen for some reason
return False # fold could not happen for some reason
self.__nbrPlayersInHand -= 1
print("Player {} folded.".format(p.name))
return True
......@@ -455,7 +455,7 @@ class Table(object):
elif self.__gameState == GameState.HAND_ENDED:
for pId in range(len(self.__players)):
p = self.__players[pId]
if p.isPlaying:
if p.isInHand:
rtn.append({
'cards': [card.json for card in p.holeCards],
'seatNumber': pId+1,
......@@ -522,14 +522,14 @@ class Table(object):
self.__smallBlind = val / 2
"""
t = Table(3, 0)
p0 = Player("p0")
p1 = Player("p1")
p2 = Player("p2")
t.addPlayer(p0)
t.addPlayer(p1)
t.addPlayer(p2)
t.startHand()
"""
# """
# t = Table(3, 0)
# p0 = Player("p0")
# p1 = Player("p1")
# p2 = Player("p2")
# t.addPlayer(p0)
# t.addPlayer(p1)
# t.addPlayer(p2)
#
# t.startHand()
# """