Skip to content

Commit

Permalink
[holdem] fix split of main pot and side pots in case some players are…
Browse files Browse the repository at this point in the history
… all-in

Former-commit-id: a565652
  • Loading branch information
ismael-elatifi committed Oct 29, 2020
1 parent ab56fea commit 0c24063
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 15 deletions.
76 changes: 61 additions & 15 deletions rlcard/games/limitholdem/judger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from rlcard.games.limitholdem.utils import compare_hands
import numpy as np


class LimitholdemJudger(object):
''' The Judger class for Texas Hold'em
Expand All @@ -9,8 +11,7 @@ def __init__(self, np_random):
'''
self.np_random = np_random

@staticmethod
def judge_game(players, hands):
def judge_game(self, players, hands):
''' Judge the winner of the game.
Args:
Expand All @@ -26,22 +27,67 @@ def judge_game(players, hands):
h = [card.get_index() for card in hand]
hands[i] = h

#winners = compare_hands(hands)
#winners = [1, 0, 0]
winners = compare_hands(hands)

# Compute the total chips
total = 0
for p in players:
total += p.in_chips

each_win = float(total) / sum(winners)
in_chips = [p.in_chips for p in players]
each_win = self.split_pots_among_players(in_chips, winners)

payoffs = []
for i, _ in enumerate(players):
if winners[i] == 1:
payoffs.append(each_win - players[i].in_chips)
else:
payoffs.append(float(-players[i].in_chips))

payoffs.append(each_win[i] - in_chips[i])
assert sum(payoffs) == 0
return payoffs

def split_pot_among_players(self, in_chips, winners):
"""
to split the next (side)pot among players (
this function is called in loop by distribute_pots_among_players until all chips are allocated
:param in_chips: list with number of chips bet not yet distributed for each player
:param winners: list with 1 if the player is among winners else 0
:return: list of how much chips each player get after this pot has been split and list of chips left to distribute
"""
nb_winners_in_pot = sum((winners[i] and in_chips[i]>0) for i in range(len(in_chips)))
nb_players_in_pot = sum(in_chips[i]>0 for i in range(len(in_chips)))
if nb_winners_in_pot == 0 or nb_winners_in_pot == nb_players_in_pot:
# no winner or all winners for this pot
allocated = list(in_chips) # we give back their chips to each players in this pot
in_chips_after = len(in_chips)*[0] # no more chips to distribute
else:
amount_in_pot_by_player = min(v for v in in_chips if v > 0)
how_much_one_win, remaining = divmod(amount_in_pot_by_player*nb_players_in_pot, nb_winners_in_pot)
# In the event of a split pot that cannot be divided equally for every winner,
# the winner who is sitting closest to the left of the dealer receives the remaining differential in chips
# cf https://www.betclic.fr/poker/house-rules--play-safely--betclic-poker-cpok_rules
# to simplify and as this case is very rare, we will give the remaining differential in chips to a random winner
allocated = len(in_chips) * [0]
in_chips_after = list(in_chips)
for i in range(len(in_chips)): # iterate on all players
if in_chips[i] == 0: # player not in pot
continue
if winners[i]:
allocated[i] += how_much_one_win
in_chips_after[i] -= amount_in_pot_by_player
if remaining > 0:
random_winning_player = self.np_random.choice([i for i in range(len(winners)) if winners[i] and in_chips[i]>0])
allocated[random_winning_player] += remaining
assert sum(in_chips[i]-in_chips_after[i] for i in range(len(in_chips))) == sum(allocated)
return allocated, in_chips_after

def split_pots_among_players(self, in_chips_initial, winners):
"""
to split main pot and side pots among players (to handle special case of all-in players)
:param in_chips_initial: list with number of chips bet for each player
:param winners: list with 1 if the player is among winners else 0
:return: list of how much chips each player get back after all pots have been split
"""
in_chips = list(in_chips_initial)
assert len(in_chips) == len(winners)
assert all(v==0 or v==1 for v in winners)
assert sum(winners) >= 1 # there must be at least one winner
allocated = np.zeros(len(in_chips), dtype=int)
while any(v > 0 for v in in_chips): # while there are still chips to allocate
allocated_current_pot, in_chips = self.split_pot_among_players(in_chips, winners)
allocated += allocated_current_pot # element-wise addition
assert all(chips >= 0 for chips in allocated) # check that all players got a non negative amount of chips
assert sum(in_chips_initial) == sum(allocated) # check that all chips bet have been allocated
return list(allocated)
66 changes: 66 additions & 0 deletions tests/utils/test_holdem_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import itertools
import unittest

from rlcard.games.limitholdem.judger import LimitholdemJudger
from rlcard.games.limitholdem.utils import compare_hands
from rlcard.games.limitholdem.utils import Hand as Hand
import numpy as np
''' Combinations selected for testing compare_hands function
Royal straight flush ['CJ', 'CT', 'CQ', 'CK', 'C9', 'C8', 'CA']
Straight flush ['CJ', 'CT', 'CQ', 'CK', 'C9', 'C8', 'C7']
Expand Down Expand Up @@ -287,6 +291,68 @@ def test_compare_hands(self):
])
self.assertEqual(winner, [0, 0, 1, 1])

def test_split_pots_among_players(self):
j = LimitholdemJudger(np.random.RandomState(seed=7))

# simple cases where all players bet same amount of chips
self.assertEqual(j.split_pots_among_players([2, 2], [0, 1]), [0, 4])
self.assertEqual(j.split_pots_among_players([2, 2], [1, 0]), [4, 0])
self.assertEqual(j.split_pots_among_players([2, 2], [1, 1]), [2, 2])
self.assertEqual(j.split_pots_among_players([2, 2, 2], [1, 0, 0]), [6, 0, 0])
self.assertEqual(j.split_pots_among_players([2, 2, 2], [0, 1, 0]), [0, 6, 0])
self.assertEqual(j.split_pots_among_players([2, 2, 2], [0, 0, 1]), [0, 0, 6])
self.assertEqual(j.split_pots_among_players([2, 2, 2], [1, 0, 1]), [3, 0, 3])
self.assertEqual(j.split_pots_among_players([2, 2, 2], [0, 1, 1]), [0, 3, 3])
self.assertEqual(j.split_pots_among_players([2, 2, 2], [1, 1, 0]), [3, 3, 0])
self.assertEqual(j.split_pots_among_players([2, 2, 2], [1, 1, 1]), [2, 2, 2])
self.assertEqual(j.split_pots_among_players([3, 3, 3], [0, 1, 1]), [0, 4, 5])
# for the case above 9 is not divisible by 2 so a random winner get the remainder

# trickier cases with different amounts bet (some players are all in)
self.assertEqual(j.split_pots_among_players([3, 2], [0, 1]), [1, 4])
self.assertEqual(j.split_pots_among_players([3, 2], [1, 0]), [5, 0])
self.assertEqual(j.split_pots_among_players([3, 2], [1, 1]), [3, 2])
self.assertEqual(j.split_pots_among_players([2, 4, 4], [1, 0, 0]), [6, 2, 2])
self.assertEqual(j.split_pots_among_players([2, 4, 4], [0, 1, 0]), [0, 10, 0])
self.assertEqual(j.split_pots_among_players([2, 4, 4], [0, 0, 1]), [0, 0, 10])
self.assertEqual(j.split_pots_among_players([2, 4, 4], [1, 1, 0]), [3, 7, 0])
self.assertEqual(j.split_pots_among_players([2, 4, 4], [1, 0, 1]), [3, 0, 7])
self.assertEqual(j.split_pots_among_players([2, 4, 4], [0, 1, 1]), [0, 5, 5])
self.assertEqual(j.split_pots_among_players([2, 4, 4], [1, 1, 1]), [2, 4, 4])
self.assertEqual(j.split_pots_among_players([1, 1, 2, 2, 3, 3], [0, 1, 0, 1, 0, 1]), [0, 2, 0, 4, 0, 6])

def test_split_pots_among_players_cases_generated(self):
def check_result(in_chips, winners, allocated):
"""check that winners have won chips (more chips in allocated than in in_chips)
and than losers have lost chips (strictly less chips in allocated than in chips)"""
assert sum(allocated) == sum(in_chips)
for i in range(len(in_chips)):
if winners[i]:
self.assertGreaterEqual(allocated[i], in_chips[i])
# can be equal for example with 2 winners and 1 loser who has bet one chip (not divisible by 2)
# so the winner who does not get the chip of the loser will have allocated[i] == in_chips[i]
elif in_chips[i] > 0:
self.assertLess(allocated[i], in_chips[i])
# because there is at least one winner so a loser who bet must lose at least one chip

randstate = np.random.RandomState(seed=7)
j = LimitholdemJudger(randstate)

# test many random cases from 2 to 6 players with all winners combinations
nb_cases = 0
for nb_players in range(2, 7):
for _ in range(300):
in_chips = [randstate.randint(0, 10) for _ in range(nb_players)]
for winners in itertools.product([0, 1], repeat=nb_players):
if sum(winners) == 0:
continue # impossible case with no winner
if sum(w * v for w, v in zip(winners, in_chips)) == 0:
continue # impossible case where all winners have not bet
allocated = j.split_pots_among_players(in_chips, winners)
nb_cases += 1
check_result(in_chips, winners, allocated)
self.assertEqual(nb_cases, 34954) # to check that correct number of cases have been tested


if __name__ == '__main__':
unittest.main()

0 comments on commit 0c24063

Please sign in to comment.