Events and Listeners#

As already mentioned in the user guide before, the Game class is implemented as a state machine and the event-driven execution model triggers the state transitions. These events are emitted to registered listeners.

Events#

The different types of events a listener can listen to are encapsulated by the GameEventType enumeration class and you can sign up to any of the events listed there.

Listening to Events#

Explicit Event Listeners#

One way of defining listeners is to define and register Python functions. The protocol a listener has to adhere the following function signature:

def custom_listener(event: GameEvent, game: Game) -> None:
    # Do something here
    ...

Since the game itself is passed to the listeners, the listeners can even interfere with the game. To register an event handler, call the subscribe method of the game instance, where the first parameter is the type of event you want to subscribe to, and the second is the listener itself.

from maverick import Game, GameEvent, GameEventType, PlayerState
from maverick.players import FoldBot, CallBot
from collections import defaultdict

print("=" * 60)
print("Maverick Event System Demo")
print("=" * 60)
print()

# Create a game
game = Game(small_blind=10, big_blind=20, max_hands=1)

# Create a global event counter
event_counts = {event_type: 0 for event_type in GameEventType}


def count_events(event: GameEvent, game: Game) -> None:
    """Count each type of event."""
    event_counts[event.type] += 1


def log_major_events(event: GameEvent, game: Game) -> None:
    """Log major game events."""
    if event.type in [
        GameEventType.GAME_STARTED,
        GameEventType.HAND_STARTED,
        GameEventType.HAND_ENDED,
    ]:
        print(event.type.name)
        print(f"📢 {event.type.name} (Hand #{event.hand_number}, Stage: {event.stage.name})")


def log_player_actions(event: GameEvent, game: Game) -> None:
    """Log player actions with details."""
    if event.type == GameEventType.PLAYER_ACTION_TAKEN:
        action_str = f"{event.action.action_type.name}"
        if event.action.amount:
            action_str += f" ({event.action.amount} chips)"
        print(
            f"  🎲 Player {event.player_id}: {action_str} | "
            f"Pot: {game.state.pot} | Current Bet: {game.state.current_bet}"
        )


def log_street_changes(event: GameEvent, game: Game) -> None:
    """Log when streets change."""
    if event.type in [
        GameEventType.FLOP_DEALT,
        GameEventType.TURN_DEALT,
        GameEventType.RIVER_DEALT,
    ]:
        print(f"  🃏 {event.type.name} - Street {event.street.name}")


# Register event handlers
game.subscribe(GameEventType.GAME_STARTED, count_events)
game.subscribe(GameEventType.GAME_STARTED, log_major_events)
game.subscribe(GameEventType.GAME_ENDED, count_events)
game.subscribe(GameEventType.GAME_ENDED, log_major_events)
game.subscribe(GameEventType.HAND_STARTED, count_events)
game.subscribe(GameEventType.HAND_STARTED, log_major_events)
game.subscribe(GameEventType.HAND_ENDED, count_events)
game.subscribe(GameEventType.HAND_ENDED, log_major_events)
game.subscribe(GameEventType.PLAYER_ACTION_TAKEN, count_events)
game.subscribe(GameEventType.PLAYER_ACTION_TAKEN, log_player_actions)
game.subscribe(GameEventType.HOLE_CARDS_DEALT, count_events)
game.subscribe(GameEventType.HOLE_CARDS_DEALT, log_player_actions)
game.subscribe(GameEventType.BETTING_ROUND_COMPLETED, count_events)
game.subscribe(GameEventType.BETTING_ROUND_COMPLETED, log_player_actions)
game.subscribe(GameEventType.BLINDS_POSTED, count_events)
game.subscribe(GameEventType.BLINDS_POSTED, log_player_actions)
game.subscribe(GameEventType.SHOWDOWN_COMPLETED, count_events)
game.subscribe(GameEventType.SHOWDOWN_COMPLETED, log_player_actions)
game.subscribe(GameEventType.PLAYER_JOINED, count_events)
game.subscribe(GameEventType.PLAYER_JOINED, log_player_actions)
game.subscribe(GameEventType.PLAYER_LEFT, count_events)
game.subscribe(GameEventType.PLAYER_LEFT, log_player_actions)
game.subscribe(GameEventType.FLOP_DEALT, count_events)
game.subscribe(GameEventType.FLOP_DEALT, log_street_changes)
game.subscribe(GameEventType.TURN_DEALT, count_events)
game.subscribe(GameEventType.TURN_DEALT, log_street_changes)
game.subscribe(GameEventType.RIVER_DEALT, count_events)
game.subscribe(GameEventType.RIVER_DEALT, log_street_changes)


class EventRecorder:
    """Helper class to record events in order."""

    def __init__(self):
        self.events: list[GameEvent] = []

    def record(self, event: GameEvent, game: Game) -> None:
        self.events.append(event)

    def clear(self) -> None:
        self.events = []

    def get_event_types(self) -> list[GameEventType]:
        return [e.type for e in self.events]
    

recorder = EventRecorder()
game.subscribe(GameEventType.GAME_STARTED, recorder.record)


class ListenerCallBot(CallBot):
    """A player that counts the observed events by type."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.event_counts = defaultdict(int)

    def on_event(self, event: GameEvent, game: Game) -> None:
        """Track how many events this player observes."""
        self.event_counts[event.type] += 1
        print(f"Player {self.name} observed event: {event.type.name}")
        

# Add players (one with event hook)
p1 = FoldBot(name="Alice", state=PlayerState(stack=1000))
p2 = ListenerCallBot(name="Bob", state=PlayerState(stack=1000))
p3 = CallBot(name="Charlie", state=PlayerState(stack=1000))

# Add the players to the game
game.add_player(p1)
game.add_player(p2)
game.add_player(p3)

print(f"Added 3 players: {p1.name}, {p2.name}, {p3.name}")
print()

# Run the game
print("Starting game...")
print()
game.start()

game.remove_player(p1)
game.remove_player(p2)
game.remove_player(p3)

print()
print("=" * 60)
print("Event Summary")
print("=" * 60)
print()

# Print event counts
print("Global Event Counts:")
for event_type, count in sorted(event_counts.items(), key=lambda x: x[1], reverse=True):
    print(f"  {event_type.name:30s}: {count:3d}")
    
# Print observed events by p2
print(f"\nEvents observed by {p2.name}:")
for event_type, count in sorted(p2.event_counts.items(), key=lambda x: x[1], reverse=True):
    print(f"  {event_type.name:30s}: {count:3d}")

print()
print("=" * 60)
print("Demo Complete!")
print("=" * 60)
============================================================
Maverick Event System Demo
============================================================

Player Bob observed event: PLAYER_JOINED
Player Bob observed event: PLAYER_JOINED
Added 3 players: Alice, Bob, Charlie

Starting game...

GAME_STARTED
📢 GAME_STARTED (Hand #0, Stage: STARTED)
Player Bob observed event: GAME_STARTED
HAND_STARTED
📢 HAND_STARTED (Hand #1, Stage: DEALING)
Player Bob observed event: HAND_STARTED
Player Bob observed event: HOLE_CARDS_DEALT
Player Bob observed event: BLINDS_POSTED
Player Bob observed event: ANTES_POSTED
Player Bob observed event: BETTING_ROUND_STARTED
  🎲 Player e8def0921c934b309901743583378252: CALL | Pot: 50 | Current Bet: 20
Player Bob observed event: PLAYER_ACTION_TAKEN
  🎲 Player 2ae6e41c509a412590144e5e1790b2d9: CALL | Pot: 60 | Current Bet: 20
Player Bob observed event: PLAYER_ACTION_TAKEN
  🎲 Player 941a0200de9b49eb8305865c59180216: FOLD | Pot: 60 | Current Bet: 20
Player Bob observed event: PLAYER_ACTION_TAKEN
Player Bob observed event: BETTING_ROUND_COMPLETED
  🃏 FLOP_DEALT - Street FLOP
Player Bob observed event: FLOP_DEALT
Player Bob observed event: BETTING_ROUND_STARTED
  🎲 Player 2ae6e41c509a412590144e5e1790b2d9: CHECK | Pot: 60 | Current Bet: 0
Player Bob observed event: PLAYER_ACTION_TAKEN
  🎲 Player e8def0921c934b309901743583378252: CHECK | Pot: 60 | Current Bet: 0
Player Bob observed event: PLAYER_ACTION_TAKEN
Player Bob observed event: BETTING_ROUND_COMPLETED
  🃏 TURN_DEALT - Street TURN
Player Bob observed event: TURN_DEALT
Player Bob observed event: BETTING_ROUND_STARTED
  🎲 Player 2ae6e41c509a412590144e5e1790b2d9: CHECK | Pot: 60 | Current Bet: 0
Player Bob observed event: PLAYER_ACTION_TAKEN
  🎲 Player e8def0921c934b309901743583378252: CHECK | Pot: 60 | Current Bet: 0
Player Bob observed event: PLAYER_ACTION_TAKEN
Player Bob observed event: BETTING_ROUND_COMPLETED
  🃏 RIVER_DEALT - Street RIVER
Player Bob observed event: RIVER_DEALT
Player Bob observed event: BETTING_ROUND_STARTED
  🎲 Player 2ae6e41c509a412590144e5e1790b2d9: CHECK | Pot: 60 | Current Bet: 0
Player Bob observed event: PLAYER_ACTION_TAKEN
  🎲 Player e8def0921c934b309901743583378252: CHECK | Pot: 60 | Current Bet: 0
Player Bob observed event: PLAYER_ACTION_TAKEN
Player Bob observed event: BETTING_ROUND_COMPLETED
Player Bob observed event: SHOWDOWN_STARTED
Player Bob observed event: PLAYER_CARDS_REVEALED
Player Bob observed event: PLAYER_CARDS_REVEALED
Player Bob observed event: POT_WON
Player Bob observed event: SHOWDOWN_COMPLETED
HAND_ENDED
📢 HAND_ENDED (Hand #1, Stage: HAND_COMPLETE)
Player Bob observed event: HAND_ENDED
Player Bob observed event: GAME_ENDED
Player Bob observed event: PLAYER_LEFT

============================================================
Event Summary
============================================================

Global Event Counts:
  PLAYER_ACTION_TAKEN           :   9
  BETTING_ROUND_COMPLETED       :   4
  PLAYER_JOINED                 :   3
  PLAYER_LEFT                   :   3
  GAME_STARTED                  :   1
  GAME_ENDED                    :   1
  HAND_STARTED                  :   1
  HAND_ENDED                    :   1
  SHOWDOWN_COMPLETED            :   1
  HOLE_CARDS_DEALT              :   1
  FLOP_DEALT                    :   1
  TURN_DEALT                    :   1
  RIVER_DEALT                   :   1
  BLINDS_POSTED                 :   1
  SHOWDOWN_STARTED              :   0
  POT_WON                       :   0
  PLAYER_CARDS_REVEALED         :   0
  PLAYER_ELIMINATED             :   0
  ANTES_POSTED                  :   0
  BETTING_ROUND_STARTED         :   0

Events observed by Bob:
  PLAYER_ACTION_TAKEN           :   9
  BETTING_ROUND_STARTED         :   4
  BETTING_ROUND_COMPLETED       :   4
  PLAYER_JOINED                 :   2
  PLAYER_CARDS_REVEALED         :   2
  GAME_STARTED                  :   1
  HAND_STARTED                  :   1
  HOLE_CARDS_DEALT              :   1
  BLINDS_POSTED                 :   1
  ANTES_POSTED                  :   1
  FLOP_DEALT                    :   1
  TURN_DEALT                    :   1
  RIVER_DEALT                   :   1
  SHOWDOWN_STARTED              :   1
  POT_WON                       :   1
  SHOWDOWN_COMPLETED            :   1
  HAND_ENDED                    :   1
  GAME_ENDED                    :   1
  PLAYER_LEFT                   :   1

============================================================
Demo Complete!
============================================================

Implicit Event Listeners#

Another way to subscribe to events (currently available only for players) is to use implicit listeners. To do this, implement a method named on_xyz, where xyz is the lowercase version of the event name.

from maverick import Game, GameEvent


class ListenerCallBot(CallBot):
    """A player that counts the observed events by type."""
        
    def on_game_ended(self, event: GameEvent, game: Game) -> None:
        """Report the event counts when the game ends."""
        assert event.type == GameEventType.GAME_ENDED
        print(f"{self.name}: Bye!")


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

# Add players (one with event hook)
p1 = FoldBot(name="Alice", state=PlayerState(stack=1000))
p2 = ListenerCallBot(name="Bob", state=PlayerState(stack=1000))
p3 = CallBot(name="Charlie", state=PlayerState(stack=1000))

# Add the players to the game
game.add_player(p1)
game.add_player(p2)
game.add_player(p3)

# Run the game
game.start()
Bob: Bye!