feat: hand history logging, tournament series, MTT consolidation, Meerkat API

Major changes:
- Add hand history module (iPoker format) with HandHistoryLogger
- Add run_sng_series() for multi-tournament aggregate statistics
- Fix MTT table consolidation deadlock (sub-minimum-player tables)
- Add Meerkat-compatible convenience methods on Action and GameInfo
- Add custom blind levels, prize structures, auto-rebuy to GameConfig
- Add SngSeriesResult/SngPlayerSeriesResult for series tracking
- Add Tournament::winner(), tables_mut(), shuffle_seats()

Review fixes:
- Fix TOML prizes field ordering (must precede [[game.blind_levels]])
- Add sensible serde defaults for small_blind (10) and big_blind (20)
- Fix prize assignment in run_sng_series (use raw_placement directly)
- Handle hand history filename collisions with suffixed retry
- Add debug_assert guard on collect_hand_history_data double-consumption
This commit is contained in:
Jan-Peter von Hunnius 2026-05-20 21:05:22 +02:00
parent 9dfd9c3ee3
commit c041cde3fe
18 changed files with 4601 additions and 208 deletions

View file

@ -4,7 +4,7 @@ num_seats = 10
small_blind = 10
big_blind = 20
ante = 0
starting_chips = 5000
starting_chips = 1500
num_hands = 10000
# Rake configuration (optional, defaults to no rake).
@ -15,8 +15,12 @@ num_hands = 10000
# reset_bankroll_each_hand = false
# bankroll_refill_threshold = 1000 # Refill when stack <= 1000
# Auto-rebuy: when enabled, players whose stack drops below half the
# starting_chips are automatically topped back up to starting_chips.
auto_rebuy = true
# Randomize seat assignments before each hand (optional, defaults to false).
# randomize_seats = true
randomize_seats = true
[game.bet_limit]
type = "no_limit"

View file

@ -0,0 +1,527 @@
# MTT Tournament Series — 10 tables, 100 players, 100 tournaments
#
# Each of the 6 bot types appears multiple times to fill 100 seats.
# Tables have 10 seats each; players are distributed round-robin
# across tables by the tournament's register_player() logic.
# As players bust out, tables will eventually consolidate
# (last player standing wins).
#
# 100 tournaments are run with aggregate statistics tracked by player name.
# Entry fee: 5 per tournament. Final table prizes: 200, 100, 50, 30, 20, 10, 10, 10, 10, 10.
[game]
game_type = "MultiTableTournament"
num_seats = 10
num_tables = 10
small_blind = 10
big_blind = 20
starting_chips = 1500
buy_in = 5
hands_per_level = 10
num_blind_levels = 20
randomize_seats = true
num_tournaments = 100
[game.bet_limit]
type = "no_limit"
min_raise = 20
# ---------------------------------------------------------------------------
# Prize structure (100 players × $5 buy-in = $500 prize pool, top 10 paid)
# ---------------------------------------------------------------------------
prizes = [200, 100, 50, 30, 20, 10, 10, 10, 10, 10]
# ---------------------------------------------------------------------------
# Blind levels (extended to 12 levels)
# ---------------------------------------------------------------------------
[[game.blind_levels]]
small_blind = 10
big_blind = 20
ante = 0
hands = 10
[[game.blind_levels]]
small_blind = 15
big_blind = 30
ante = 0
hands = 10
[[game.blind_levels]]
small_blind = 25
big_blind = 50
ante = 5
hands = 10
[[game.blind_levels]]
small_blind = 50
big_blind = 100
ante = 10
hands = 10
[[game.blind_levels]]
small_blind = 75
big_blind = 150
ante = 15
hands = 10
[[game.blind_levels]]
small_blind = 100
big_blind = 200
ante = 20
hands = 10
[[game.blind_levels]]
small_blind = 150
big_blind = 300
ante = 25
hands = 10
[[game.blind_levels]]
small_blind = 200
big_blind = 400
ante = 50
hands = 10
[[game.blind_levels]]
small_blind = 300
big_blind = 600
ante = 75
hands = 10
[[game.blind_levels]]
small_blind = 400
big_blind = 800
ante = 100
hands = 10
[[game.blind_levels]]
small_blind = 600
big_blind = 1200
ante = 150
hands = 10
[[game.blind_levels]]
small_blind = 800
big_blind = 1600
ante = 200
hands = 10
# ---------------------------------------------------------------------------
# Bots — 100 total (10 tables × 10 seats)
# 17 FlockBot, 17 ChumpBot, 17 CallBot, 17 RaiseBot, 16 RandomBot, 16 FoldBot
# ---------------------------------------------------------------------------
# FlockBot copies (17)
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-1"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-2"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-3"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-4"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-5"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-6"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-7"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-8"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-9"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-10"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-11"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-12"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-13"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-14"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-15"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-16"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-17"
# ChumpBot copies (17)
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-1"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-2"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-3"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-4"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-5"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-6"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-7"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-8"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-9"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-10"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-11"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-12"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-13"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-14"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-15"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-16"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-17"
# CallBot copies (17)
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-1"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-2"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-3"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-4"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-5"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-6"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-7"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-8"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-9"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-10"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-11"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-12"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-13"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-14"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-15"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-16"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-17"
# RaiseBot copies (17)
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-1"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-2"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-3"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-4"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-5"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-6"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-7"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-8"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-9"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-10"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-11"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-12"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-13"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-14"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-15"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-16"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-17"
# RandomBot copies (16)
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-1"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-2"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-3"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-4"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-5"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-6"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-7"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-8"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-9"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-10"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-11"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-12"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-13"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-14"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-15"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-16"
# FoldBot copies (16)
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-1"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-2"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-3"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-4"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-5"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-6"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-7"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-8"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-9"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-10"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-11"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-12"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-13"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-14"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-15"
[[game.bots]]
bot_type = "fold_bot"
name = "FoldBot-16"
# ---------------------------------------------------------------------------
# Output settings
# ---------------------------------------------------------------------------
[output]
log_hands = false
log_actions = false

View file

@ -1,18 +0,0 @@
[game]
game_type = "MultiTableTournament"
num_seats = 9
num_tables = 3
small_blind = 10
big_blind = 20
starting_chips = 10000
buy_in = 5000
hands_per_level = 10
num_blind_levels = 20
[game.bet_limit]
type = "no_limit"
min_raise = 20
[output]
log_hands = true
log_actions = true

View file

@ -1,21 +1,107 @@
[game]
game_type = "SitAndGo"
num_seats = 9
small_blind = 10
big_blind = 20
starting_chips = 5000
buy_in = 1000
hands_per_level = 10
num_blind_levels = 15
num_seats = 10
starting_chips = 1500
buy_in = 11
randomize_seats = true
num_tournaments = 100
# Custom prize structure in chip amounts: 1st=5, 2nd=3, 3rd=2
prizes = [50, 30, 20]
[game.bet_limit]
type = "no_limit"
min_raise = 20
# Explicit blind levels
[[game.blind_levels]]
small_blind = 10
big_blind = 20
ante = 0
hands = 10
[[game.blind_levels]]
small_blind = 15
big_blind = 30
ante = 0
hands = 10
[[game.blind_levels]]
small_blind = 25
big_blind = 50
ante = 5
hands = 10
[[game.blind_levels]]
small_blind = 50
big_blind = 100
ante = 10
hands = 10
[[game.blind_levels]]
small_blind = 75
big_blind = 150
ante = 15
hands = 10
[[game.blind_levels]]
small_blind = 100
big_blind = 200
ante = 20
hands = 10
[[game.blind_levels]]
small_blind = 150
big_blind = 300
ante = 25
hands = 10
[[game.blind_levels]]
small_blind = 200
big_blind = 400
ante = 50
hands = 10
# 10 bots (all 6 bot types, with duplicates to fill the table)
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-1"
[[game.bots]]
bot_type = "chump_bot"
name = "ChumpBot-1"
[[game.bots]]
bot_type = "call_bot"
name = "Bot 1"
name = "CallBot-1"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-1"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-1"
[[game.bots]]
bot_type = "fold_bot"
name = "Bot 2"
name = "FoldBot-1"
[[game.bots]]
bot_type = "flock_bot"
name = "FlockBot-2"
[[game.bots]]
bot_type = "call_bot"
name = "CallBot-2"
[[game.bots]]
bot_type = "raise_bot"
name = "RaiseBot-2"
[[game.bots]]
bot_type = "random_bot"
name = "RandomBot-2"
[output]
log_hands = true