Create and Custom Player with Complex Strategy

On this page

Create and Custom Player with Complex Strategy#

The notebook illustrates how you can utilize game information to design a complex strategy.

from maverick import (
    Game,
    Player,
    ActionType,
    PlayerLike,
    Street,
    HandType,
    PlayerAction,
    PlayerState,
)
from maverick.players import FoldBot, AggressiveBot
from maverick.utils import estimate_holding_strength, find_highest_scoring_hand
import sys

Custom Player#

class ComplexPlayer(Player):
    """A custom player with a complex strategy."""

    def decide_action(
        self,
        *,
        game: Game,
        valid_actions: list["ActionType"],
        min_raise_amount: int,
        call_amount: int,
        **_
    ) -> "PlayerAction":
        print(f"\n{self.name} is requested to take action.")
        
        # Information related to the game itself
        print("     Current street:", game.state.street.name)
        print("     Button position:", game.state.button_position)
        print("     Current pot:", game.state.pot)
        print("     Small blind:", game.state.small_blind)
        print("     Big blind:", game.state.big_blind)
        print("     Community cards:", game.state.community_cards)
        
        # Information related to the player
        print("     Player stack:", self.state.stack)
        print("     Current player bet:", self.state.current_bet)
        print("     Private cards:", self.state.holding.cards)
        
        # Information related to the actions available for the player
        print("     Valid actions:", [v.name for v in valid_actions])
        print("     Minimum raise:", min_raise_amount)
        
        # Private and community cards
        private_cards = self.state.holding.cards
        community_cards = game.state.community_cards
        print("     Private cards:", private_cards)
        print("     Community cards:", community_cards)
        
        # Estimate the strongest possible hand of the player, considering
        # all community cards.
        hand_prob = estimate_holding_strength(
            private_cards,
            community_cards=community_cards,
            n_private=game.rules.showdown.hole_cards_required,
            n_simulations=1000,
            n_players=len(game.state.get_players_in_hand()),
        )
        
        (
            strongest_hand, 
            strongest_hand_type, 
            strongest_hand_score
        ) = find_highest_scoring_hand(
            private_cards, 
            community_cards, 
            n_private=game.rules.showdown.hole_cards_required
        )
        
        print("     Strongest hand:", strongest_hand)
        print("     Strongest hand type:", strongest_hand_type.name)
        print("     Strongest hand score:", strongest_hand_score)
        print("     Estimated holding strength:", hand_prob)
        
        # This is the players strategy
        match game.state.street:
            case Street.PRE_FLOP:
                if hand_prob < 0.2:
                    # This is the time to bluff if you want to
                    print("     Player decides to FOLD.\n")
                    return PlayerAction(player_id=self.id, action_type=ActionType.FOLD)
                elif hand_prob > 0.4 and strongest_hand_type >= HandType.PAIR:
                    if ActionType.BET in valid_actions:
                        bet_amount = max(game.state.big_blind * 4, min_raise_amount)
                        print(f"     Player decides to BET {bet_amount}.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.BET, amount=bet_amount)
                    elif ActionType.RAISE in valid_actions:
                        raise_amount = max(game.state.big_blind * 4, min_raise_amount)
                        print(f"     Player decides to RAISE {raise_amount}.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.RAISE, amount=raise_amount)
                    elif ActionType.CALL in valid_actions:
                        call_amount = max(game.state.current_bet - self.state.current_bet, self.state.stack)
                        print(f"     Player decides to CALL {call_amount}.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.CALL, amount=call_amount)
                    elif ActionType.CHECK in valid_actions:
                        print("     Player decides to CHECK.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.CHECK)
                    else:
                        print("     Player decides to ALL IN.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.ALL_IN, amount=self.state.stack)           
                else:
                    if ActionType.CHECK in valid_actions:
                        print("     Player decides to CHECK.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.CHECK)
                    else:
                        print("     Player decides to FOLD.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.FOLD)
            
            case Street.FLOP:
                if strongest_hand_type in [
                    HandType.ROYAL_FLUSH, 
                    HandType.STRAIGHT_FLUSH, 
                    HandType.FOUR_OF_A_KIND, 
                    HandType.FULL_HOUSE
                ]:
                    if ActionType.CHECK in valid_actions:
                        print("     Player decides to CHECK.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.CHECK)
                    assert ActionType.CALL in valid_actions
                    call_amount = max(game.state.current_bet - self.state.current_bet, self.state.stack)
                    print(f"     Player decides to CALL {call_amount}.\n")
                    return PlayerAction(player_id=self.id, action_type=ActionType.CALL, amount=call_amount)
                else:
                    if hand_prob < 0.3:
                        print("     Player decides to FOLD.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.FOLD)
                    elif hand_prob > 0.6:
                        if ActionType.RAISE in valid_actions:
                            raise_amount = max(game.state.big_blind * 3, min_raise_amount)
                            print(f"     Player decides to RAISE {raise_amount}.\n")
                            return PlayerAction(player_id=self.id, action_type=ActionType.RAISE, amount=raise_amount)
                        elif ActionType.CALL in valid_actions:
                            call_amount = max(game.state.current_bet - self.state.current_bet, self.state.stack)
                            print(f"     Player decides to CALL {call_amount}.\n")
                            return PlayerAction(player_id=self.id, action_type=ActionType.CALL, amount=call_amount)
                    else:
                        if ActionType.CHECK in valid_actions:
                            print("     Player decides to CHECK.\n")
                            return PlayerAction(player_id=self.id, action_type=ActionType.CHECK)
                        else:
                            print("     Player decides to FOLD.\n")
                            return PlayerAction(player_id=self.id, action_type=ActionType.FOLD)
                        
            case Street.TURN:
                to_call = max(0, game.state.current_bet - self.state.current_bet)

                # Very strong made hands: play safely (slowplay), but don't fold.
                if strongest_hand_type in [
                    HandType.ROYAL_FLUSH,
                    HandType.STRAIGHT_FLUSH,
                    HandType.FOUR_OF_A_KIND,
                    HandType.FULL_HOUSE,
                ]:
                    if ActionType.CHECK in valid_actions:
                        print("     Player decides to CHECK.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.CHECK)
                    if ActionType.CALL in valid_actions:
                        call_amount = min(to_call, self.state.stack)
                        print(f"     Player decides to CALL {call_amount}.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.CALL, amount=call_amount)
                # Weak: avoid putting more money in.
                if hand_prob < 0.25:
                    if ActionType.CHECK in valid_actions:
                        print("     Player decides to CHECK.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.CHECK)
                    print("     Player decides to FOLD.\n")
                    return PlayerAction(player_id=self.id, action_type=ActionType.FOLD)

                # Strong: value bet / raise.
                if hand_prob > 0.65:
                    if ActionType.BET in valid_actions:
                        bet_amount = min(self.state.stack, max(game.state.big_blind * 3, min_raise_amount))
                        print(f"     Player decides to BET {bet_amount}.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.BET, amount=bet_amount)
                    if ActionType.RAISE in valid_actions:
                        raise_amount = min(self.state.stack, max(game.state.big_blind * 3, min_raise_amount))
                        print(f"     Player decides to RAISE {raise_amount}.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.RAISE, amount=raise_amount)
                    if ActionType.CALL in valid_actions:
                        call_amount = min(to_call, self.state.stack)
                        print(f"     Player decides to CALL {call_amount}.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.CALL, amount=call_amount)
                # Medium: prefer checking, otherwise call small commitments.
                if ActionType.CHECK in valid_actions:
                    print("     Player decides to CHECK.\n")
                    return PlayerAction(player_id=self.id, action_type=ActionType.CHECK)
                if ActionType.CALL in valid_actions:
                    call_amount = min(to_call, self.state.stack)
                    print(f"     Player decides to CALL {call_amount}.\n")
                    return PlayerAction(player_id=self.id, action_type=ActionType.CALL, amount=call_amount)
                print("     Player decides to FOLD.\n")
                return PlayerAction(player_id=self.id, action_type=ActionType.FOLD)

            case Street.RIVER:
                to_call = max(0, game.state.current_bet - self.state.current_bet)

                # On the river, be even more conservative with weak hands.
                if hand_prob < 0.35:
                    if ActionType.CHECK in valid_actions:
                        print("     Player decides to CHECK.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.CHECK)
                    print("     Player decides to FOLD.\n")
                    return PlayerAction(player_id=self.id, action_type=ActionType.FOLD)

                # Value bet thin only when strong.
                if hand_prob > 0.75:
                    if ActionType.BET in valid_actions:
                        bet_amount = min(self.state.stack, max(game.state.big_blind * 3, min_raise_amount))
                        print(f"     Player decides to BET {bet_amount}.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.BET, amount=bet_amount)
                    if ActionType.RAISE in valid_actions:
                        raise_amount = min(self.state.stack, max(game.state.big_blind * 3, min_raise_amount))
                        print(f"     Player decides to RAISE {raise_amount}.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.RAISE, amount=raise_amount)
                    if ActionType.CALL in valid_actions:
                        call_amount = min(to_call, self.state.stack)
                        print(f"     Player decides to CALL {call_amount}.\n")
                        return PlayerAction(player_id=self.id, action_type=ActionType.CALL, amount=call_amount)
                # Medium strength: check if possible, otherwise call.
                if ActionType.CHECK in valid_actions:
                    print("     Player decides to CHECK.\n")
                    return PlayerAction(player_id=self.id, action_type=ActionType.CHECK)
                if ActionType.CALL in valid_actions:
                    call_amount = min(to_call, self.state.stack)
                    print(f"     Player decides to CALL {call_amount}.\n")
                    return PlayerAction(player_id=self.id, action_type=ActionType.CALL, amount=call_amount)
                print("     Player decides to FOLD.\n")
                return PlayerAction(player_id=self.id, action_type=ActionType.FOLD)

        # Make sure to always return a valid action. Folding is always available.
        print("     Player decides to FOLD.\n")
        sys.stdout.flush()  # Ensure all prints are output before returning
        return PlayerAction(player_id=self.id, action_type=ActionType.FOLD)
# Create game with blinds
game = Game(small_blind=10, big_blind=20, max_hands=1)

# Create and add players with different strategies
players: list[PlayerLike] = [
    ComplexPlayer(id="p1", name="ComplexPlayer", state=PlayerState(stack=1000)),
    FoldBot(id="p2", name="FoldBot", state=PlayerState(stack=1000)),
    AggressiveBot(id="p3", name="AggressiveBot", state=PlayerState(stack=1000)),
]

for player in players:
    game.add_player(player)
    
game.start()
ComplexPlayer is requested to take action.
     Current street: PRE_FLOP
     Button position: 1
     Current pot: 60
     Small blind: 10
     Big blind: 20
     Community cards: []
     Player stack: 980
     Current player bet: 20
     Private cards: [Card(2d), Card(5c)]
     Valid actions: ['FOLD', 'CALL', 'RAISE', 'ALL_IN']
     Minimum raise: 40
     Private cards: [Card(2d), Card(5c)]
     Community cards: []
     Strongest hand: [Card(2d), Card(5c)]
     Strongest hand type: HIGH_CARD
     Strongest hand score: 100.0502
     Estimated holding strength: 0.376
     Player decides to FOLD.


ComplexPlayer is requested to take action.
     Current street: PRE_FLOP
     Button position: 2
     Current pot: 70
     Small blind: 10
     Big blind: 20
     Community cards: []
     Player stack: 970
     Current player bet: 10
     Private cards: [Card(8h), Card(9c)]
     Valid actions: ['FOLD', 'CALL', 'RAISE', 'ALL_IN']
     Minimum raise: 50
     Private cards: [Card(8h), Card(9c)]
     Community cards: []
     Strongest hand: [Card(8h), Card(9c)]
     Strongest hand type: HIGH_CARD
     Strongest hand score: 100.0908
     Estimated holding strength: 0.359
     Player decides to FOLD.