diff --git a/rlcard/envs/__init__.py b/rlcard/envs/__init__.py index de9dbb8c1..6cafd110f 100644 --- a/rlcard/envs/__init__.py +++ b/rlcard/envs/__init__.py @@ -23,6 +23,11 @@ entry_point='rlcard.envs.nolimitholdem:NolimitholdemEnv', ) +register( + env_id='fixed-no-limit-holdem', + entry_point='rlcard.envs.fixednolimitholdem:FixedNolimitholdemEnv', +) + register( env_id='leduc-holdem', entry_point='rlcard.envs.leducholdem:LeducholdemEnv' diff --git a/rlcard/envs/env.py b/rlcard/envs/env.py index 93e239548..300eb37f1 100644 --- a/rlcard/envs/env.py +++ b/rlcard/envs/env.py @@ -30,7 +30,7 @@ def __init__(self, config): # Game specific configurations # Currently only support blackjack、limit-holdem、no-limit-holdem # TODO support game configurations for all the games - supported_envs = ['blackjack', 'leduc-holdem', 'limit-holdem', 'no-limit-holdem'] + supported_envs = ['blackjack', 'leduc-holdem', 'limit-holdem', 'no-limit-holdem', 'fixed-no-limit-holdem'] if self.name in supported_envs: _game_config = self.default_game_config.copy() for key in config: diff --git a/rlcard/envs/fixednolimitholdem.py b/rlcard/envs/fixednolimitholdem.py new file mode 100644 index 000000000..43e37fc50 --- /dev/null +++ b/rlcard/envs/fixednolimitholdem.py @@ -0,0 +1,119 @@ +import json +import os +import numpy as np +from collections import OrderedDict + +import rlcard +from rlcard.envs import Env +from rlcard.games.fixednolimitholdem import Game +from rlcard.games.fixednolimitholdem.round import Action + +DEFAULT_GAME_CONFIG = { + 'game_num_players': 2, + 'chips_for_each': 200, + 'dealer_id': None, + } + +class FixedNolimitholdemEnv(Env): + ''' FixedNolimitholdem Environment + ''' + + def __init__(self, config): + ''' Initialize the Limitholdem environment + ''' + self.name = 'fixed-no-limit-holdem' + self.default_game_config = DEFAULT_GAME_CONFIG + self.game = Game() + super().__init__(config) + self.actions = Action + self.state_shape = [[54] for _ in range(self.num_players)] + self.action_shape = [None for _ in range(self.num_players)] + # for raise_amount in range(1, self.game.init_chips+1): + # self.actions.append(raise_amount) + + with open(os.path.join(rlcard.__path__[0], 'games/limitholdem/card2index.json'), 'r') as file: + self.card2index = json.load(file) + + def _get_legal_actions(self): + ''' Get all leagal actions + + Returns: + encoded_action_list (list): return encoded legal action list (from str to int) + ''' + return self.game.get_legal_actions() + + def _extract_state(self, state): + ''' Extract the state representation from state dictionary for agent + + Note: Currently the use the hand cards and the public cards. TODO: encode the states + + Args: + state (dict): Original state from the game + + Returns: + observation (list): combine the player's score and dealer's observable score for observation + ''' + extracted_state = {} + + legal_actions = OrderedDict({action.value: None for action in state['legal_actions']}) + extracted_state['legal_actions'] = legal_actions + + public_cards = state['public_cards'] + hand = state['hand'] + my_chips = state['my_chips'] + all_chips = state['all_chips'] + cards = public_cards + hand + idx = [self.card2index[card] for card in cards] + obs = np.zeros(54) + obs[idx] = 1 + obs[52] = float(my_chips) + obs[53] = float(max(all_chips)) + extracted_state['obs'] = obs + + extracted_state['raw_obs'] = state + extracted_state['raw_legal_actions'] = [a for a in state['legal_actions']] + extracted_state['action_record'] = self.action_recorder + + return extracted_state + + def get_payoffs(self): + ''' Get the payoff of a game + + Returns: + payoffs (list): list of payoffs + ''' + return np.array(self.game.get_payoffs()) + + def _decode_action(self, action_id): + ''' Decode the action for applying to the game + + Args: + action id (int): action id + + Returns: + action (str): action for the game + ''' + legal_actions = self.game.get_legal_actions() + if self.actions(action_id) not in legal_actions: + if Action.CHECK in legal_actions: + return Action.CHECK + else: + print("Tried non legal action", action_id, self.actions(action_id), legal_actions) + return Action.FOLD + return self.actions(action_id) + + def get_perfect_information(self): + ''' Get the perfect information of the current state + + Returns: + (dict): A dictionary of all the perfect information of the current state + ''' + state = {} + state['chips'] = [self.game.players[i].in_chips for i in range(self.num_players)] + state['public_card'] = [c.get_index() for c in self.game.public_cards] if self.game.public_cards else None + state['hand_cards'] = [[c.get_index() for c in self.game.players[i].hand] for i in range(self.num_players)] + state['current_player'] = self.game.game_pointer + state['legal_actions'] = self.game.get_legal_actions() + return state + + diff --git a/rlcard/games/fixednolimitholdem/__init__.py b/rlcard/games/fixednolimitholdem/__init__.py new file mode 100644 index 000000000..0c3951b37 --- /dev/null +++ b/rlcard/games/fixednolimitholdem/__init__.py @@ -0,0 +1,7 @@ +from rlcard.games.fixednolimitholdem.dealer import NolimitholdemDealer as Dealer +from rlcard.games.fixednolimitholdem.judger import NolimitholdemJudger as Judger +from rlcard.games.fixednolimitholdem.player import NolimitholdemPlayer as Player +from rlcard.games.fixednolimitholdem.round import Action +from rlcard.games.fixednolimitholdem.round import NolimitholdemRound as Round +from rlcard.games.fixednolimitholdem.game import NolimitholdemGame as Game + diff --git a/rlcard/games/fixednolimitholdem/dealer.py b/rlcard/games/fixednolimitholdem/dealer.py new file mode 100644 index 000000000..16b87682c --- /dev/null +++ b/rlcard/games/fixednolimitholdem/dealer.py @@ -0,0 +1,136 @@ +from rlcard.games.limitholdem import Dealer + + +class NolimitholdemDealer(Dealer): + def __init__(self, np_random): + super().__init__(np_random) + # Initialize new properties with default values + self.preset_player0_hand = [] + self.preset_flop = [] + self.preset_turn = None + self.preset_river = None + self.current_stage = None + self.manual_mode = False # Default to automatic dealing + self.player0_cards_dealt = 0 # Track how many cards have been dealt to player 0 + + def enable_manual_mode(self): + """Enable manual card selection mode""" + self.manual_mode = True + + def set_player0_hand(self, cards): + """Set specific cards for Player 0's hand + + Args: + cards (list): List of card objects to be used as Player 0's hand + """ + if not self.manual_mode: + return # Do nothing if not in manual mode + + self.preset_player0_hand = cards.copy() # Make a copy to avoid modifying the original + # Remove these cards from the deck + for card in cards: + if card in self.deck: + self.deck.remove(card) + + def set_flop(self, cards): + """Set specific flop cards + + Args: + cards (list): List of 3 card objects to be used as flop + """ + if not self.manual_mode: + return # Do nothing if not in manual mode + + if len(cards) != 3: + raise ValueError("Flop must consist of exactly 3 cards") + self.preset_flop = cards.copy() # Make a copy to avoid modifying the original + # Remove these cards from the deck + for card in cards: + if card in self.deck: + self.deck.remove(card) + + def set_turn(self, card): + """Set specific turn card + + Args: + card (object): Card object to be used as turn + """ + if not self.manual_mode: + return # Do nothing if not in manual mode + + self.preset_turn = card + # Remove this card from the deck + if card in self.deck: + self.deck.remove(card) + + def set_river(self, card): + """Set specific river card + + Args: + card (object): Card object to be used as river + """ + if not self.manual_mode: + return # Do nothing if not in manual mode + + self.preset_river = card + # Remove this card from the deck + if card in self.deck: + self.deck.remove(card) + + def has_preset_cards(self, stage): + """Check if dealer has preset cards for the given stage + + Args: + stage (str): The stage to check ('flop', 'turn', or 'river') + + Returns: + (bool): True if dealer has preset cards for the stage + """ + if not self.manual_mode: + return True # In automatic mode, we always have cards + + if stage == 'flop': + return len(self.preset_flop) == 3 + elif stage == 'turn': + return self.preset_turn is not None + elif stage == 'river': + return self.preset_river is not None + return False + + def deal_card(self, player_id=None): + """Deal a card from the deck + + Args: + player_id (int, optional): The ID of the player to deal to + + Returns: + (object): A card object + """ + # Only use preset cards if in manual mode + if self.manual_mode and player_id == 0 and self.player0_cards_dealt < 2 and len(self.preset_player0_hand) > 0: + # For player 0's hand (first two cards) + card = self.preset_player0_hand.pop(0) + self.player0_cards_dealt += 1 + return card + + # For community cards based on current stage + if self.manual_mode: + if self.current_stage == 'flop' and len(self.preset_flop) > 0: + return self.preset_flop.pop(0) + elif self.current_stage == 'turn' and self.preset_turn is not None: + card = self.preset_turn + self.preset_turn = None + return card + elif self.current_stage == 'river' and self.preset_river is not None: + card = self.preset_river + self.preset_river = None + return card + + # Default behavior - deal from deck + return super().deal_card() + + def shuffle(self): + """Shuffle the deck""" + super().shuffle() + # Reset the player0_cards_dealt counter when shuffling + self.player0_cards_dealt = 0 diff --git a/rlcard/games/fixednolimitholdem/game.py b/rlcard/games/fixednolimitholdem/game.py new file mode 100644 index 000000000..d7d0c056d --- /dev/null +++ b/rlcard/games/fixednolimitholdem/game.py @@ -0,0 +1,432 @@ +from enum import Enum +import numpy as np +from copy import deepcopy +from rlcard.games.limitholdem import Game +from rlcard.games.limitholdem import PlayerStatus + +from rlcard.games.fixednolimitholdem import Dealer +from rlcard.games.fixednolimitholdem import Player +from rlcard.games.fixednolimitholdem import Judger +from rlcard.games.fixednolimitholdem import Round, Action + + +class Stage(Enum): + PREFLOP = 0 + FLOP = 1 + TURN = 2 + RIVER = 3 + END_HIDDEN = 4 + SHOWDOWN = 5 + WAITING_FOR_FLOP = 6 # Waiting for flop cards + WAITING_FOR_TURN = 7 # Waiting for turn card + WAITING_FOR_RIVER = 8 # Waiting for river card + + +class NolimitholdemGame(Game): + def __init__(self, allow_step_back=False, num_players=2): + """Initialize the class no limit holdem Game""" + super().__init__(allow_step_back, num_players) + + self.np_random = np.random.RandomState() + + # small blind and big blind + self.small_blind = 1 + self.big_blind = 2 * self.small_blind + + # config players + self.init_chips = [self.big_blind * 100] * num_players + + # If None, the dealer will be randomly chosen + self.dealer_id = None + + # For manually setting cards + self.player0_hand = [] + self.manual_dealer = False # Default to automatic dealing + + def configure(self, game_config): + """ + Specify some game specific parameters, such as number of players, initial chips, and dealer id. + If dealer_id is None, he will be randomly chosen + """ + self.num_players = game_config['game_num_players'] + # must have num_players length + self.init_chips = [game_config['chips_for_each']] * game_config["game_num_players"] + self.dealer_id = game_config['dealer_id'] + + # Check for manual dealer flag + self.manual_dealer = game_config.get('manual_dealer', False) + + # Set player0 hand if provided and manual dealer is enabled + if self.manual_dealer and 'player0_hand' in game_config: + self.player0_hand = game_config['player0_hand'] + + def init_game(self): + """ + Initialize the game of not limit holdem + + This version supports two-player no limit texas holdem + + Returns: + (tuple): Tuple containing: + + (dict): The first state of the game + (int): Current player's id + """ + if self.dealer_id is None: + self.dealer_id = self.np_random.randint(0, self.num_players) + else: + self.dealer_id = (self.dealer_id + 1) % self.num_players + + # Initialize a dealer that can deal cards + self.dealer = Dealer(self.np_random) + + # Enable manual mode if configured + if self.manual_dealer: + if hasattr(self.dealer, 'enable_manual_mode'): + self.dealer.enable_manual_mode() + + # Set preset cards for Player 0's hand if provided + if self.player0_hand and hasattr(self.dealer, 'set_player0_hand'): + self.dealer.set_player0_hand(self.player0_hand) + + # Initialize players to play the game + self.players = [Player(i, self.init_chips[i], self.np_random) for i in range(self.num_players)] + + # Initialize a judger class which will decide who wins in the end + self.judger = Judger(self.np_random) + + # Deal cards to each player to prepare for the first round + for i in range(self.num_players): + for _ in range(2): # Each player gets 2 cards + # Pass player_id to deal_card if the dealer supports it + if hasattr(self.dealer, 'deal_card') and 'player_id' in self.dealer.deal_card.__code__.co_varnames: + card = self.dealer.deal_card(player_id=i) + else: + card = self.dealer.deal_card() + self.players[i].hand.append(card) + + # Initialize public cards + self.public_cards = [] + self.stage = Stage.PREFLOP + + # Big blind and small blind + if self.num_players == 2: + # In heads-up dealer posts small blind + s = (self.dealer_id) % self.num_players + b = (self.dealer_id + 1) % self.num_players + else: + s = (self.dealer_id + 1) % self.num_players + b = (self.dealer_id + 2) % self.num_players + + self.players[b].bet(chips=self.big_blind) + self.players[s].bet(chips=self.small_blind) + + # The player next to the big blind plays the first + self.game_pointer = (b + 1) % self.num_players + + # Initialize a bidding round, in the first round, the big blind and the small blind needs to + # be passed to the round for processing. + self.round = Round(self.num_players, self.big_blind, dealer=self.dealer, np_random=self.np_random) + + self.round.start_new_round(game_pointer=self.game_pointer, raised=[p.in_chips for p in self.players]) + + # Count the round. There are 4 rounds in each game. + self.round_counter = 0 + + # Save the history for stepping back to the last state. + self.history = [] + + state = self.get_state(self.game_pointer) + + return state, self.game_pointer + + def set_flop(self, cards): + """Set specific flop cards + + Args: + cards (list): List of 3 card objects to be used as flop + """ + if not self.manual_dealer: + return # Do nothing if not in manual mode + + if hasattr(self.dealer, 'set_flop'): + self.dealer.set_flop(cards) + + # If we were waiting for flop cards, resume the game + if self.stage == Stage.WAITING_FOR_FLOP: + self._deal_flop() + self.stage = Stage.FLOP + + def set_turn(self, card): + """Set specific turn card + + Args: + card (object): Card object to be used as turn + """ + if not self.manual_dealer: + return # Do nothing if not in manual mode + + if hasattr(self.dealer, 'set_turn'): + self.dealer.set_turn(card) + + # If we were waiting for turn card, resume the game + if self.stage == Stage.WAITING_FOR_TURN: + self._deal_turn() + self.stage = Stage.TURN + + def set_river(self, card): + """Set specific river card + + Args: + card (object): Card object to be used as river + """ + if not self.manual_dealer: + return # Do nothing if not in manual mode + + if hasattr(self.dealer, 'set_river'): + self.dealer.set_river(card) + + # If we were waiting for river card, resume the game + if self.stage == Stage.WAITING_FOR_RIVER: + self._deal_river() + self.stage = Stage.RIVER + + def _deal_flop(self): + """Deal the flop cards""" + if hasattr(self.dealer, 'current_stage'): + self.dealer.current_stage = 'flop' + self.public_cards.append(self.dealer.deal_card()) + self.public_cards.append(self.dealer.deal_card()) + self.public_cards.append(self.dealer.deal_card()) + + def _deal_turn(self): + """Deal the turn card""" + if hasattr(self.dealer, 'current_stage'): + self.dealer.current_stage = 'turn' + self.public_cards.append(self.dealer.deal_card()) + + def _deal_river(self): + """Deal the river card""" + if hasattr(self.dealer, 'current_stage'): + self.dealer.current_stage = 'river' + self.public_cards.append(self.dealer.deal_card()) + + def get_legal_actions(self): + """ + Return the legal actions for current player + + Returns: + (list): A list of legal actions + """ + # If we're waiting for manual cards, no betting actions are allowed + if self.stage in (Stage.WAITING_FOR_FLOP, Stage.WAITING_FOR_TURN, Stage.WAITING_FOR_RIVER): + return [] # No betting actions allowed while waiting for cards + + # Check if all players are either folded or all-in + players_in_bypass = [1 if player.status in (PlayerStatus.FOLDED, PlayerStatus.ALLIN) else 0 for player in self.players] + if sum(players_in_bypass) == self.num_players: + return [] # No actions allowed when all players are all-in or folded + + return self.round.get_nolimit_legal_actions(players=self.players) + + def step(self, action): + """ + Get the next state + + Args: + action (str): a specific action. (call, raise, fold, or check) + + Returns: + (tuple): Tuple containing: + + (dict): next player's state + (int): next player id + """ + # If we're waiting for manual cards, don't allow any actions + if self.stage in (Stage.WAITING_FOR_FLOP, Stage.WAITING_FOR_TURN, Stage.WAITING_FOR_RIVER): + raise Exception('Cannot take actions while waiting for manual cards') + + if action not in self.get_legal_actions(): + print(action, self.get_legal_actions()) + print(self.get_state(self.game_pointer)) + raise Exception('Action not allowed') + + if self.allow_step_back: + # First snapshot the current state + r = deepcopy(self.round) + b = self.game_pointer + r_c = self.round_counter + d = deepcopy(self.dealer) + p = deepcopy(self.public_cards) + ps = deepcopy(self.players) + self.history.append((r, b, r_c, d, p, ps)) + + # Then we proceed to the next round + self.game_pointer = self.round.proceed_round(self.players, action) + + players_in_bypass = [1 if player.status in (PlayerStatus.FOLDED, PlayerStatus.ALLIN) else 0 for player in self.players] + if self.num_players - sum(players_in_bypass) == 1: + last_player = players_in_bypass.index(0) + if self.round.raised[last_player] >= max(self.round.raised): + # If the last player has put enough chips, he is also bypassed + players_in_bypass[last_player] = 1 + + # Check if all players are either folded or all-in + if sum(players_in_bypass) == self.num_players: + # Skip all betting rounds and deal all remaining community cards + if self.stage == Stage.PREFLOP: + # Deal flop + self._deal_flop() + # Deal turn + self._deal_turn() + # Deal river + self._deal_river() + self.stage = Stage.SHOWDOWN + self.round_counter = 3 # Set to end of river + elif self.stage == Stage.FLOP: + # Deal turn + self._deal_turn() + # Deal river + self._deal_river() + self.stage = Stage.SHOWDOWN + self.round_counter = 3 # Set to end of river + elif self.stage == Stage.TURN: + # Deal river + self._deal_river() + self.stage = Stage.SHOWDOWN + self.round_counter = 3 # Set to end of river + elif self.stage == Stage.RIVER: + self.stage = Stage.SHOWDOWN + + # If a round is over, we deal more public cards + if self.round.is_over(): + # Game pointer goes to the first player not in bypass after the dealer, if there is one + self.game_pointer = (self.dealer_id + 1) % self.num_players + if sum(players_in_bypass) < self.num_players: + while players_in_bypass[self.game_pointer]: + self.game_pointer = (self.game_pointer + 1) % self.num_players + + # Handle the end of each betting round + if self.round_counter == 0: # End of preflop + if self.manual_dealer: + # Check if we have preset flop cards + if hasattr(self.dealer, 'has_preset_cards') and self.dealer.has_preset_cards('flop'): + self._deal_flop() + self.stage = Stage.FLOP + else: + # Wait for flop cards to be set + self.stage = Stage.WAITING_FOR_FLOP + else: + # Automatic dealer mode + self._deal_flop() + self.stage = Stage.FLOP + + elif self.round_counter == 1: # End of flop + if self.manual_dealer: + # Check if we have preset turn card + if hasattr(self.dealer, 'has_preset_cards') and self.dealer.has_preset_cards('turn'): + self._deal_turn() + self.stage = Stage.TURN + else: + # Wait for turn card to be set + self.stage = Stage.WAITING_FOR_TURN + else: + # Automatic dealer mode + self._deal_turn() + self.stage = Stage.TURN + + elif self.round_counter == 2: # End of turn + if self.manual_dealer: + # Check if we have preset river card + if hasattr(self.dealer, 'has_preset_cards') and self.dealer.has_preset_cards('river'): + self._deal_river() + self.stage = Stage.RIVER + else: + # Wait for river card to be set + self.stage = Stage.WAITING_FOR_RIVER + else: + # Automatic dealer mode + self._deal_river() + self.stage = Stage.RIVER + + # Increment round counter + self.round_counter += 1 + + # Start a new bidding round + self.round.start_new_round(self.game_pointer) + + state = self.get_state(self.game_pointer) + + return state, self.game_pointer + + def get_state(self, player_id): + """ + Return player's state + + Args: + player_id (int): player id + + Returns: + (dict): The state of the player + """ + self.dealer.pot = np.sum([player.in_chips for player in self.players]) + + chips = [self.players[i].in_chips for i in range(self.num_players)] + legal_actions = self.get_legal_actions() + state = self.players[player_id].get_state(self.public_cards, chips, legal_actions) + state['stakes'] = [self.players[i].remained_chips for i in range(self.num_players)] + state['current_player'] = self.game_pointer + state['pot'] = self.dealer.pot + state['stage'] = self.stage + + # Add waiting_for_cards flag for manual dealer mode + if self.stage in (Stage.WAITING_FOR_FLOP, Stage.WAITING_FOR_TURN, Stage.WAITING_FOR_RIVER): + state['waiting_for_cards'] = True + state['waiting_stage'] = self.stage + else: + state['waiting_for_cards'] = False + + return state + + def get_payoffs(self): + """ + Return the payoffs of the game + + Returns: + (list): Each entry corresponds to the payoff of one player + """ + hands = [p.hand + self.public_cards if p.status in (PlayerStatus.ALIVE, PlayerStatus.ALLIN) else None for p in self.players] + chips_payoffs = self.judger.judge_game(self.players, hands) + return chips_payoffs + + def get_num_players(self): + """ + Return the number of players in no limit texas holdem + + Returns: + (int): The number of players in the game + """ + return self.num_players + + def step_back(self): + """ + Return to the previous state of the game + + Returns: + (bool): True if the game steps back successfully + """ + if len(self.history) > 0: + self.round, self.game_pointer, self.round_counter, self.dealer, self.public_cards, self.players = self.history.pop() + self.stage = Stage(self.round_counter) + return True + return False + + @staticmethod + def get_num_actions(): + """ + Return the number of applicable actions + + Returns: + (int): The number of actions. There are 6 actions (call, raise_half_pot, raise_pot, all_in, check and fold) + """ + return len(Action) diff --git a/rlcard/games/fixednolimitholdem/judger.py b/rlcard/games/fixednolimitholdem/judger.py new file mode 100644 index 000000000..0c9c427e7 --- /dev/null +++ b/rlcard/games/fixednolimitholdem/judger.py @@ -0,0 +1,5 @@ +from rlcard.games.limitholdem import Judger + + +class NolimitholdemJudger(Judger): + pass diff --git a/rlcard/games/fixednolimitholdem/player.py b/rlcard/games/fixednolimitholdem/player.py new file mode 100644 index 000000000..c63cc6e89 --- /dev/null +++ b/rlcard/games/fixednolimitholdem/player.py @@ -0,0 +1,19 @@ +from rlcard.games.limitholdem import Player + + +class NolimitholdemPlayer(Player): + def __init__(self, player_id, init_chips, np_random): + """ + Initialize a player. + + Args: + player_id (int): The id of the player + init_chips (int): The number of chips the player has initially + """ + super().__init__(player_id, np_random) + self.remained_chips = init_chips + + def bet(self, chips): + quantity = chips if chips <= self.remained_chips else self.remained_chips + self.in_chips += quantity + self.remained_chips -= quantity diff --git a/rlcard/games/fixednolimitholdem/round.py b/rlcard/games/fixednolimitholdem/round.py new file mode 100644 index 000000000..c07fca4de --- /dev/null +++ b/rlcard/games/fixednolimitholdem/round.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +"""Implement no limit texas holdem Round class""" +from enum import Enum + +from rlcard.games.limitholdem import PlayerStatus + + +class Action(Enum): + FOLD = 0 + CHECK = 1 + CALL = 2 + # RAISE_3BB = 3 + RAISE_HALF_POT = 3 + RAISE_POT = 4 + # RAISE_2POT = 5 + ALL_IN = 5 + # SMALL_BLIND = 7 + # BIG_BLIND = 8 + + +class NolimitholdemRound: + """Round can call functions from other classes to keep the game running""" + + def __init__(self, num_players, init_raise_amount, dealer, np_random): + """ + Initialize the round class + + Args: + num_players (int): The number of players + init_raise_amount (int): The min raise amount when every round starts + """ + self.np_random = np_random + self.game_pointer = None + self.num_players = num_players + self.init_raise_amount = init_raise_amount + + self.dealer = dealer + + # Count the number without raise + # If every player agree to not raise, the round is over + self.not_raise_num = 0 + + # Count players that are not playing anymore (folded or all-in) + self.not_playing_num = 0 + + # Raised amount for each player + self.raised = [0 for _ in range(self.num_players)] + + def start_new_round(self, game_pointer, raised=None): + """ + Start a new bidding round + + Args: + game_pointer (int): The game_pointer that indicates the next player + raised (list): Initialize the chips for each player + + Note: For the first round of the game, we need to setup the big/small blind + """ + self.game_pointer = game_pointer + self.not_raise_num = 0 + if raised: + self.raised = raised + else: + self.raised = [0 for _ in range(self.num_players)] + + def proceed_round(self, players, action): + """ + Call functions from other classes to keep one round running + + Args: + players (list): The list of players that play the game + action (str/int): An legal action taken by the player + + Returns: + (int): The game_pointer that indicates the next player + """ + player = players[self.game_pointer] + + if action == Action.CHECK: + self.not_raise_num += 1 + + if action == Action.CALL: + diff = max(self.raised) - self.raised[self.game_pointer] + self.raised[self.game_pointer] = max(self.raised) + player.bet(chips=diff) + self.not_raise_num += 1 + + elif action == Action.ALL_IN: + all_in_quantity = player.remained_chips + self.raised[self.game_pointer] = all_in_quantity + self.raised[self.game_pointer] + player.bet(chips=all_in_quantity) + + self.not_raise_num = 1 + + elif action == Action.RAISE_POT: + self.raised[self.game_pointer] += self.dealer.pot + player.bet(chips=self.dealer.pot) + self.not_raise_num = 1 + + elif action == Action.RAISE_HALF_POT: + quantity = int(self.dealer.pot / 2) + self.raised[self.game_pointer] += quantity + player.bet(chips=quantity) + self.not_raise_num = 1 + + elif action == Action.FOLD: + player.status = PlayerStatus.FOLDED + + if player.remained_chips < 0: + raise Exception("Player in negative stake") + + if player.remained_chips == 0 and player.status != PlayerStatus.FOLDED: + player.status = PlayerStatus.ALLIN + + self.game_pointer = (self.game_pointer + 1) % self.num_players + + if player.status == PlayerStatus.ALLIN: + self.not_playing_num += 1 + self.not_raise_num -= 1 # Because already counted in not_playing_num + if player.status == PlayerStatus.FOLDED: + self.not_playing_num += 1 + + # Skip the folded players + while players[self.game_pointer].status == PlayerStatus.FOLDED: + self.game_pointer = (self.game_pointer + 1) % self.num_players + + return self.game_pointer + + def get_nolimit_legal_actions(self, players): + """ + Obtain the legal actions for the current player + + Args: + players (list): The players in the game + + Returns: + (list): A list of legal actions + """ + + full_actions = list(Action) + + # The player can always check or call + player = players[self.game_pointer] + + diff = max(self.raised) - self.raised[self.game_pointer] + if diff == 0: + full_actions.remove(Action.CALL) + full_actions.remove(Action.FOLD) + + if diff > 0: + full_actions.remove(Action.CHECK) + + # If the current player has no more chips after call, we cannot raise + if diff > 0 and diff >= player.remained_chips: + full_actions.remove(Action.RAISE_HALF_POT) + full_actions.remove(Action.RAISE_POT) + full_actions.remove(Action.ALL_IN) + # Even if we can raise, we have to check remained chips + else: + if self.dealer.pot > player.remained_chips: + full_actions.remove(Action.RAISE_POT) + + if int(self.dealer.pot / 2) > player.remained_chips: + full_actions.remove(Action.RAISE_HALF_POT) + + # Can't raise if the total raise amount is leq than the max raise amount of this round + # If raise by pot, there is no such concern + if Action.RAISE_HALF_POT in full_actions and \ + int(self.dealer.pot / 2) + self.raised[self.game_pointer] <= max(self.raised): + full_actions.remove(Action.RAISE_HALF_POT) + + return full_actions + + def is_over(self): + """ + Check whether the round is over + + Returns: + (boolean): True if the current round is over + """ + # If all players are either all-in or folded, the round is over + if self.not_playing_num >= self.num_players: + return True + + # If all players have checked or called, the round is over + if self.not_raise_num + self.not_playing_num >= self.num_players: + return True + + return False diff --git a/rlcard/games/fixednolimitholdem/utils.py b/rlcard/games/fixednolimitholdem/utils.py new file mode 100644 index 000000000..90fc38a4b --- /dev/null +++ b/rlcard/games/fixednolimitholdem/utils.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +"""Implement utils for no limit texas holdem""" + +def get_card_id(suit, rank): + """ + Get the card ID based on suit and rank + + Args: + suit (str): The suit of the card ('S', 'H', 'D', 'C') + rank (str): The rank of the card ('A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K') + + Returns: + (int): The ID of the card + """ + suit_list = ['S', 'H', 'D', 'C'] + rank_list = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K'] + + suit_index = suit_list.index(suit) + rank_index = rank_list.index(rank) + + return rank_index + 13 * suit_index + +# get_card_from_id +def get_card_from_id(card_id): + """ + Get a Card object from a card ID + + Args: + card_id (int): The ID of the card + + Returns: + (Card): A Card object + """ + suit_list = ['S', 'H', 'D', 'C'] + rank_list = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K'] + + suit_index = card_id // 13 + rank_index = card_id % 13 + + return Card(suit_list[suit_index], rank_list[rank_index]) +