Creating Custom Players#

To create a custom player, you have to define a class that admits the protocol implemented by PlayerLike. The easiest way is to inherit from Player.

Implementing a Custom Strategy#

from maverick import Game, Player, ActionType, PlayerAction
from maverick.utils import estimate_holding_strength, find_highest_scoring_hand


class CustomPlayer(Player):
    
    def decide_action(
        self,
        *,
        game: Game,
        valid_actions: list["ActionType"],
        min_raise_amount: int,
        call_amount: int,
        **_
    ) -> "PlayerAction":
        # ---------- Information used to make decision ----------
        
        min_raise_by = int(min_raise_amount)                            # Minimum amount to raise BY if the action is RAISE
        
        street_name = game.state.street.name                     # This is the current street (e.g., PRE_FLOP, FLOP, TURN, RIVER)
        button_position = game.state.button_position             # Position of the dealer button
        pot = game.state.pot                                     # Total chips in the pot
        current_bet = game.state.current_bet                     # Current highest bet in this round
        small_blind = game.state.small_blind                     # Small blind amount
        big_blind = game.state.big_blind                         # Big blind amount
        community_cards = game.state.community_cards             # List of community cards on the table
        stack = self.state.stack                                 # Player's current stack
        current_bet_player = self.state.current_bet              # Player's current bet in this round
        private_cards = self.state.holding.cards                 # Player's private cards

        # ----------- Hand strength evaluation ----------
        
        (
            strongest_hand, 
            strongest_hand_type, 
            strongest_hand_score
        ) = find_highest_scoring_hand(
            private_cards, 
            community_cards, 
            n_private=game.rules.showdown.hole_cards_required
        )
        
        # strongest_hand: list[Card] = best possible hand cards
        # strongest_hand_type: HandType = type of the best hand (e.g., STRAIGHT_FLUSH)
        # strongest_hand_score: int = numerical score of the best hand

        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()),
        )
        
        # hand_prob: float = estimated probability (relative likeliness) of having the best hand (equity)

        # ---------- Decision logic (example, this is what is personal to a player) ----------
        
        if ActionType.RAISE in valid_actions and hand_prob > 0.55:
            raise_amount = min(self.state.stack, min_raise_by * 2)
            action = PlayerAction(player_uid=self.uid, action_type=ActionType.RAISE, amount=raise_amount)
            raise_to = current_bet_player + raise_amount
            print(f"     Decision: RAISE to {raise_to}")
        elif ActionType.CALL in valid_actions and hand_prob > 0.2:
            action = PlayerAction(player_uid=self.uid, action_type=ActionType.CALL)
            print(f"     Decision: CALL {call_amount}")
        elif ActionType.CHECK in valid_actions:
            action = PlayerAction(player_uid=self.uid, action_type=ActionType.CHECK)
            print("     Decision: CHECK")
        else:
            action = PlayerAction(player_uid=self.uid, action_type=ActionType.FOLD)
            print("     Decision: FOLD")
        
        # Return the chosen action
        return action
from maverick import Game, PlayerLike, PlayerState
from maverick.players import FoldBot, CallBot, AggressiveBot

game = Game(small_blind=10, big_blind=20, max_hands=2)

players: list[PlayerLike] = [
    CallBot(name="CallBot", state=PlayerState(stack=1000)),
    AggressiveBot(name="AggroBot", state=PlayerState(stack=1000)),
    FoldBot(name="FoldBot", state=PlayerState(stack=1000)),
    CustomPlayer(name="CustomBot", state=PlayerState(stack=1000)),
]

for player in players:
    game.add_player(player)
    
game.start()
     Decision: CALL 20
     Decision: CHECK
     Decision: RAISE to 160
     Decision: RAISE to 640
     Decision: CHECK
     Decision: RAISE to 160
     Decision: CALL 120
     Decision: CHECK
     Decision: CALL 40

Implementing to_dict()#

The players are part of the game state. The game state object an instance of the GameState class, which is inherited from BaseModel. Hence a game state is serializable only, if players are serializable. To make sure that your custom class is serializale, you might want to implement the to_dict method, but you only need to do that if you wish to recover member attributed that you added yourself on top of what the base class already does.

The class below declares a new member attribute _event_counter, hence the to_dict() method has to be adjusted. Whatever to_dict() returned is going to be passed to the constructor of the class at instantiation.

Note

If you don’t know what the method on_event it or what it does, check out this section of the user guide.

from maverick import GameEvent


class CustomPlayer2(CustomPlayer):
    
    def __init__(self, event_counter=0, **kwargs):
        super().__init__(**kwargs)
        self._event_counter = event_counter
        
    @property
    def event_counter(self) -> int:
        return self._event_counter
        
    def on_event(self, event: GameEvent, game: Game) -> None:
        self._event_counter += 1
        
    def to_dict(self) -> dict:
        d = super().to_dict()
        d.update({"event_counter": self._event_counter})
        return d
    

game = Game(small_blind=10, big_blind=20, max_hands=2)

players: list[PlayerLike] = [
    CallBot(name="CallBot", state=PlayerState(stack=1000)),
    AggressiveBot(name="AggroBot", state=PlayerState(stack=1000)),
    FoldBot(name="FoldBot", state=PlayerState(stack=1000)),
    CustomPlayer2(name="CustomBot", state=PlayerState(stack=1000)),
]

for player in players:
    game.add_player(player)
    
game.start()
     Decision: CALL 20
     Decision: CALL 20
     Decision: CALL 40
     Decision: RAISE to 160
     Decision: RAISE to 640
     Decision: RAISE to 160
     Decision: CALL 120

This way, the game state is not just serializable, you can even recover it.

from maverick import GameState

game_state = game.state.model_dump()
recovered_game_state = GameState.model_validate(game_state)

Custom Payload with PlayerAction#

When constructing a {class}~maverick.playeraction.PlayerAction, you can attach arbitrary data to the decision by passing a dictionary via the payload` argument. This is useful when you have a custom event handler and need that context available later.

player = CustomPlayer(name="CustomPlayer", state=PlayerState(stack=1000))
action = PlayerAction(player_uid=player.uid, action_type=ActionType.FOLD, payload={"example_key": "example_value"})
action.model_dump()
{'player_uid': '4e3373facb5c48f9b8f8180b1e19577d',
 'action_type': <ActionType.FOLD: 1>,
 'amount': None,
 'payload': {'example_key': 'example_value'}}

Best Practices#

  • Always end decide_action with a FOLD action. Folding is always available as an action and it ensures a feasible decision among all circumstances.

  • Use as much information as you can for making a decision.