Core Concepts Illustrated Through a Simple Game#
This section of the user guide explains the core concepts of the library through playing a simple game. It also has references to other sections for more advanced use cases.
Defining the Game#
The first thing you might want to do is to define the game itself. By default, the Game class sets up a No-Limit Texas Hold’em game for you.
Note
You can read about how to set up other poker variants with custom rulesets here.
from maverick import Game
game = Game(small_blind=10, big_blind=20, max_hands=2)
Note
Here we defined the maximum number of hands as 2, but this is optional. With that being said, an upper limit on the number of hands always exists, even if you dont define it. Refer to the API Reference to see the default values for the input parameters.
Tip
It is recommended to always set a reasonable upper limit on the number of hands, especially if you use bots such as FoldBot (as we do in the next section of this document).
The Game class is implemented as a state machine, which is a common design pattern. It has an attribute called state, which is an instance of GameState. The game maintains this state through events and state transitions.
game.state.stage
<GameStage.WAITING_FOR_PLAYERS: 1>
The which is an instance of GameState class inherited from PyDantic’s BaseModel class, hence it can be dumped to a dictionary:
game.state.model_dump()
{'stage': <GameStage.WAITING_FOR_PLAYERS: 1>,
'street': None,
'players': [],
'current_player_index': None,
'deck': None,
'community_cards': [],
'pot': 0,
'current_bet': 0,
'min_bet': 0,
'last_raise_size': 0,
'small_blind': 10,
'big_blind': 20,
'ante': 0,
'hand_number': 0,
'button_position': None,
'small_blind_position': None,
'big_blind_position': None}
We will issue the print statement above to illustrate how the state of the game changes.
Defining the Players of the Game#
The library contains built-in players you can use as training opponents. For an exhaustive list of available built-in players, refer to this section of the API Reference. What you have to tell at the minimum when setting up a player is the player’s name, and the amount of chips they start the game with. Among other things that might change during a game, the amount of chips (aka. stack) is part of the player’s state.
from maverick.players import FoldBot, CallBot, AggressiveBot
from maverick import PlayerLike, PlayerState
players: list[PlayerLike] = [
CallBot(name="CallBot", state=PlayerState(stack=1000)),
AggressiveBot(name="AggroBot", state=PlayerState(stack=1000)),
FoldBot(name="FoldBot", state=PlayerState(stack=1000)),
]
for player in players:
game.add_player(player)
Note
You can read about how to implement custom players here.
Every implemented player class must adhere to the protocol defined by the PlayerLike class. Every instance of a player class has a name, and id, and a state. The state attribute is an instance of PlayerState and it captures all information of a player that might change during a game
game.state.model_dump()
{'stage': <GameStage.READY: 2>,
'street': None,
'players': [{'uid': 'c5de8aea2f0143529bba9ef278aed701',
'name': 'CallBot',
'state': {'seat': 0,
'state_type': <PlayerStateType.ACTIVE: 1>,
'stack': 1000,
'holding': None,
'current_bet': 0,
'total_contributed': 0,
'acted_this_street': False},
'__class__.__name__': 'CallBot'},
{'uid': '5211937050e0470eac830606e1c53029',
'name': 'AggroBot',
'state': {'seat': 1,
'state_type': <PlayerStateType.ACTIVE: 1>,
'stack': 1000,
'holding': None,
'current_bet': 0,
'total_contributed': 0,
'acted_this_street': False},
'__class__.__name__': 'AggressiveBot'},
{'uid': 'f34bd65d1b8b47a3aca4b3271111d363',
'name': 'FoldBot',
'state': {'seat': 2,
'state_type': <PlayerStateType.ACTIVE: 1>,
'stack': 1000,
'holding': None,
'current_bet': 0,
'total_contributed': 0,
'acted_this_street': False},
'__class__.__name__': 'FoldBot'}],
'current_player_index': None,
'deck': None,
'community_cards': [],
'pot': 0,
'current_bet': 0,
'min_bet': 0,
'last_raise_size': 0,
'small_blind': 10,
'big_blind': 20,
'ante': 0,
'hand_number': 0,
'button_position': None,
'small_blind_position': None,
'big_blind_position': None}
Playing the Game#
The following cell kicks off the event flow of the game and finishes when the game has terminated. A game might terminate for a number of reasons:
there are not enough players to continue (because everyone got eliminated except one)
the number of hands reached the maximum number of hands defined at instantiation (current case)
Before you start a game, you might want to configure logging, so you can inspect the event flow of the game without distractions.
import logging
# Configure logging such that we only get the log messages of the game
logging.basicConfig(level=logging.INFO, format="%(name)s: %(message)s", force=True)
logging.getLogger().setLevel(logging.WARNING)
logging.getLogger("maverick").setLevel(logging.INFO)
And now we can kick off the game.
# Start the game
game.start()
maverick: Game started.
maverick: ============================== Hand 1 ==============================
maverick: DEALING | Dealing hole cards. Button: AggroBot
maverick: DEALING | Posting small blind of 10 by player FoldBot. Remaining stack: 990
maverick: DEALING | Posting big blind of 20 by player CallBot. Remaining stack: 980
maverick: PRE_FLOP | Player AggroBot raises by 40 chips to total bet 40. Remaining stack: 960.
maverick: PRE_FLOP | Current pot: 70 | Current bet: 40
maverick: PRE_FLOP | Player FoldBot folds.
maverick: PRE_FLOP | Current pot: 70 | Current bet: 40
maverick: PRE_FLOP | Player CallBot calls with amount 20. Remaining stack: 960.
maverick: PRE_FLOP | Current pot: 90 | Current bet: 40
maverick: PRE_FLOP | Betting round complete
maverick: FLOP | Dealt flop. Community cards: ['6♦', '10♠', '2♥']
maverick: FLOP | Player CallBot checks.
maverick: FLOP | Current pot: 90 | Current bet: 0
maverick: FLOP | Player AggroBot bets amount 40. Remaining stack: 920.
maverick: FLOP | Current pot: 130 | Current bet: 40
maverick: FLOP | Player CallBot calls with amount 40. Remaining stack: 920.
maverick: FLOP | Current pot: 170 | Current bet: 40
maverick: FLOP | Betting round complete
maverick: TURN | Dealt turn. Community cards: ['6♦', '10♠', '2♥', '8♥']
maverick: TURN | Player CallBot checks.
maverick: TURN | Current pot: 170 | Current bet: 0
maverick: TURN | Player AggroBot bets amount 40. Remaining stack: 880.
maverick: TURN | Current pot: 210 | Current bet: 40
maverick: TURN | Player CallBot calls with amount 40. Remaining stack: 880.
maverick: TURN | Current pot: 250 | Current bet: 40
maverick: TURN | Betting round complete
maverick: RIVER | Dealt river. Community cards: ['6♦', '10♠', '2♥', '8♥', '3♠']
maverick: RIVER | Player CallBot checks.
maverick: RIVER | Current pot: 250 | Current bet: 0
maverick: RIVER | Player AggroBot bets amount 40. Remaining stack: 840.
maverick: RIVER | Current pot: 290 | Current bet: 40
maverick: RIVER | Player CallBot calls with amount 40. Remaining stack: 840.
maverick: RIVER | Current pot: 330 | Current bet: 40
maverick: RIVER | Betting round complete
maverick: SHOWDOWN | Player CallBot has holding 8♣ A♥ at showdown,
maverick: SHOWDOWN | Player CallBot has hand PAIR with cards ['8♣', 'A♥', '6♦', '10♠', '8♥'] (score: 208.14101)
maverick: SHOWDOWN | Player AggroBot has holding 7♣ 6♥ at showdown,
maverick: SHOWDOWN | Player AggroBot has hand PAIR with cards ['7♣', '6♥', '6♦', '10♠', '8♥'] (score: 206.10081)
maverick: SHOWDOWN | Player CallBot wins 330 from the pot.
maverick: SHOWDOWN | Showdown complete
maverick: Hand ended
maverick: ============================== Hand 2 ==============================
maverick: DEALING | Dealing hole cards. Button: FoldBot
maverick: DEALING | Posting small blind of 10 by player CallBot. Remaining stack: 1160
maverick: DEALING | Posting big blind of 20 by player AggroBot. Remaining stack: 820
maverick: PRE_FLOP | Player FoldBot folds.
maverick: PRE_FLOP | Current pot: 30 | Current bet: 20
maverick: PRE_FLOP | Player CallBot calls with amount 10. Remaining stack: 1150.
maverick: PRE_FLOP | Current pot: 40 | Current bet: 20
maverick: PRE_FLOP | Player AggroBot raises by 20 chips to total bet 40. Remaining stack: 800.
maverick: PRE_FLOP | Current pot: 60 | Current bet: 40
maverick: PRE_FLOP | Player CallBot calls with amount 20. Remaining stack: 1130.
maverick: PRE_FLOP | Current pot: 80 | Current bet: 40
maverick: PRE_FLOP | Betting round complete
maverick: FLOP | Dealt flop. Community cards: ['4♠', '8♦', '4♥']
maverick: FLOP | Player CallBot checks.
maverick: FLOP | Current pot: 80 | Current bet: 0
maverick: FLOP | Player AggroBot bets amount 40. Remaining stack: 760.
maverick: FLOP | Current pot: 120 | Current bet: 40
maverick: FLOP | Player CallBot calls with amount 40. Remaining stack: 1090.
maverick: FLOP | Current pot: 160 | Current bet: 40
maverick: FLOP | Betting round complete
maverick: TURN | Dealt turn. Community cards: ['4♠', '8♦', '4♥', '7♥']
maverick: TURN | Player CallBot checks.
maverick: TURN | Current pot: 160 | Current bet: 0
maverick: TURN | Player AggroBot bets amount 40. Remaining stack: 720.
maverick: TURN | Current pot: 200 | Current bet: 40
maverick: TURN | Player CallBot calls with amount 40. Remaining stack: 1050.
maverick: TURN | Current pot: 240 | Current bet: 40
maverick: TURN | Betting round complete
maverick: RIVER | Dealt river. Community cards: ['4♠', '8♦', '4♥', '7♥', '2♣']
maverick: RIVER | Player CallBot checks.
maverick: RIVER | Current pot: 240 | Current bet: 0
maverick: RIVER | Player AggroBot bets amount 40. Remaining stack: 680.
maverick: RIVER | Current pot: 280 | Current bet: 40
maverick: RIVER | Player CallBot calls with amount 40. Remaining stack: 1010.
maverick: RIVER | Current pot: 320 | Current bet: 40
maverick: RIVER | Betting round complete
maverick: SHOWDOWN | Player CallBot has holding 6♠ K♥ at showdown,
maverick: SHOWDOWN | Player CallBot has hand PAIR with cards ['K♥', '4♠', '8♦', '4♥', '7♥'] (score: 204.13081)
maverick: SHOWDOWN | Player AggroBot has holding 5♠ 7♠ at showdown,
maverick: SHOWDOWN | Player AggroBot has hand TWO_PAIR with cards ['7♠', '4♠', '8♦', '4♥', '7♥'] (score: 307.0408)
maverick: SHOWDOWN | Player AggroBot wins 320 from the pot.
maverick: SHOWDOWN | Showdown complete
maverick: Hand ended
maverick: Reached maximum number of hands, ending game.
maverick: Game ended
game.state.model_dump()
{'stage': <GameStage.GAME_OVER: 11>,
'street': None,
'players': [{'uid': 'c5de8aea2f0143529bba9ef278aed701',
'name': 'CallBot',
'state': {'seat': 0,
'state_type': <PlayerStateType.ACTIVE: 1>,
'stack': 1010,
'holding': {'cards': [{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.SIX: 6>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.KING: 13>}]},
'current_bet': 0,
'total_contributed': 160,
'acted_this_street': False},
'__class__.__name__': 'CallBot'},
{'uid': '5211937050e0470eac830606e1c53029',
'name': 'AggroBot',
'state': {'seat': 1,
'state_type': <PlayerStateType.ACTIVE: 1>,
'stack': 1000,
'holding': {'cards': [{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.FIVE: 5>},
{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.SEVEN: 7>}]},
'current_bet': 0,
'total_contributed': 160,
'acted_this_street': False},
'__class__.__name__': 'AggressiveBot'},
{'uid': 'f34bd65d1b8b47a3aca4b3271111d363',
'name': 'FoldBot',
'state': {'seat': 2,
'state_type': <PlayerStateType.FOLDED: 2>,
'stack': 990,
'holding': {'cards': [{'suit': <Suit.HEARTS: 'H'>,
'rank': <Rank.EIGHT: 8>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.ACE: 14>}]},
'current_bet': 0,
'total_contributed': 0,
'acted_this_street': False},
'__class__.__name__': 'FoldBot'}],
'current_player_index': 0,
'deck': {'cards': [{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.TEN: 10>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.FIVE: 5>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.ACE: 14>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.FIVE: 5>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.ACE: 14>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.QUEEN: 12>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.SEVEN: 7>},
{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.JACK: 11>},
{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.NINE: 9>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.NINE: 9>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.JACK: 11>},
{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.KING: 13>},
{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.TWO: 2>},
{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.TEN: 10>},
{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.EIGHT: 8>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.EIGHT: 8>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.TEN: 10>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.THREE: 3>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.SEVEN: 7>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.FOUR: 4>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.TEN: 10>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.TWO: 2>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.TWO: 2>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.THREE: 3>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.SIX: 6>},
{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.ACE: 14>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.KING: 13>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.NINE: 9>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.QUEEN: 12>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.FOUR: 4>},
{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.THREE: 3>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.FIVE: 5>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.THREE: 3>},
{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.QUEEN: 12>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.SIX: 6>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.KING: 13>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.QUEEN: 12>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.SIX: 6>}]},
'community_cards': [{'suit': <Suit.SPADES: 'S'>, 'rank': <Rank.FOUR: 4>},
{'suit': <Suit.DIAMONDS: 'D'>, 'rank': <Rank.EIGHT: 8>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.FOUR: 4>},
{'suit': <Suit.HEARTS: 'H'>, 'rank': <Rank.SEVEN: 7>},
{'suit': <Suit.CLUBS: 'C'>, 'rank': <Rank.TWO: 2>}],
'pot': 0,
'current_bet': 0,
'min_bet': 20,
'last_raise_size': 0,
'small_blind': 10,
'big_blind': 20,
'ante': 0,
'hand_number': 2,
'button_position': 2,
'small_blind_position': 0,
'big_blind_position': 1}
How does it work?#
When you call game.start(), an event is added to the event queue of the game. When an event is added to the event queue, it usually results in a state transition and triggers downstream events and the cycle continues until the event queue is fully drained. Certain events require players to take action, and the players respond with an action. You will learn about these when you implement your first custom player here: here.
Inspecting the Results#
Every player has a state attribute, which is an instance of PlayerState. You can use this class to print the actual stack of every player.
for player in players:
print(f"{player.name} - Stack: {player.state.stack}")
CallBot - Stack: 1010
AggroBot - Stack: 1000
FoldBot - Stack: 990
Inspecting the Event History#
Games maintain a full history of events and it’s accessible through the history property of an instance. It returns a dictionary with keys ‘game’, ‘hand’ and ‘street’, all mapping to a list of GameEvent. The three keys map to the full events and the events of the current hand and street respectively. The latter two are subsets of the first one and they are probably only interesting during the game.
# Show the first 10 events that occurred in the game
game.history[:10]
[GameEvent(uid='534a75a3bb5449ecad72f94467cf52d4', ts=1773960258.350422, type=<GameEventType.PLAYER_JOINED: 14>, hand_number=0, street=None, stage=<GameStage.WAITING_FOR_PLAYERS: 1>, player_uid='c5de8aea2f0143529bba9ef278aed701', action=None, payload={}),
GameEvent(uid='70d6c27d72fc49b6907edf85d0b29112', ts=1773960258.350508, type=<GameEventType.PLAYER_JOINED: 14>, hand_number=0, street=None, stage=<GameStage.READY: 2>, player_uid='5211937050e0470eac830606e1c53029', action=None, payload={}),
GameEvent(uid='5e0349fdd6444dd5bc63d031cda061f1', ts=1773960258.350529, type=<GameEventType.PLAYER_JOINED: 14>, hand_number=0, street=None, stage=<GameStage.READY: 2>, player_uid='f34bd65d1b8b47a3aca4b3271111d363', action=None, payload={}),
GameEvent(uid='4c88a7653c8747d199cd36b109ac5f7e', ts=1773960258.366533, type=<GameEventType.GAME_STARTED: 1>, hand_number=0, street=None, stage=<GameStage.STARTED: 3>, player_uid=None, action=None, payload={}),
GameEvent(uid='a1fbdf478dcf4ffb8fb2a4cbf1a119e9', ts=1773960258.3669271, type=<GameEventType.HAND_STARTED: 3>, hand_number=1, street=<Street.PRE_FLOP: 0>, stage=<GameStage.DEALING: 4>, player_uid=None, action=None, payload={}),
GameEvent(uid='dbdf8ef657d5407bb61136d5f7908ffd', ts=1773960258.367434, type=<GameEventType.HOLE_CARDS_DEALT: 7>, hand_number=1, street=<Street.PRE_FLOP: 0>, stage=<GameStage.DEALING: 4>, player_uid=None, action=None, payload={}),
GameEvent(uid='65397437d11542479589ed537d95f1f5', ts=1773960258.368198, type=<GameEventType.BLINDS_POSTED: 17>, hand_number=1, street=<Street.PRE_FLOP: 0>, stage=<GameStage.DEALING: 4>, player_uid=None, action=None, payload={}),
GameEvent(uid='fcb902ba292c450ba39336b818ea73ad', ts=1773960258.368242, type=<GameEventType.ANTES_POSTED: 18>, hand_number=1, street=<Street.PRE_FLOP: 0>, stage=<GameStage.DEALING: 4>, player_uid=None, action=None, payload={}),
GameEvent(uid='4ee8b9ccc2bc43978c547fc9c65e7e40', ts=1773960258.3683782, type=<GameEventType.BETTING_ROUND_STARTED: 19>, hand_number=1, street=<Street.PRE_FLOP: 0>, stage=<GameStage.PRE_FLOP: 5>, player_uid=None, action=None, payload={}),
GameEvent(uid='68185ab206b146a88c5723c6f1c04368', ts=1773960258.3689961, type=<GameEventType.PLAYER_ACTION_TAKEN: 11>, hand_number=1, street=<Street.PRE_FLOP: 0>, stage=<GameStage.PRE_FLOP: 5>, player_uid='5211937050e0470eac830606e1c53029', action=PlayerAction(player_uid='5211937050e0470eac830606e1c53029', action_type=<ActionType.RAISE: 5>, amount=40, payload={}), payload={})]
If you are only interested in the full game history, you can also use the game_history attribute of the instance and the following block is equivalent to the previous one.