From b9963bfbb16b227109b83fb7922c9b5eb20b4c4d Mon Sep 17 00:00:00 2001 From: Jan-Peter von Hunnius Date: Tue, 8 Sep 2026 16:23:08 +0200 Subject: [PATCH] =?UTF-8?q?feat(sim):=20cash=5Fout=5Fthreshold=20=E2=80=94?= =?UTF-8?q?=20rathole=20stack=20excess=20to=20bankroll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After each hand, stack above the threshold moves to the bankroll. Bounds effective depth while keeping natural short-stack dynamics (losses, rebuy cycles). Needed for realistic deep-stack-safe benchmarks: the oracle hybrid's EV collapses with stack depth (+6.9 at 75BB reset vs -33.8 natural), so unbounded natural-stack snowballs distort candidate evaluation. --- holdem_core/src/game/config.rs | 16 ++++++++++++++++ holdem_core/src/game/simulation.rs | 26 +++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/holdem_core/src/game/config.rs b/holdem_core/src/game/config.rs index 3503917..810e011 100644 --- a/holdem_core/src/game/config.rs +++ b/holdem_core/src/game/config.rs @@ -354,6 +354,12 @@ pub struct GameConfig { #[serde(default)] pub bankroll_refill_threshold: ChipCount, + /// Cash-out (rathole) threshold: after each hand, stack excess above + /// this amount is moved to the player's bankroll. 0 = disabled. + /// Keeps natural stack dynamics (losses, rebuys) while bounding depth. + #[serde(default)] + pub cash_out_threshold: ChipCount, + /// If true, player seat assignments are randomized before each hand /// (cash game) or before tournament start. #[serde(default)] @@ -430,6 +436,7 @@ impl Default for GameConfig { rake_cap: 0, reset_bankroll_each_hand: false, bankroll_refill_threshold: 0, + cash_out_threshold: 0, randomize_seats: false, auto_rebuy: false, rebuy_threshold_percentage: default_rebuy_threshold(), @@ -1069,6 +1076,7 @@ min_raise = 50 rake_cap: 0, reset_bankroll_each_hand: false, bankroll_refill_threshold: 0, + cash_out_threshold: 0, randomize_seats: false, auto_rebuy: false, rebuy_threshold_percentage: 25, @@ -1158,6 +1166,7 @@ min_raise = 50 rake_cap: 0, reset_bankroll_each_hand: false, bankroll_refill_threshold: 0, + cash_out_threshold: 0, randomize_seats: false, auto_rebuy: false, rebuy_threshold_percentage: 25, @@ -1197,6 +1206,7 @@ min_raise = 50 rake_cap: 0, reset_bankroll_each_hand: false, bankroll_refill_threshold: 0, + cash_out_threshold: 0, randomize_seats: false, auto_rebuy: false, rebuy_threshold_percentage: 25, @@ -1252,6 +1262,7 @@ min_raise = 50 rake_cap: 0, reset_bankroll_each_hand: false, bankroll_refill_threshold: 0, + cash_out_threshold: 0, randomize_seats: false, auto_rebuy: false, rebuy_threshold_percentage: 25, @@ -1309,6 +1320,7 @@ min_raise = 50 rake_cap: 0, reset_bankroll_each_hand: false, bankroll_refill_threshold: 0, + cash_out_threshold: 0, randomize_seats: false, auto_rebuy: false, rebuy_threshold_percentage: 25, @@ -1360,6 +1372,7 @@ min_raise = 50 rake_cap: 0, reset_bankroll_each_hand: false, bankroll_refill_threshold: 0, + cash_out_threshold: 0, randomize_seats: false, auto_rebuy: false, rebuy_threshold_percentage: 25, @@ -1422,6 +1435,7 @@ min_raise = 50 rake_cap: 0, reset_bankroll_each_hand: false, bankroll_refill_threshold: 0, + cash_out_threshold: 0, randomize_seats: false, auto_rebuy: false, rebuy_threshold_percentage: 25, @@ -1475,6 +1489,7 @@ min_raise = 50 rake_cap: 0, reset_bankroll_each_hand: false, bankroll_refill_threshold: 0, + cash_out_threshold: 0, randomize_seats: false, auto_rebuy: false, rebuy_threshold_percentage: 25, @@ -1594,6 +1609,7 @@ min_raise = 20 rake_cap: 0, reset_bankroll_each_hand: false, bankroll_refill_threshold: 0, + cash_out_threshold: 0, randomize_seats: false, auto_rebuy: false, rebuy_threshold_percentage: 25, diff --git a/holdem_core/src/game/simulation.rs b/holdem_core/src/game/simulation.rs index 85b0871..ee1681c 100644 --- a/holdem_core/src/game/simulation.rs +++ b/holdem_core/src/game/simulation.rs @@ -128,6 +128,28 @@ pub fn run_cash_game(config: &GameConfig) -> Result<(SimulationStats, Table), Ga } } } + // Cash-out (rathole): move stack excess above the threshold + // into the bankroll after each hand. Bounds effective depth + // while keeping natural short-stack dynamics intact. + if config.cash_out_threshold > 0 { + for slot in table.seats_mut().iter_mut() { + if let Some(seat) = slot { + if seat.stack > config.cash_out_threshold { + let excess = + seat.stack - config.cash_out_threshold; + log::debug!( + "Cash-out: {} ratholes {} (stack {} -> {})", + seat.player.name(), + excess, + seat.stack, + config.cash_out_threshold + ); + seat.bankroll += excess as i64; + seat.stack = config.cash_out_threshold; + } + } + } + } } Err(GameError::NotEnoughPlayers(count)) => { // Not enough active players to continue. This should only @@ -175,7 +197,9 @@ pub fn run_cash_game(config: &GameConfig) -> Result<(SimulationStats, Table), Ga .map(|seat| (seat.player_id, seat.bankroll)) }) .collect(); - let bankroll_mode = config.reset_bankroll_each_hand || config.bankroll_refill_threshold > 0; + let bankroll_mode = config.reset_bankroll_each_hand + || config.bankroll_refill_threshold > 0 + || config.cash_out_threshold > 0; stats.record_final_stacks(&final_stacks, &starting_stacks, &bankrolls, bankroll_mode, config.auto_rebuy); Ok((stats, table))