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_actionwith 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.