# Architecture: Rake, Bankroll Management & Randomized Seats ## Overview This document describes the architecture for three new features in the `holdem_core` poker engine: 1. **Rake** — Configurable percentage deducted from pots at showdown 2. **Bankroll Management** — Per-hand stack reset and automatic short-stack refill 3. **Randomized Seats** — Shuffle player seat assignments before each hand or tournament All three features are backward-compatible: existing TOML configs without these fields continue to work with sensible defaults (no rake, no reset, no refill, no randomization). --- ## Feature 1: Rake ### 1.1 Config Changes Add two fields to [`GameConfig`](holdem_core/src/game/config.rs:178): ```rust // In GameConfig struct: /// Rake percentage taken from each pot at showdown (e.g., 0.05 = 5%). /// Rake is deducted proportionally from each pot (main + side pots). /// Set to 0.0 (default) to disable rake. #[serde(default)] pub rake_percentage: f64, /// Maximum rake in chips per pot. 0 means no cap. #[serde(default)] pub rake_cap: ChipCount, ``` **Defaults**: `rake_percentage: 0.0`, `rake_cap: 0` (no rake, no cap). Since both use `#[serde(default)]`, existing TOML files without these fields deserialize correctly. **Validation**: Add a validation method or `#[serde(deserialize_with = ...)]` that ensures `rake_percentage` is in `[0.0, 1.0]`. ### 1.2 Data Model Changes #### New struct: `RakeConfig` ```rust /// Runtime rake configuration, derived from GameConfig. #[derive(Debug, Clone, Copy)] pub struct RakeConfig { /// Fraction of each pot taken as rake (e.g., 0.05). pub percentage: f64, /// Maximum rake per pot in chips; 0 = uncapped. pub cap: ChipCount, } impl RakeConfig { pub fn no_rake() -> Self { Self { percentage: 0.0, cap: 0 } } /// Returns true if rake is effectively disabled. pub fn is_zero(&self) -> bool { self.percentage == 0.0 } /// Calculate rake for a pot of the given size. /// Returns the rake amount (floored to whole chips). pub fn calculate_rake(&self, pot_amount: ChipCount) -> ChipCount { if self.percentage == 0.0 || pot_amount == 0 { return 0; } let rake = (pot_amount as f64 * self.percentage).floor() as ChipCount; if self.cap > 0 { rake.min(self.cap) } else { rake } } } ``` #### Changes to `HandResult` Add a field to [`HandResult`](holdem_core/src/game/hand_runner.rs:86): ```rust pub struct HandResult { pub pot_winners: Vec>, pub hand_values: HashMap, pub board: Board, pub chip_movements: HashMap, /// Total rake deducted from all pots this hand. pub total_rake: ChipCount, /// Rake deducted per pot, parallel to pot_winners. pub rake_per_pot: Vec, } ``` ### 1.3 Behavior Flow The rake is applied **after** [`PotManager::distribute()`](holdem_core/src/game/pot.rs:180) returns its results, but **before** chip movements are calculated and stacks are credited. This keeps the pot manager's core logic clean and testable. ```mermaid flowchart TD A[pot_manager.distribute - hand_values] --> B[For each pot: calculate rake] B --> C[For each winner in pot: reduce award by proportional rake] C --> D[Accumulate total_rake and rake_per_pot] D --> E[Calculate chip_movements with raked amounts] E --> F[Credit raked winnings to stacks] F --> G[Build HandResult with rake info] ``` **Hook point**: Both [`HandRunner::showdown()`](holdem_core/src/game/hand_runner.rs:543) and [`HandRunner::resolve_last_player_wins()`](holdem_core/src/game/hand_runner.rs:664) need rake logic. The cleanest approach is: 1. Add `rake_config: RakeConfig` field to [`HandRunner`](holdem_core/src/game/hand_runner.rs:100) 2. Create a private helper method: ```rust impl HandRunner { /// Apply rake to pot distribution results. /// Returns (raked_pot_winners, rake_per_pot, total_rake). fn apply_rake( &self, pot_winners: Vec>, ) -> (Vec>, Vec, ChipCount) { if self.rake_config.is_zero() { let total: ChipCount = 0; let per_pot = vec![0; pot_winners.len()]; return (pot_winners, per_pot, total); } let mut total_rake: ChipCount = 0; let mut rake_per_pot = Vec::with_capacity(pot_winners.len()); let raked_winners: Vec> = pot_winners .into_iter() .map(|mut pot_result| { // Sum the pot from winner amounts (they haven't been raked yet). let pot_total: ChipCount = pot_result.iter().map(|w| w.amount).sum(); let rake = self.rake_config.calculate_rake(pot_total); rake_per_pot.push(rake); total_rake += rake; // Deduct rake proportionally from each winner. if rake == 0 || pot_result.is_empty() { return pot_result; } let num_winners = pot_result.len() as ChipCount; let rake_per_winner = rake / num_winners; let rake_remainder = rake % num_winners; for (i, winner) in pot_result.iter_mut().enumerate() { let extra = if (i as ChipCount) < rake_remainder { 1 } else { 0 }; winner.amount = winner.amount.saturating_sub(rake_per_winner + extra); } pot_result }) .collect(); (raked_winners, rake_per_pot, total_rake) } } ``` 3. In [`showdown()`](holdem_core/src/game/hand_runner.rs:611), after line 611 (`let pot_winners = self.pot_manager.distribute(&hand_values);`), call `apply_rake()` before computing chip movements. 4. In [`resolve_last_player_wins()`](holdem_core/src/game/hand_runner.rs:677), same pattern after line 677. 5. Pass `RakeConfig` through [`HandRunner::new()`](holdem_core/src/game/hand_runner.rs:155) — add it as a parameter. 6. Thread `RakeConfig` from [`GameConfig`](holdem_core/src/game/config.rs:178) → [`Table`](holdem_core/src/game/table.rs:64) → [`HandRunner`](holdem_core/src/game/hand_runner.rs:100): - Add `rake_config: RakeConfig` to [`TableConfig`](holdem_core/src/game/table.rs:29) - Store it in [`Table`](holdem_core/src/game/table.rs:64) - Pass it when constructing `HandRunner` in [`Table::start_hand()`](holdem_core/src/game/table.rs:322) ### 1.4 Stats Impact Add to [`PlayerStats`](holdem_core/src/game/stats.rs:20): ```rust /// Total rake paid by this player across all hands. /// Rake is attributed proportionally based on the player's contribution to each pot. pub total_rake_paid: ChipCount, ``` Add to [`SimulationStats`](holdem_core/src/game/stats.rs:92): ```rust /// Total rake collected across the entire simulation. pub total_rake: ChipCount, ``` Update [`SimulationStats::record_hand_result()`](holdem_core/src/game/stats.rs:120) to accumulate `total_rake` from `result.total_rake`. **Rake attribution**: To attribute rake to individual players, we need to know each player's contribution to each pot. The `PotManager` already tracks this via `wagers`. We can add a method to compute per-player rake: ```rust impl PotManager { /// Returns each player's contribution to each finalized pot. /// Useful for proportional rake attribution. pub fn player_pot_contributions(&self) -> Vec> { // For each pot, compute what each player contributed // based on the side pot algorithm thresholds } } ``` Alternatively, a simpler approach: track rake only at the simulation level (total_rake) and don't attribute per-player. This is simpler and sufficient for most use cases. Per-player rake attribution can be added later. ### 1.5 Edge Cases | Scenario | Behavior | |----------|----------| | Pot of 0 chips | Rake = 0 (guard in `calculate_rake`) | | Rake > winner's share | Use `saturating_sub` — winner gets 0, rake is capped at pot total | | Split pot with uneven shares | Remainder rake chips distributed round-robin to winners in order | | No showdown (all fold) | **Rake still applies** — the winner's pot is raked | | Rake cap reached | `rake.min(self.cap)` applied per pot | | Multiple side pots | Rake calculated independently per pot | ### 1.6 TOML Config Example ```toml [game] game_type = "Cash" num_seats = 6 small_blind = 10 big_blind = 20 starting_chips = 10000 num_hands = 1000 rake_percentage = 0.05 # 5% rake rake_cap = 50 # Max 50 chips rake per pot [game.bet_limit] type = "no_limit" min_raise = 20 ``` --- ## Feature 2: Bankroll Management ### 2.1 Config Changes Add two fields to [`GameConfig`](holdem_core/src/game/config.rs:178): ```rust /// If true, all players' stacks are reset to `starting_chips` before every hand. /// Their overall profit/loss balance is tracked separately via `bankroll`. #[serde(default)] pub reset_bankroll_each_hand: bool, /// When a player's stack drops to or below this threshold (and reset is NOT enabled), /// they automatically buy back up to `starting_chips`. Their overall balance /// decreases by the buy-in amount. #[serde(default)] pub bankroll_refill_threshold: ChipCount, ``` **Defaults**: `reset_bankroll_each_hand: false`, `bankroll_refill_threshold: 0` (disabled). Both use `#[serde(default)]` for backward compatibility. ### 2.2 Data Model Changes #### New field on `Seat` Add a `bankroll` field to [`Seat`](holdem_core/src/game/player.rs:268): ```rust pub struct Seat { // ... existing fields ... /// The player's overall balance (profit/loss) across all hands. /// Starts at 0. Increases when the player wins, decreases when they lose. /// When reset_bankroll_each_hand is enabled, this tracks the cumulative /// profit/loss while the stack is reset each hand. /// When bankroll_refill_threshold triggers a reload, the buy-in amount /// is deducted from bankroll. pub bankroll: i64, } ``` Update [`Seat::new()`](holdem_core/src/game/player.rs:331) to initialize `bankroll: 0`. #### Changes to `Table` Add bankroll config fields to [`Table`](holdem_core/src/game/table.rs:64): ```rust pub struct Table { // ... existing fields ... /// If true, reset all stacks to starting_chips before each hand. reset_bankroll_each_hand: bool, /// Auto-refill threshold. 0 = disabled. bankroll_refill_threshold: ChipCount, /// Starting chips (for resets and refills). starting_chips: ChipCount, } ``` Add these to [`TableConfig`](holdem_core/src/game/table.rs:29) as well so they flow from config: ```rust pub struct TableConfig { // ... existing fields ... pub reset_bankroll_each_hand: bool, pub bankroll_refill_threshold: ChipCount, pub starting_chips: ChipCount, } ``` #### Changes to `PlayerInfo` Add to [`PlayerInfo`](holdem_core/src/game/player_info.rs:35): ```rust /// The player's overall bankroll balance across all hands. /// Positive = profit, negative = loss. pub bankroll: i64, ``` #### Changes to `GameInfo` Add to [`GameInfo`](holdem_core/src/game/game_info.rs:21): ```rust /// Whether bankroll reset mode is enabled. pub reset_bankroll_each_hand: bool, ``` ### 2.3 Behavior Flow #### Reset Mode (`reset_bankroll_each_hand = true`) ```mermaid flowchart TD A[Before start_hand] --> B{reset_bankroll_each_hand?} B -- Yes --> C[For each seated player] C --> D[bankroll += stack as i64 - starting_chips as i64] D --> E[stack = starting_chips] E --> F[state = Active] B -- No --> G{refill_threshold > 0?} G -- Yes --> H[For each seated player] H --> I{stack <= threshold AND state != SittingOut?} I -- Yes --> J[refill_amount = starting_chips - stack] J --> K[bankroll -= refill_amount as i64] K --> L[stack = starting_chips] I -- No --> M[Skip] G -- No --> N[Proceed normally] F --> N L --> N M --> N ``` **Hook point**: In [`Table::start_hand()`](holdem_core/src/game/table.rs:288), insert the bankroll logic **before** step 3 (advance button). The new step 2.5: ```rust // In Table::start_hand(), after the active_count check: // 2.5. Bankroll management. if self.reset_bankroll_each_hand { self.reset_stacks_to_starting(); } else if self.bankroll_refill_threshold > 0 { self.refill_short_stacks(); } ``` New private methods on `Table`: ```rust impl Table { /// Reset all seated players' stacks to starting_chips. /// Track the change in their bankroll. fn reset_stacks_to_starting(&mut self) { let starting = self.starting_chips; for slot in &mut self.seats { if let Some(seat) = slot.as_mut() { // The "cost" of resetting is the difference between // starting and current stack. // If they have more than starting, they profit. // If they have less, they lost. seat.bankroll += seat.stack as i64 - starting as i64; seat.stack = starting; // Re-activate sitting-out players (they have chips again). if seat.state == PlayerState::SittingOut { seat.state = PlayerState::Active; } } } } /// Refill short-stacked players up to starting_chips. fn refill_short_stacks(&mut self) { let threshold = self.bankroll_refill_threshold; let starting = self.starting_chips; for slot in &mut self.seats { if let Some(seat) = slot.as_mut() { if seat.state == PlayerState::SittingOut { continue; } if seat.stack <= threshold { let refill = starting.saturating_sub(seat.stack); if refill > 0 { seat.bankroll -= refill as i64; seat.stack = starting; } } } } } } ``` **Important**: When `reset_bankroll_each_hand = true`, busted players (stack = 0, state = SittingOut) are re-activated with a full stack. This means all players play every hand — the simulation tracks their cumulative profit/loss via `bankroll`. When `bankroll_refill_threshold > 0` (without reset), only players below the threshold get refilled. Busted players (SittingOut) are **not** refilled — they stay out. This matches real poker behavior where a bust-out is different from being short-stacked. If you want busted players to also reload, set `reset_bankroll_each_hand = true` instead. #### After Hand Completion In [`Table::complete_hand()`](holdem_core/src/game/table.rs:351), after restoring seats, the bankroll needs to be updated with the hand results. The chip movements from `HandResult` already reflect the net change per hand. However, with `reset_bankroll_each_hand`, the bankroll is updated at the **start** of the next hand (via `reset_stacks_to_starting`), not at the end of the current one. This is simpler and avoids double-counting. For `bankroll_refill_threshold` mode (without reset), the bankroll is only affected by refill purchases. The stack changes from winning/losing are already reflected in `seat.stack`. The `bankroll` field tracks the additional money spent on refills. ### 2.4 Stats Impact The existing [`SimulationStats::record_final_stacks()`](holdem_core/src/game/stats.rs:153) computes `total_profit = final_stack - starting_stack`. With bankroll management, this is no longer accurate. **Solution**: Add `bankroll` tracking to stats: ```rust // In PlayerStats: /// Total bankroll balance (includes refill costs and reset adjustments). pub bankroll: i64, ``` Update [`SimulationStats::record_final_stacks()`](holdem_core/src/game/stats.rs:153) to also record bankroll: ```rust pub fn record_final_stacks( &mut self, stacks: &HashMap, starting_stacks: &HashMap, bankrolls: &HashMap, // NEW parameter ) { for (&player_id, &final_stack) in stacks { if let Some(stats) = self.players.get_mut(&player_id) { stats.final_stack = final_stack; let starting = starting_stacks.get(&player_id).copied().unwrap_or(0); stats.bankroll = *bankrolls.get(&player_id).unwrap_or(&0); // With bankroll tracking, total_profit = bankroll // (bankroll already captures all net flows) if stats.bankroll != 0 { stats.total_profit = stats.bankroll; } else { // Fallback for non-bankroll mode stats.total_profit = final_stack as i64 - starting as i64; } } } } ``` In [`run_cash_game()`](holdem_core/src/game/simulation.rs:87), collect bankrolls alongside final stacks: ```rust let bankrolls: HashMap = table .seats() .iter() .filter_map(|slot| { slot.as_ref().map(|seat| (seat.player_id, seat.bankroll)) }) .collect(); stats.record_final_stacks(&final_stacks, &starting_stacks, &bankrolls); ``` ### 2.5 Edge Cases | Scenario | Behavior | |----------|----------| | Player wins big, stack > starting_chips, reset mode | bankroll increases by the surplus, stack reset to starting_chips | | Player busts, reset mode | Re-activated with starting_chips, bankroll decreases by starting_chips | | Refill threshold = 0 | No refills (disabled) | | Refill threshold >= starting_chips | Refill every hand (unusual but valid) | | Both reset and refill enabled | Reset takes precedence (refill is skipped) | | Tournament mode | Bankroll management is **cash-game only** — ignored for tournaments | | Bankroll goes very negative | `i64` provides ~9 quintillion range — no practical overflow concern | ### 2.6 TOML Config Example ```toml # Example: Cash game with bankroll reset (tournament-style tracking) [game] game_type = "Cash" num_seats = 6 small_blind = 10 big_blind = 20 starting_chips = 10000 num_hands = 1000 reset_bankroll_each_hand = true [game.bet_limit] type = "no_limit" min_raise = 20 ``` ```toml # Example: Cash game with auto-refill [game] game_type = "Cash" num_seats = 6 small_blind = 10 big_blind = 20 starting_chips = 10000 num_hands = 1000 bankroll_refill_threshold = 1000 # Refill when stack <= 1000 [game.bet_limit] type = "no_limit" min_raise = 20 ``` --- ## Feature 3: Randomized Seats ### 3.1 Config Changes Add one field to [`GameConfig`](holdem_core/src/game/config.rs:178): ```rust /// If true, player seat assignments are randomized before each hand /// (cash game) or before tournament start. #[serde(default)] pub randomize_seats: bool, ``` **Default**: `false`. Uses `#[serde(default)]` for backward compatibility. ### 3.2 Data Model Changes Add to [`Table`](holdem_core/src/game/table.rs:64): ```rust pub struct Table { // ... existing fields ... /// If true, shuffle seat assignments before each hand. randomize_seats: bool, } ``` Add to [`TableConfig`](holdem_core/src/game/table.rs:29): ```rust pub struct TableConfig { // ... existing fields ... pub randomize_seats: bool, } ``` ### 3.3 Behavior Flow ```mermaid flowchart TD A[Table::start_hand] --> B{randomize_seats?} B -- Yes --> C[Collect all seated players into a Vec] C --> D[Shuffle the Vec using seeded RNG] D --> E[Clear all seats] E --> F[Re-seat players in shuffled order to positions 0..N] B -- No --> G[Proceed with existing seat assignments] F --> G G --> H[Advance button and continue hand start] ``` **Hook point**: In [`Table::start_hand()`](holdem_core/src/game/table.rs:288), insert randomization **before** step 3 (advance button) and **after** bankroll management. The new step 2.7: ```rust // In Table::start_hand(): // 2.7. Randomize seats if configured. if self.randomize_seats { self.shuffle_seats()?; } ``` New method on `Table`: ```rust impl Table { /// Randomize seat assignments by collecting all seated players, /// shuffling them, and re-seating at positions 0..N. fn shuffle_seats(&mut self) -> Result<(), GameError> { // 1. Collect all occupied seats. let mut occupied: Vec = self .seats .iter_mut() .filter_map(|slot| slot.take()) .collect(); if occupied.len() < 2 { // Put them back. for (i, seat) in occupied.into_iter().enumerate() { self.seats[i] = Some(seat); } return Ok(()); } // 2. Shuffle using seeded RNG for reproducibility. // Use a seed derived from the table seed + hand number. let rng_seed = self.seed .map(|s| s.wrapping_add(self.hand_number)) .unwrap_or_else(|| { // Use a simple hash of hand_number as fallback. self.hand_number }); let mut rng = rand::rngs::StdRng::seed_from_u64(rng_seed); use rand::seq::SliceRandom; occupied.shuffle(&mut rng); // 3. Clear all seats (already empty from take). // 4. Re-seat at positions 0, 1, 2, ... for (i, mut seat) in occupied.into_iter().enumerate() { seat.index = i as SeatIndex; self.seats[i] = Some(seat); } // 5. Reset button to 0 since seat assignments changed. self.button = 0; Ok(()) } } ``` **For tournaments**: Randomization happens once at tournament start, not per hand. The tournament code in [`tournament.rs`](holdem_core/src/game/tournament.rs) would call a `Table::shuffle_seats()` method during initialization. This is a separate call site but uses the same method. ### 3.4 Stats Impact No new stats fields needed. Seat randomization is transparent to stats tracking, which uses `PlayerId` (not seat index) as the key. ### 3.5 Edge Cases | Scenario | Behavior | |----------|----------| | Only 1 player seated | No shuffle (not enough players) | | No seed configured | Use hand_number as a fallback seed | | Seed configured | Use `seed + hand_number` for per-hand variation | | During a hand | No-op (randomization only happens between hands) | | Tournament | Shuffle once at start, not per hand | | Button position | Reset to 0 after shuffle to avoid invalid button references | ### 3.6 TOML Config Example ```toml [game] game_type = "Cash" num_seats = 6 small_blind = 10 big_blind = 20 starting_chips = 10000 num_hands = 1000 randomize_seats = true [game.bet_limit] type = "no_limit" min_raise = 20 ``` --- ## Cross-Feature Interactions ### Rake + Bankroll Reset When both are enabled: 1. Rake is deducted from pot winnings during the hand (inside `HandRunner`) 2. Bankroll reset happens between hands (inside `Table`) 3. The `chip_movements` in `HandResult` already reflect rake deductions 4. Bankroll tracking picks up the post-rake stack values The interaction is clean because rake operates **within** a hand and bankroll operates **between** hands. ### Rake + Stats With rake enabled, `total_profit` across all players will sum to `-(total_rake as i64)` instead of 0. This is correct — the rake is money leaving the system. ### Bankroll Reset + Randomized Seats Both operate between hands in `Table::start_hand()`. The execution order is: 1. Bankroll management (reset/refill) — step 2.5 2. Seat randomization — step 2.7 3. Advance button — step 3 This order ensures bankroll adjustments happen before seats are shuffled. ### All Three Together ```mermaid flowchart TD A[Table::start_hand] --> B[Validate: enough players?] B -- No --> ERR[Return NotEnoughPlayers] B -- Yes --> C{reset_bankroll?} C -- Yes --> D[Reset all stacks to starting_chips] C -- No --> E{refill_threshold > 0?} E -- Yes --> F[Refill short stacks] E -- No --> G[Skip] D --> H{randomize_seats?} F --> H G --> H H -- Yes --> I[Shuffle seat assignments] H -- No --> J[Skip] I --> K[Advance button] J --> K K --> L[Collect active seats] L --> M[Create HandRunner with RakeConfig] M --> N[Start hand: post blinds, deal] ``` --- ## Implementation Todo List The following is the recommended implementation order, broken into independent units: ### Phase 1: Rake 1. Add `RakeConfig` struct to `pot.rs` (or a new `rake.rs` module) 2. Add `rake_percentage` and `rake_cap` fields to `GameConfig` with `#[serde(default)]` 3. Add `RakeConfig::from_game_config()` conversion 4. Add `rake_config: RakeConfig` to `TableConfig` and `Table` 5. Add `rake_config: RakeConfig` to `HandRunner` 6. Implement `HandRunner::apply_rake()` helper method 7. Update `HandRunner::showdown()` to call `apply_rake()` 8. Update `HandRunner::resolve_last_player_wins()` to call `apply_rake()` 9. Add `total_rake` and `rake_per_pot` to `HandResult` 10. Add `total_rake` to `SimulationStats` and update `record_hand_result()` 11. Update `GameConfig::to_table_config()` to pass rake config 12. Add unit tests for `RakeConfig::calculate_rake()` 13. Add integration tests for raked hands ### Phase 2: Bankroll Management 1. Add `bankroll: i64` field to `Seat` (default 0) 2. Add `reset_bankroll_each_hand` and `bankroll_refill_threshold` to `GameConfig` 3. Add these fields to `TableConfig` and `Table` 4. Implement `Table::reset_stacks_to_starting()` 5. Implement `Table::refill_short_stacks()` 6. Update `Table::start_hand()` to call bankroll management 7. Add `bankroll: i64` to `PlayerInfo` and update `Seat::to_player_info()` 8. Add `reset_bankroll_each_hand: bool` to `GameInfo` 9. Update `SimulationStats::record_final_stacks()` to accept bankrolls 10. Update `run_cash_game()` to collect bankrolls 11. Add `bankroll: i64` to `PlayerStats` 12. Add tests for reset mode, refill mode, and edge cases ### Phase 3: Randomized Seats 1. Add `randomize_seats: bool` to `GameConfig`, `TableConfig`, and `Table` 2. Implement `Table::shuffle_seats()` using seeded RNG 3. Update `Table::start_hand()` to call shuffle when enabled 4. Ensure `rand` crate is available as a dependency 5. Add tests for seat randomization with and without seed 6. Add tournament integration (shuffle at start) ### Phase 4: Cross-Cutting 1. Update existing TOML config examples with commented-out new fields 2. Update `GameConfig::default()` to include new fields 3. Update `SimulationStats::summary()` to display rake and bankroll info 4. Verify backward compatibility: run existing tests unchanged 5. Update `docs/` README with new config options --- ## Summary of All Struct Changes | Struct | File | New Fields | |--------|------|------------| | `GameConfig` | `config.rs` | `rake_percentage: f64`, `rake_cap: ChipCount`, `reset_bankroll_each_hand: bool`, `bankroll_refill_threshold: ChipCount`, `randomize_seats: bool` | | `TableConfig` | `table.rs` | `rake_config: RakeConfig`, `reset_bankroll_each_hand: bool`, `bankroll_refill_threshold: ChipCount`, `starting_chips: ChipCount`, `randomize_seats: bool` | | `Table` | `table.rs` | `rake_config: RakeConfig`, `reset_bankroll_each_hand: bool`, `bankroll_refill_threshold: ChipCount`, `starting_chips: ChipCount`, `randomize_seats: bool` | | `HandRunner` | `hand_runner.rs` | `rake_config: RakeConfig` | | `HandResult` | `hand_runner.rs` | `total_rake: ChipCount`, `rake_per_pot: Vec` | | `Seat` | `player.rs` | `bankroll: i64` | | `PlayerInfo` | `player_info.rs` | `bankroll: i64` | | `GameInfo` | `game_info.rs` | `reset_bankroll_each_hand: bool` | | `PlayerStats` | `stats.rs` | `total_rake_paid: ChipCount`, `bankroll: i64` | | `SimulationStats` | `stats.rs` | `total_rake: ChipCount` | | `RakeConfig` | NEW | `percentage: f64`, `cap: ChipCount` |