Skip to content

Commit

Permalink
🚀 Add docstring, optimize game logic, enhance logging
Browse files Browse the repository at this point in the history
  • Loading branch information
Skripkon committed Jan 5, 2025
1 parent d51bf22 commit f37bdbe
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 67 deletions.
187 changes: 122 additions & 65 deletions PokerBots/game/Game.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,51 @@
from pokerkit import Automation, NoLimitTexasHoldem
from PokerBots.players.BasePlayer import BasePlayer
from PokerBots.players.CallingPlayer import CallingPlayer
from PokerBots.players.RandomPlayer import RandomPlayer

class Game:
"""
Represents a game of No-Limit Texas Hold'em poker.
Attributes:
players (list[BasePlayer]): List of player objects participating in the game.
n_players (int): Number of players in the game.
stacks (list[float]): Current stack for each player.
state (NoLimitTexasHoldem.State): Current game state.
"""

def __init__ (self, initial_stack: float = 30_000, players: list[BasePlayer] = None):
self.players = players
"""
Initializes the Game instance with the given players and initial stack (the same for each player).
Args:
initial_stack (float): Starting chip count for each player. Default is 30,000.
players (list[BasePlayer]): List of player objects. Default is [RandomPlayer(), CallingPlayer()].
"""
if players is None:
self.players = [RandomPlayer(), CallingPlayer()]
else:
self.players = players
self.n_players = len(players)
self.stacks = [initial_stack] * self.n_players

self.state = None

def play_round(self, verbose: bool = True):
"""
Plays a single round of No-Limit Texas Hold'em poker.
Args:
verbose (bool): If True, logs detailed information about the round. Default is True.
Returns:
bool: True if the game is over, otherwise False.
"""
if verbose:
print("INFO: ROUND STARTS")
print(f"INFO: stacks = {self.stacks}")

# Initialize the game state
self.state = NoLimitTexasHoldem.create_state(
(
Automation.ANTE_POSTING,
Expand All @@ -21,92 +56,85 @@ def play_round(self, verbose: bool = True):
Automation.CHIPS_PUSHING,
Automation.CHIPS_PULLING,
),
True, # Uniform antes?
True, # Uniform antes
500, # Antes
(1000, 2000), # Blinds or straddles
2000, # Min-bet
self.stacks, # Starting stacks
self.n_players, # Number of players
)

# Deal hole cards and log initial game state
self.__deal_cards()
if verbose:
self.__log_posting_of_blinds_or_straddles()

# Preflop
self.__play_street(verbose=verbose)

# Flop
self.__try_to_burn_and_deal_cards(n_cards=3)

if self.state.actor_index is not None:
self.__play_street(verbose=verbose)
# Play the streets (Preflop, Flop, Turn, River)
for street_name, cards_to_deal in [("PREFLOP", 0), ("FLOP", 3), ("TURN", 1), ("RIVER", 1)]:
if cards_to_deal > 0:
# Dealing might be impossible if all players except for one folded.
self.__try_to_burn_and_deal_cards(n_cards=cards_to_deal)

# Tern
self.__try_to_burn_and_deal_cards(n_cards=1)
if self.state.actor_index is not None:
if verbose:
print(f"INFO: ===== {street_name} =====")
self.__play_street(verbose=verbose)

# River
self.__try_to_burn_and_deal_cards(n_cards=1)
if self.state.actor_index is not None:
self.__play_street(verbose=verbose)
else:
# River
self.__try_to_burn_and_deal_cards(n_cards=1)
else:
# Tern
self.__try_to_burn_and_deal_cards(n_cards=1)
# River
self.__try_to_burn_and_deal_cards(n_cards=1)

# Update stacks
# Update stacks and log results
self.stacks = self.state.stacks

# Remove players with zero stack
self.__remove_bankrupt_players(verbose=verbose)

if verbose:
self.__log_winner()
self.__log_results()

# Remove bankrupt players and check if the game is over
self.__remove_bankrupt_players()
return self.__check_if_game_is_over(verbose=verbose)

# Check if Game is over
game_is_over: bool = self.__check_if_game_is_over(verbose=verbose)
return game_is_over

def __deal_cards(self):
""" Deals the hole cards for all players. """
"""
Deals two hole cards to each player in the game.
"""
for _ in range(self.state.player_count):
self.state.deal_hole(2)

def __play_street(self, verbose: bool = True):
"""
Manages the betting actions of players for a single betting street.
Args:
verbose (bool): Whether to log player actions. Default is True.
"""
while self.state.actor_index is not None:
current_player_idx = self.state.actor_index
valid_actions = self.__get_valid_actions()
action, amount = self.players[current_player_idx].play(valid_actions=valid_actions, state=self.state)

if verbose:
match action:
case "fold":
self.state.fold()
match action:
case "fold":
self.state.fold()
if verbose:
print(f"INFO: Player {self.players[current_player_idx].name} folds.")
case "check_or_call":
self.state.check_or_call()
print(f"INFO: Player {self.players[current_player_idx].name} checks/calls.")
case "complete_bet_or_raise_to":
self.state.complete_bet_or_raise_to(amount=amount)
case "check_or_call":
self.state.check_or_call()
if verbose:
if amount == 0:
print(f"INFO: Player {self.players[current_player_idx].name} checks.")
else:
print(f"INFO: Player {self.players[current_player_idx].name} calls {amount}.")
case "complete_bet_or_raise_to":
self.state.complete_bet_or_raise_to(amount=amount)
if verbose:
print(f"INFO: Player {self.players[current_player_idx].name} raises to {amount}")
case _:
raise ValueError(f"Unknown action: {action}. Valid actions are ['fold', 'check_or_call', 'complete_bet_or_raise_to']")
else:
match action:
case "fold":
self.state.fold()
case "check_or_call":
self.state.check_or_call()
case "complete_bet_or_raise_to":
self.state.complete_bet_or_raise_to(amount=amount)
case _:
raise ValueError(f"Unknown action: {action}. Valid actions are ['fold', 'check_or_call', 'complete_bet_or_raise_to']")
case _:
raise ValueError(f"Unknown action: {action}. Valid actions are ['fold', 'check_or_call', 'complete_bet_or_raise_to']")

def __get_valid_actions(self):
"""
Determines the valid actions available to the current player.
Returns:
dict: A dictionary mapping action names to their respective amounts or ranges.
"""
valid_actions = {"fold": 0}
if self.state.can_check_or_call():
valid_actions["check_or_call"] = self.state.checking_or_calling_amount
Expand All @@ -116,12 +144,10 @@ def __get_valid_actions(self):

return valid_actions

def __remove_bankrupt_players(self, verbose: bool = True):
if verbose:
for idx, stack in enumerate(self.stacks):
if stack == 0:
print(f"INFO: Player {self.players[idx].name} lost his stack.")

def __remove_bankrupt_players(self):
"""
Removes players with zero stack from the game.
"""
self.stacks, self.players = zip(
*[(stack, player) for stack, player in zip(self.stacks, self.players) if stack > 0]
)
Expand All @@ -131,19 +157,50 @@ def __remove_bankrupt_players(self, verbose: bool = True):


def __check_if_game_is_over(self, verbose: bool = True):
"""
Checks whether the game is over (only one player remains).
Args:
verbose (bool): Whether to log the winner. Default is True.
Returns:
bool: True if the game is over, False otherwise.
"""
if len(self.stacks) == 1:
if verbose:
print(f"INFO: Player {self.players[0].name} won the Tournament.")
return True

return False

def __log_winner(self):
def __log_results(self):
"""
Logs the results of the round, including winnings or losses for each player.
"""
print("INFO: ===== ROUND RESULTS =====")
for idx in range(self.n_players):
if self.state.can_win_now(idx):
print(f"INFO: Player {self.players[idx].name} won.")
payoff = self.state.payoffs[idx]
if payoff >= 0:
print(f"INFO: Player {self.players[idx].name} won {payoff}")
else:
print(f"INFO: Player {self.players[idx].name} lost {-payoff}")
print("==========================================")

def __try_to_burn_and_deal_cards(self, n_cards: int = 1):
"""
Attempts to burn one card and deal the specified number of community cards.
Args:
n_cards (int): Number of community cards to deal. Default is 1.
"""
if self.state.can_burn_card():
self.state.burn_card()
self.state.deal_board(n_cards)

def __log_posting_of_blinds_or_straddles(self):
"""
Logs the posting of blinds or straddles at the start of the round.
"""
for idx, straddle in enumerate(self.state.blinds_or_straddles):
if straddle > 0:
print(f"INFO: Player {self.players[idx].name} bets {straddle}.")
4 changes: 2 additions & 2 deletions tests/test_players.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ def create_random_players():


# Test with calling players
def test_multiple_game_simulations_with_calling_players(num_simulations=10, rounds=10):
def test_multiple_game_simulations_with_calling_players(num_simulations=100, rounds=10):
simulate_multiple_games(create_calling_players(), num_simulations, rounds, verbose=True)
simulate_multiple_games(create_calling_players(), num_simulations, rounds, verbose=False)


# Test with random players
def test_multiple_game_simulations_with_random_players(num_simulations=10, rounds=10):
def test_multiple_game_simulations_with_random_players(num_simulations=100, rounds=10):
simulate_multiple_games(create_random_players(), num_simulations, rounds, verbose=True)
simulate_multiple_games(create_random_players(), num_simulations, rounds, verbose=False)

0 comments on commit f37bdbe

Please sign in to comment.