Skip to main content

rbp_gameplay/
game.rs

1use super::*;
2use rbp_cards::*;
3use rbp_core::*;
4use std::ops::Not;
5
6/// The memoryless state of a poker hand.
7///
8/// `Game` is the core state machine for No-Limit Texas Hold'em, encoding everything
9/// needed to determine legal actions and compute payoffs. It manages player stacks,
10/// the pot, community cards, and whose turn it is to act.
11///
12/// # Architecture
13///
14/// The design is deliberately memoryless: `Game` contains only the current state,
15/// not the history of how we got here. This makes it suitable as a CFR node
16/// representation where states can be reached via different action sequences.
17///
18/// State transitions are functional—[`apply`](Self::apply) returns a new `Game`
19/// rather than mutating in place. This enables tree traversal without undo logic.
20///
21/// # Fields
22///
23/// - `pot` — Total chips in the center (including current street bets)
24/// - `board` — Community cards (0–5 depending on street)
25/// - `seats` — Per-player state (stack, stake, status, hole cards)
26/// - `dealer` — Button position (0 or 1 for heads-up)
27/// - `ticker` — Action counter for determining whose turn it is
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Game {
30    pot: Chips,
31    board: Board,
32    seats: [Seat; N],
33    dealer: Position,
34    ticker: Position,
35}
36
37impl Default for Game {
38    fn default() -> Self {
39        let mut deck = Deck::new();
40        Self {
41            pot: 0,
42            board: Board::empty(),
43            seats: [(); N]
44                .map(|_| deck.hole())
45                .map(|h| (h, STACK))
46                .map(Seat::from),
47            dealer: 0usize,
48            ticker: 0usize,
49        }
50    }
51}
52
53/// Game tree entry points.
54impl Game {
55    /// Creates the canonical starting state for MCCFR traversal.
56    ///
57    /// Returns a game with blinds posted and ready for the dealer's first
58    /// decision. Default stack is 100bb with P0 on the button.
59    pub fn root() -> Self {
60        let mut game = Self::default();
61        game.act(game.posts());
62        game.act(game.posts());
63        game
64    }
65    /// Replaces all players' hole cards with the given hand.
66    ///
67    /// Used for setting up counterfactual game states during analysis.
68    pub fn wipe(mut self, hole: Hole) -> Self {
69        for seat in self.seats.iter_mut() {
70            seat.reset_cards(hole);
71        }
72        self
73    }
74    /// Replaces all players' hole cards EXCEPT the given seat.
75    ///
76    /// Used for computing opponent reach: sets all non-hero seats to the
77    /// assumed opponent hole while preserving hero's cards.
78    ///
79    /// TODO
80    /// this might be incorrect, i don't know if it takes into considration the relativity of
81    /// dealer position in Turn.
82    pub fn assume(mut self, hero: Turn, hole: Hole) -> Self {
83        self.seats
84            .iter_mut()
85            .enumerate()
86            .filter(|(i, _)| Turn::Choice(*i) != hero)
87            .for_each(|(_, seat)| seat.reset_cards(hole));
88        self
89    }
90    /// Fast-forward to the given street by taking passive actions.
91    ///
92    /// From the root state, advances the game by repeatedly applying
93    /// `passive()` (check if allowed, fold otherwise) until reaching
94    /// the target street. This is useful for constructing subgame roots
95    /// at arbitrary streets for exact subgame solving.
96    ///
97    /// # Panics
98    ///
99    /// Panics if:
100    /// - The target street has already passed
101    /// - The game reaches a terminal state before the target street
102    /// - An all-in situation occurs before reaching the target street
103    pub fn ffwd(mut self, target: Street) -> Self {
104        while self.street() < target {
105            match self.turn() {
106                Turn::Terminal => panic!("reached terminal before target street"),
107                Turn::Chance => {
108                    let action = self.reveal();
109                    self.act(action);
110                }
111                Turn::Choice(_) => {
112                    let action = self.passive();
113                    self.act(action);
114                }
115            }
116        }
117        debug_assert_eq!(self.street(), target, "overshot target street");
118        self
119    }
120}
121
122/// Public state accessors.
123impl Game {
124    /// Number of players (constant for heads-up).
125    pub fn n(&self) -> usize {
126        self.seats.len()
127    }
128    /// Total chips in the pot.
129    pub fn pot(&self) -> Chips {
130        self.pot
131    }
132    /// All player seats.
133    pub fn seats(&self) -> [Seat; N] {
134        self.seats
135    }
136    /// Community cards on the board.
137    pub fn board(&self) -> Board {
138        self.board
139    }
140    /// Determines whether it's a player's turn, chance node, or terminal.
141    pub fn turn(&self) -> Turn {
142        if self.must_stop() {
143            Turn::Terminal
144        } else if self.must_deal() {
145            Turn::Chance
146        } else {
147            Turn::Choice(self.actor_idx())
148        }
149    }
150    /// The seat of the player to act.
151    pub fn actor(&self) -> &Seat {
152        self.actor_ref()
153    }
154    /// The current observation from the acting player's perspective.
155    pub fn sweat(&self) -> Observation {
156        Observation::from((
157            Hand::from(self.actor().cards()), //
158            Hand::from(self.board()),         //
159        ))
160    }
161    /// The dealer position as a turn.
162    pub fn dealer(&self) -> Turn {
163        Turn::Choice(self.dealer)
164    }
165    /// Current street based on board cards.
166    pub fn street(&self) -> Street {
167        self.board.street()
168    }
169}
170
171/// Action validation and application.
172impl Game {
173    /// Applies an action mutably and returns a clone of the new state.
174    pub fn consume(&mut self, action: Action) -> Self {
175        self.act(action);
176        self.clone()
177    }
178    /// Returns a new game state with the action applied.
179    ///
180    /// Panics if the action is not legal in the current state.
181    pub fn apply(&self, action: Action) -> Self {
182        self.try_apply(action).expect("valid action")
183    }
184    /// Fallible version of [`apply`](Self::apply).
185    ///
186    /// Returns `Err` if the action is not legal in the current state,
187    /// enabling graceful error handling instead of panicking.
188    pub fn try_apply(&self, action: Action) -> anyhow::Result<Self> {
189        if !self.is_allowed(&action) {
190            return Err(anyhow::anyhow!(
191                "illegal action {:?} in state {:?}",
192                action,
193                self.turn()
194            ));
195        }
196        let mut child = self.clone();
197        child.act(action);
198        Ok(child)
199    }
200    /// Returns all legal actions in the current state.
201    ///
202    /// Empty at terminal nodes. Contains exactly one action at chance nodes.
203    /// Contains multiple options at decision nodes.
204    pub fn legal(&self) -> Vec<Action> {
205        // action is determined if it's Turn::Chance
206        if self.must_stop() {
207            return vec![];
208        }
209        if self.must_deal() {
210            return vec![self.reveal()];
211        }
212        if self.must_post() {
213            return vec![self.posts()];
214        }
215        // now it's certainly a Turn::Choice
216        let mut options = Vec::new();
217        if self.may_raise() {
218            options.push(self.raise());
219        }
220        if self.may_shove() {
221            options.push(self.shove());
222        }
223        if self.may_call() {
224            options.push(self.calls());
225        }
226        if self.may_fold() {
227            options.push(self.folds());
228        }
229        if self.may_check() {
230            options.push(self.check());
231        }
232        debug_assert!(options.len() > 0);
233        options
234    }
235    /// Checks if a specific action is legal.
236    ///
237    /// Performs bounds checking for raises (min/max) and draws (correct cards).
238    pub fn is_allowed(&self, action: &Action) -> bool {
239        // do "bounds checking" on the two actions with degrees of freedom;
240        // Action::Raise is constrained by min/max raise
241        // Action::Draw is constrained by the deck and the number of cards
242        match action {
243            Action::Raise(raise) => {
244                self.may_raise()
245                    && self.must_stop().not()
246                    && self.must_deal().not()
247                    && raise.clone() >= self.to_raise()
248                    && raise.clone() <= self.to_shove() - 1
249            }
250            Action::Draw(cards) => {
251                self.must_deal()
252                    && self.must_stop().not()
253                    && cards.clone().all(|c| self.deck().contains(&c))
254                    && cards.count() == self.board().street().next().n_revealed()
255            }
256            other => self.legal().contains(other),
257        }
258    }
259}
260
261/// Hand-to-hand transitions.
262impl Game {
263    /// Advances to the next hand if both players have sufficient stacks.
264    ///
265    /// Returns `None` if a player is busted (can't cover the big blind).
266    /// Otherwise resets the board, deals new cards, posts blinds, and
267    /// rotates the button.
268    pub fn continuation(mut self) -> Option<Self> {
269        debug_assert!(self.turn() == Turn::Terminal);
270        self.settlements()
271            .iter()
272            .zip(self.seats())
273            .all(|(s, seat)| seat.stack() + s.pnl().reward() >= Game::bblind())
274            .then(|| {
275                self.give_chips();
276                self.wipe_board();
277                self.wipe_seats();
278                self.move_button();
279                self.act(self.posts());
280                self.act(self.posts());
281                self
282            })
283    }
284
285    fn give_chips(&mut self) {
286        for (_, (settlement, seat)) in self
287            .settlements()
288            .iter()
289            .zip(self.seats.iter_mut())
290            .enumerate()
291            .inspect(|(i, (x, s))| log::trace!("{} {} {:>7} {}", i, s.cards(), s.stack(), x.won()))
292        {
293            seat.win(settlement.pnl().reward());
294        }
295        self.pot = 0;
296    }
297
298    fn wipe_board(&mut self) {
299        debug_assert!(self.pot() == 0);
300        self.board.clear();
301    }
302    fn wipe_seats(&mut self) {
303        debug_assert!(self.pot() == 0);
304        debug_assert!(self.street() == Street::Pref);
305        let mut deck = Deck::new();
306        for seat in self.seats.iter_mut() {
307            seat.reset_state(State::Betting);
308            seat.reset_cards(deck.hole());
309            seat.reset_stake();
310            seat.reset_spent();
311        }
312    }
313
314    fn move_button(&mut self) {
315        debug_assert!(self.pot() == 0);
316        debug_assert!(self.seats.len() == self.n());
317        debug_assert!(self.street() == Street::Pref);
318        self.dealer = self.dealer + 1;
319        self.dealer = self.dealer % self.n();
320        self.ticker = 0;
321    }
322}
323
324/// Private mutation methods.
325impl Game {
326    /// Core state transition logic.
327    fn act(&mut self, a: Action) {
328        debug_assert!(self.is_allowed(&a));
329        match a {
330            Action::Check => {
331                self.next_player();
332            }
333            Action::Fold => {
334                self.fold();
335                self.next_player();
336            }
337            Action::Call(chips)
338            | Action::Blind(chips)
339            | Action::Raise(chips)
340            | Action::Shove(chips) => {
341                self.bet(chips);
342                self.next_player();
343            }
344            Action::Draw(cards) => {
345                self.show(cards);
346                self.next_player();
347                self.next_street();
348            }
349        }
350    }
351    fn bet(&mut self, bet: Chips) {
352        debug_assert!(self.actor_ref().stack() >= bet);
353        self.pot += bet;
354        self.actor_mut().bet(bet);
355        if self.actor_ref().stack() == 0 {
356            self.allin();
357        }
358    }
359    fn allin(&mut self) {
360        self.actor_mut().reset_state(State::Shoving);
361    }
362    fn fold(&mut self) {
363        self.actor_mut().reset_state(State::Folding);
364    }
365    fn show(&mut self, hand: Hand) {
366        self.ticker = 0;
367        self.board.add(hand);
368    }
369}
370
371/// Street and player advancement.
372impl Game {
373    /// Resets per-street stakes when a new street begins.
374    fn next_street(&mut self) {
375        for seat in self.seats.iter_mut() {
376            seat.reset_stake();
377        }
378    }
379    /// Advances to the next active player, skipping folded/all-in players.
380    fn next_player(&mut self) {
381        if !self.is_everyone_alright() {
382            loop {
383                self.ticker += 1;
384                match self.actor_ref().state() {
385                    State::Betting => break,
386                    State::Folding => continue,
387                    State::Shoving => continue,
388                }
389            }
390        }
391    }
392}
393
394/// Termination and continuation predicates.
395impl Game {
396    /// True if the hand is complete (showdown or everyone folded).
397    pub fn must_stop(&self) -> bool {
398        if self.street() == Street::Rive {
399            self.is_everyone_alright()
400        } else {
401            self.is_everyone_folding()
402        }
403    }
404    /// True if we need to deal the next street's cards.
405    pub fn must_deal(&self) -> bool {
406        self.street() != Street::Rive && self.is_everyone_alright()
407    }
408    /// True if blinds have not yet been posted.
409    pub fn must_post(&self) -> bool {
410        self.street() == Street::Pref && self.pot() < Self::sblind() + Self::bblind()
411    }
412    /// All players have acted and the pot is right.
413    fn is_everyone_alright(&self) -> bool {
414        self.is_everyone_calling() || self.is_everyone_folding() || self.is_everyone_shoving()
415    }
416    /// All betting players are in for the same amount.
417    fn is_everyone_calling(&self) -> bool {
418        self.is_everyone_touched() && self.is_everyone_matched()
419    }
420    /// All players have acted at least once this street.
421    fn is_everyone_touched(&self) -> bool {
422        self.ticker > self.n() + if self.street() == Street::Pref { 1 } else { 0 }
423    }
424    /// All betting players are in for the effective stake.
425    fn is_everyone_matched(&self) -> bool {
426        let stake = self.stakes();
427        self.seats
428            .iter()
429            .filter(|s| s.state() == State::Betting)
430            .all(|s| s.stake() == stake)
431    }
432    /// All non-folded players are all-in.
433    fn is_everyone_shoving(&self) -> bool {
434        self.seats
435            .iter()
436            .filter(|s| s.state() != State::Folding)
437            .all(|s| s.state() == State::Shoving)
438    }
439    /// Exactly one player remains (all others folded).
440    fn is_everyone_folding(&self) -> bool {
441        self.seats
442            .iter()
443            .filter(|s| s.state() != State::Folding)
444            .count()
445            == 1
446    }
447    /// True if folding is a legal option (facing a bet).
448    pub fn may_fold(&self) -> bool {
449        matches!(self.turn(), Turn::Choice(_)) && self.to_call() > 0
450    }
451    /// True if calling is legal (facing a bet we can cover).
452    pub fn may_call(&self) -> bool {
453        matches!(self.turn(), Turn::Choice(_))
454            && self.may_fold()
455            && self.to_call() < self.to_shove()
456    }
457    /// True if checking is legal (no bet to call).
458    pub fn may_check(&self) -> bool {
459        matches!(self.turn(), Turn::Choice(_)) && self.stakes() == self.actor_ref().stake()
460    }
461    /// True if raising is legal (have chips beyond the min-raise).
462    pub fn may_raise(&self) -> bool {
463        matches!(self.turn(), Turn::Choice(_)) && self.to_raise() < self.to_shove()
464    }
465    /// True if shoving (all-in) is legal.
466    pub fn may_shove(&self) -> bool {
467        matches!(self.turn(), Turn::Choice(_)) && self.to_shove() > 0
468    }
469}
470
471/// Bet sizing constraints and action constructors.
472impl Game {
473    /// Chips needed to call the current bet.
474    pub fn to_call(&self) -> Chips {
475        self.stakes() - self.actor_ref().stake()
476    }
477    /// Blind amount to post (SB or BB depending on position).
478    pub fn to_post(&self) -> Chips {
479        debug_assert!(self.street() == Street::Pref);
480        if self.actor_idx() == self.dealer {
481            Self::sblind().min(self.actor_ref().stack())
482        } else {
483            Self::bblind().min(self.actor_ref().stack())
484        }
485    }
486    /// All remaining chips (for all-in).
487    pub fn to_shove(&self) -> Chips {
488        self.actor_ref().stack()
489    }
490    /// Minimum legal raise size.
491    ///
492    /// Computed as: chips to call + max(last raise increment, big blind).
493    pub fn to_raise(&self) -> Chips {
494        let (most_large_stake, next_large_stake) = self
495            .seats
496            .iter()
497            .filter(|s| s.state() != State::Folding)
498            .map(|s| s.stake())
499            .fold((0, 0), |(most, next), stake| {
500                if stake > most {
501                    (stake, most)
502                } else if stake > next {
503                    (most, stake)
504                } else {
505                    (most, next)
506                }
507            });
508        let relative_raise = most_large_stake - self.actor().stake();
509        let marginal_raise = most_large_stake - next_large_stake;
510        let required_raise = std::cmp::max(marginal_raise, Self::bblind());
511        relative_raise + required_raise
512    }
513    /// Constructs a minimum-raise action.
514    pub fn raise(&self) -> Action {
515        Action::Raise(self.to_raise())
516    }
517    /// Constructs an all-in action.
518    pub fn shove(&self) -> Action {
519        Action::Shove(self.to_shove())
520    }
521    /// Constructs a call action.
522    pub fn calls(&self) -> Action {
523        Action::Call(self.to_call())
524    }
525    /// Constructs a blind-posting action.
526    pub fn posts(&self) -> Action {
527        Action::Blind(self.to_post())
528    }
529    /// Constructs a fold action.
530    pub fn folds(&self) -> Action {
531        Action::Fold
532    }
533    /// Constructs a check action.
534    pub fn check(&self) -> Action {
535        Action::Check
536    }
537    /// Returns check if allowed, otherwise fold.
538    pub fn passive(&self) -> Action {
539        if self.may_check() {
540            Action::Check
541        } else {
542            Action::Fold
543        }
544    }
545    /// Deals the next street's cards from the deck.
546    pub fn reveal(&self) -> Action {
547        Action::Draw(self.deck().deal(self.street()))
548    }
549}
550
551/// Showdown and payout logic.
552impl Game {
553    /// Computes final chip distributions at a terminal node.
554    pub fn settlements(&self) -> Vec<Settlement> {
555        debug_assert!(self.must_stop(), "non terminal game state:\n{}", self);
556        Showdown::from(self.ledger()).settle()
557    }
558    /// Returns true if this is a showdown (multiple players remain).
559    pub fn is_showdown(&self) -> bool {
560        self.seats.iter().filter(|s| s.state().is_active()).count() > 1
561    }
562    fn ledger(&self) -> Vec<Settlement> {
563        self.seats
564            .iter()
565            .enumerate()
566            .map(|(position, _)| self.settlement(position))
567            .collect()
568    }
569    fn settlement(&self, position: usize) -> Settlement {
570        let seat = &self.seats[position];
571        let strength = Strength::from(Hand::add(
572            Hand::from(seat.cards()),
573            Hand::from(self.board()),
574        ));
575        Settlement::from((seat.spent(), seat.state(), strength))
576    }
577}
578
579/// Card operations.
580impl Game {
581    /// Deals random cards for the next street.
582    pub fn draw(&self) -> Hand {
583        self.deck().deal(self.street())
584    }
585    /// Returns the remaining deck (all cards not in play).
586    pub fn deck(&self) -> Deck {
587        let mut removed = Hand::from(self.board);
588        for seat in self.seats.iter() {
589            removed = Hand::or(removed, Hand::from(seat.cards()));
590        }
591        Deck::from(removed.complement())
592    }
593}
594
595/// Position tracking.
596impl Game {
597    /// Index of the player to act.
598    fn actor_idx(&self) -> Position {
599        (self.dealer + self.ticker) % self.n()
600    }
601    fn actor_ref(&self) -> &Seat {
602        self.seats
603            .get(self.actor_idx())
604            .expect("index should be in bounds bc modulo")
605    }
606    fn actor_mut(&mut self) -> &mut Seat {
607        let index = self.actor_idx();
608        self.seats
609            .get_mut(index)
610            .expect("index should be in bounds bc modulo")
611    }
612}
613
614/// Stack and SPR calculations.
615impl Game {
616    /// Total chips in play (pot + all stacks).
617    pub fn total(&self) -> Chips {
618        self.pot() + self.seats().iter().map(|s| s.stack()).sum::<Chips>()
619    }
620    /// Effective stack (minimum stack for heads-up).
621    ///
622    /// In N-way pots this would be the second-largest stack;
623    /// for heads-up it's simply the smaller of the two.
624    pub fn effective(&self) -> Chips {
625        self.seats.iter().map(|s| s.stack()).min().unwrap_or(0)
626    }
627    /// Stack-to-pot ratio (effective stack / pot).
628    pub fn spr(&self) -> f32 {
629        match self.pot() {
630            0 => 0.0,
631            p => self.effective() as f32 / p as f32,
632        }
633    }
634    /// Maximum stake among all players this street.
635    fn stakes(&self) -> Chips {
636        self.seats
637            .iter()
638            .map(|s| s.stake())
639            .max()
640            .expect("non-empty seats")
641    }
642    /// True if this is a preflop opening spot (no player actions yet).
643    /// Used to interpret Odds(n,1) as nBB rather than nx pot.
644    #[allow(dead_code)]
645    fn is_opening(&self) -> bool {
646        self.street() == Street::Pref && self.pot() == Self::sblind() + Self::bblind()
647    }
648}
649
650/// Blind configuration.
651impl Game {
652    /// Returns the blind posting actions [SB, BB].
653    pub const fn blinds() -> [Action; 2] {
654        [Action::Blind(Self::sblind()), Action::Blind(Self::bblind())]
655    }
656    /// Big blind size.
657    pub const fn bblind() -> Chips {
658        rbp_core::B_BLIND
659    }
660    /// Small blind size.
661    pub const fn sblind() -> Chips {
662        rbp_core::S_BLIND
663    }
664}
665
666/// Abstraction interface: mapping between concrete Actions and abstract Edges.
667impl Game {
668    /// Returns all available edges for current game state.
669    /// Expands legal actions into the discretized edge space.
670    pub fn choices(&self, depth: usize) -> Path {
671        self.legal()
672            .into_iter()
673            .flat_map(|action| self.unfold(depth, action))
674            .collect()
675    }
676    /// Expands an action into edges using street-specific bet grids.
677    /// Non-raise actions map 1:1; raises expand to all grid sizes.
678    fn unfold(&self, depth: usize, action: Action) -> Vec<Edge> {
679        match action {
680            Action::Raise(_) => Edge::raises(self.street(), depth),
681            _ => vec![Edge::from(action)],
682        }
683    }
684    /// Converts an abstract [`Edge`] into a concrete [`Action`].
685    /// The resulting action may be illegal; use [`Self::snap`] to coerce.
686    pub fn actionize(&self, edge: Edge) -> Action {
687        match edge {
688            Edge::Fold => Action::Fold,
689            Edge::Draw => self.reveal(),
690            Edge::Call => Action::Call(self.to_call()),
691            Edge::Check => Action::Check,
692            Edge::Shove => Action::Shove(self.to_shove()),
693            Edge::Open(n) => Action::Raise(n * rbp_core::B_BLIND),
694            Edge::Raise(_) => Action::Raise(edge.into_chips(self.pot())),
695        }
696    }
697    /// Converts a concrete [`Action`] into an abstract [`Edge`].
698    /// Raise amounts snap to the closest grid size; other actions map directly.
699    pub fn edgify(&self, action: Action, depth: usize) -> Edge {
700        match action {
701            Action::Fold => Edge::Fold,
702            Action::Check => Edge::Check,
703            Action::Draw(_) => Edge::Draw,
704            Action::Call(_) => Edge::Call,
705            Action::Blind(_) => Edge::Call,
706            Action::Shove(_) => Edge::Shove,
707            Action::Raise(chips) => self.snap_to_edge(chips, depth),
708        }
709    }
710    /// Snaps a chip amount to the nearest edge in the grid.
711    fn snap_to_edge(&self, chips: Chips, depth: usize) -> Edge {
712        let edges = Edge::raises(self.street(), depth);
713        edges
714            .into_iter()
715            .min_by_key(|e| (e.into_chips(self.pot()) as i32 - chips as i32).abs())
716            .unwrap_or(Edge::Shove)
717    }
718    /// Maps an action to the nearest legal action in the current state.
719    ///
720    /// Used for CFR traversal where canonical edges may not correspond to
721    /// legal actions due to stack/pot differences from prior streets.
722    /// Semi-recursive: aggressive actions cascade through the fallback chain
723    /// `Raise → Shove → Call → passive`.
724    ///
725    /// # Mapping rules
726    ///
727    /// - `Raise(x)` where `x >= to_shove()` → recurse with `Shove`
728    /// - `Raise(x)` where `x < to_raise()` → `Raise(to_raise())`
729    /// - `Raise(_)` when `!may_raise()` → recurse with `Shove`
730    /// - `Shove` when `!may_shove()` → recurse with `Call`
731    /// - `Call` when `!may_call()` → `passive()`
732    /// - `Check` when `!may_check()` → `Call` or `Fold`
733    /// - `Fold` when `!may_fold()` → `Check`
734    pub fn snap(&self, action: Action) -> Action {
735        match action {
736            Action::Raise(x) if x >= self.to_shove() => self.snap(self.shove()), //
737            Action::Raise(_) if !self.may_raise() => self.snap(self.shove()),    //
738            Action::Raise(x) if x < self.to_raise() => self.raise(),             //
739            Action::Raise(x) => Action::Raise(x),                                //
740            Action::Shove(_) if self.may_shove() => self.shove(),                //
741            Action::Shove(_) if self.may_call() => self.calls(),                 // ? unnecessary
742            Action::Shove(_) => self.passive(),                                  // ? unreachable
743            Action::Call(_) if self.may_call() => self.calls(),                  // ? unnecessary
744            Action::Call(_) if self.may_shove() => self.shove(),                 // ? unnecessary
745            Action::Call(_) => self.passive(),                                   // ? unnecessary
746            Action::Check if self.may_check() => Action::Check,                  // ? self.passive()
747            Action::Check if self.may_call() => self.calls(),                    // ? self.passive()
748            Action::Check => self.folds(),                                       // ? self.passive()
749            Action::Fold if self.may_fold() => Action::Fold,                     // ? self.passive()
750            Action::Fold => Action::Check,                                       // ? self.passive()
751            Action::Draw(_) | Action::Blind(_) => action,
752        }
753    }
754}
755
756
757impl std::fmt::Display for Game {
758    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
759        for seat in self.seats.iter() {
760            writeln!(
761                f,
762                "{:>3} {:>3} {:<6}",
763                seat.state(),
764                seat.cards(),
765                seat.stack()
766            )?;
767        }
768        writeln!(f, "Pot   {}", self.pot())?;
769        writeln!(f, "Board {}", self.board())?;
770        Ok(())
771    }
772}
773/// Infinite iterator over actions across games.
774///
775/// Yields each `Action` played, resetting to a fresh game when busted.
776/// Never terminates — use `.take(n)` to bound iteration.
777pub struct Perpetual(Game);
778impl Perpetual {
779    pub fn new(game: Game) -> Self {
780        Self(game)
781    }
782}
783impl Iterator for Perpetual {
784    type Item = Action;
785    fn next(&mut self) -> Option<Self::Item> {
786        loop {
787            let actions = self.0.legal();
788            if !actions.is_empty() {
789                let action = actions[rand::random_range(0..actions.len())];
790                self.0 = self.0.apply(action);
791                return Some(action);
792            }
793            self.0 = self.0.clone().continuation().unwrap_or_else(Game::root);
794        }
795    }
796}
797
798/// Iterator over completed hands in a session.
799///
800/// Yields the terminal `Game` state at the end of each hand.
801/// Stops when a player busts (can't cover the big blind).
802pub struct Hands(Game);
803impl Hands {
804    pub fn new(game: Game) -> Self {
805        Self(game)
806    }
807}
808impl Iterator for Hands {
809    type Item = Game;
810    fn next(&mut self) -> Option<Self::Item> {
811        while !self.0.must_stop() {
812            let actions = self.0.legal();
813            let action = actions[rand::random_range(0..actions.len())];
814            self.0 = self.0.apply(action);
815        }
816        let terminal = self.0.clone();
817        self.0 = self.0.clone().continuation()?;
818        Some(terminal)
819    }
820}
821
822/// Iterator over actions in a session.
823///
824/// Yields each `Action` played across multiple hands.
825/// Stops when a player busts (can't cover the big blind).
826pub struct Session(Game);
827impl Session {
828    pub fn new(game: Game) -> Self {
829        Self(game)
830    }
831}
832impl Iterator for Session {
833    type Item = Action;
834    fn next(&mut self) -> Option<Self::Item> {
835        loop {
836            let actions = self.0.legal();
837            if !actions.is_empty() {
838                let action = actions[rand::random_range(0..actions.len())];
839                self.0 = self.0.apply(action);
840                return Some(action);
841            }
842            self.0 = self.0.clone().continuation()?;
843        }
844    }
845}
846
847impl Game {
848    /// Infinite iterator over actions, resetting on bust.
849    pub fn perpetual(self) -> Perpetual {
850        Perpetual::new(self)
851    }
852    /// Iterator over completed hands, stopping when busted.
853    pub fn hands(self) -> Hands {
854        Hands::new(self)
855    }
856    /// Iterator over actions, stopping when busted.
857    pub fn session(self) -> Session {
858        Session::new(self)
859    }
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    /// dealer posts SB, non-dealer posts BB, dealer acts first after blinds
867    #[test]
868    fn test_root() {
869        let game = Game::root();
870        assert_eq!(game.board().street(), Street::Pref);
871        assert_eq!(game.actor().state(), State::Betting);
872        assert_eq!(game.pot(), Game::sblind() + Game::bblind());
873        assert_eq!(game.turn(), Turn::Choice(game.dealer)); // dealer acts first
874    }
875
876    #[test]
877    fn everyone_folds_pref() {
878        let game = Game::root();
879        let game = game.apply(Action::Fold);
880        assert!(game.is_everyone_folding() == true);
881        assert!(game.is_everyone_alright() == true);
882        assert!(game.is_everyone_calling() == false);
883        assert!(game.must_deal() == true); // ambiguous
884        assert!(game.must_stop() == true);
885    }
886
887    #[test]
888    fn everyone_folds_flop() {
889        let game = Game::root();
890        let flop = game.deck().deal(Street::Pref);
891        let game = game.apply(Action::Call(1));
892        let game = game.apply(Action::Check);
893        let game = game.apply(Action::Draw(flop));
894        let game = game.apply(Action::Raise(10));
895        let game = game.apply(Action::Fold);
896        assert!(game.is_everyone_folding() == true);
897        assert!(game.is_everyone_alright() == true);
898        assert!(game.is_everyone_calling() == false);
899        assert!(game.must_deal() == true); // ambiguous
900        assert!(game.must_stop() == true);
901    }
902
903    #[test]
904    fn history_of_checks() {
905        // Blinds
906        let game = Game::root();
907        assert!(game.board().street() == Street::Pref);
908        assert!(game.pot() == 3);
909        assert!(game.must_post() == false);
910        assert!(game.must_stop() == false);
911        assert!(game.must_deal() == false);
912        assert!(game.is_everyone_alright() == false);
913        assert!(game.is_everyone_calling() == false);
914        assert!(game.is_everyone_touched() == false);
915        assert!(game.is_everyone_matched() == false);
916
917        // SmallB Preflop
918        let game = game.apply(Action::Call(1));
919        assert!(game.board().street() == Street::Pref);
920        assert!(game.pot() == 4); //
921        assert!(game.must_post() == false);
922        assert!(game.must_stop() == false);
923        assert!(game.must_deal() == false);
924        assert!(game.is_everyone_alright() == false);
925        assert!(game.is_everyone_calling() == false);
926        assert!(game.is_everyone_touched() == false);
927        assert!(game.is_everyone_matched() == true); //
928
929        // Dealer Preflop
930        let game = game.apply(Action::Check);
931        assert!(game.board().street() == Street::Pref);
932        assert!(game.pot() == 4);
933        assert!(game.must_post() == false);
934        assert!(game.must_stop() == false);
935        assert!(game.must_deal() == true); //
936        assert!(game.is_everyone_alright() == true); //
937        assert!(game.is_everyone_calling() == true); //
938        assert!(game.is_everyone_touched() == true); //
939        assert!(game.is_everyone_matched() == true);
940
941        // Flop
942        let flop = game.deck().deal(game.board().street());
943        let game = game.apply(Action::Draw(flop));
944        assert!(game.board().street() == Street::Flop); //
945        assert!(game.pot() == 4);
946        assert!(game.must_post() == false);
947        assert!(game.must_stop() == false);
948        assert!(game.must_deal() == false); //
949        assert!(game.is_everyone_alright() == false); //
950        assert!(game.is_everyone_calling() == false); //
951        assert!(game.is_everyone_touched() == false); //
952        assert!(game.is_everyone_matched() == true);
953
954        // SmallB Flop
955        let game = game.apply(Action::Check);
956        assert!(game.board().street() == Street::Flop);
957        assert!(game.pot() == 4);
958        assert!(game.must_post() == false);
959        assert!(game.must_stop() == false);
960        assert!(game.must_deal() == false);
961        assert!(game.is_everyone_alright() == false);
962        assert!(game.is_everyone_calling() == false);
963        assert!(game.is_everyone_touched() == false);
964        assert!(game.is_everyone_matched() == true);
965
966        // Dealer Flop
967        let game = game.apply(Action::Check);
968        assert!(game.board().street() == Street::Flop);
969        assert!(game.pot() == 4);
970        assert!(game.must_post() == false);
971        assert!(game.must_stop() == false);
972        assert!(game.must_deal() == true); //
973        assert!(game.is_everyone_alright() == true); //
974        assert!(game.is_everyone_calling() == true); //
975        assert!(game.is_everyone_touched() == true); //
976        assert!(game.is_everyone_matched() == true);
977
978        // Turn
979        let turn = game.deck().deal(game.board().street());
980        let game = game.apply(Action::Draw(turn));
981        assert!(game.board().street() == Street::Turn);
982        assert!(game.pot() == 4);
983        assert!(game.must_post() == false);
984        assert!(game.must_stop() == false);
985        assert!(game.must_deal() == false); //
986        assert!(game.is_everyone_alright() == false); //
987        assert!(game.is_everyone_calling() == false); //
988        assert!(game.is_everyone_touched() == false); //
989        assert!(game.is_everyone_matched() == true);
990
991        // SmallB Turn
992        let game = game.apply(Action::Check);
993        assert!(game.board().street() == Street::Turn);
994        assert!(game.pot() == 4);
995        assert!(game.must_post() == false);
996        assert!(game.must_stop() == false);
997        assert!(game.must_deal() == false);
998        assert!(game.is_everyone_alright() == false);
999        assert!(game.is_everyone_calling() == false);
1000        assert!(game.is_everyone_touched() == false);
1001        assert!(game.is_everyone_matched() == true);
1002
1003        // Dealer Turn
1004        let game = game.apply(Action::Raise(4));
1005        assert!(game.board().street() == Street::Turn);
1006        assert!(game.pot() == 8);
1007        assert!(game.must_post() == false);
1008        assert!(game.must_stop() == false);
1009        assert!(game.must_deal() == false);
1010        assert!(game.is_everyone_alright() == false);
1011        assert!(game.is_everyone_calling() == false);
1012        assert!(game.is_everyone_touched() == true); //
1013        assert!(game.is_everyone_matched() == false); //
1014
1015        // SmallB Turn
1016        let game = game.apply(Action::Call(4));
1017        assert!(game.board().street() == Street::Turn);
1018        assert!(game.pot() == 12); //
1019        assert!(game.must_post() == false);
1020        assert!(game.must_stop() == false);
1021        assert!(game.must_deal() == true); //
1022        assert!(game.is_everyone_alright() == true); //
1023        assert!(game.is_everyone_calling() == true); //
1024        assert!(game.is_everyone_touched() == true);
1025        assert!(game.is_everyone_matched() == true);
1026
1027        // River
1028        let rive = game.deck().deal(game.board().street());
1029        let game = game.apply(Action::Draw(rive));
1030        assert!(game.board().street() == Street::Rive); //
1031        assert!(game.pot() == 12);
1032        assert!(game.must_post() == false);
1033        assert!(game.must_stop() == false);
1034        assert!(game.must_deal() == false); //
1035        assert!(game.is_everyone_alright() == false); //
1036        assert!(game.is_everyone_calling() == false); //
1037        assert!(game.is_everyone_touched() == false); //
1038        assert!(game.is_everyone_matched() == true); //
1039
1040        // SmallB River
1041        let game = game.apply(Action::Check);
1042        assert!(game.board().street() == Street::Rive);
1043        assert!(game.pot() == 12);
1044        assert!(game.must_post() == false);
1045        assert!(game.must_stop() == false);
1046        assert!(game.must_deal() == false);
1047        assert!(game.is_everyone_alright() == false);
1048        assert!(game.is_everyone_calling() == false);
1049        assert!(game.is_everyone_touched() == false);
1050        assert!(game.is_everyone_matched() == true);
1051
1052        // Dealer River
1053        let game = game.apply(Action::Check);
1054        assert!(game.board().street() == Street::Rive);
1055        assert!(game.pot() == 12);
1056        assert!(game.must_post() == false);
1057        assert!(game.must_stop() == true); //
1058        assert!(game.must_deal() == false);
1059        assert!(game.is_everyone_alright() == true); //
1060        assert!(game.is_everyone_calling() == true); //
1061        assert!(game.is_everyone_touched() == true); //
1062        assert!(game.is_everyone_matched() == true); //
1063    }
1064
1065    /// next() resets game state correctly after terminal
1066    #[test]
1067    fn next_after_fold() {
1068        let game = Game::root().apply(Action::Fold);
1069        assert!(game.must_stop());
1070        let next = game.continuation().expect("can continue");
1071        assert_eq!(next.street(), Street::Pref);
1072        assert_eq!(next.pot(), Game::sblind() + Game::bblind());
1073        assert_eq!(next.board(), Board::empty());
1074        assert_eq!(next.dealer, 1); // rotated from 0
1075        assert_eq!(next.turn(), Turn::Choice(1)); // new dealer acts first
1076        assert!(!next.is_everyone_touched());
1077    }
1078
1079    /// dealer rotates correctly across multiple hands
1080    #[test]
1081    fn dealer_rotation() {
1082        let game = Game::root();
1083        assert_eq!(game.dealer, 0);
1084        let game = game.apply(Action::Fold).continuation().unwrap();
1085        assert_eq!(game.dealer, 1);
1086        let game = game.apply(Action::Fold).continuation().unwrap();
1087        assert_eq!(game.dealer, 0); // wraps around
1088        let game = game.apply(Action::Fold).continuation().unwrap();
1089        assert_eq!(game.dealer, 1);
1090    }
1091
1092    /// ticker resets correctly for each new hand, regardless of dealer
1093    #[test]
1094    fn ticker_reset_on_next() {
1095        let g0 = Game::root();
1096        let g1 = g0.apply(Action::Fold).continuation().unwrap();
1097        let g2 = g1.apply(Action::Fold).continuation().unwrap();
1098        // both should have same ticker after blinds, despite different dealers
1099        assert_eq!(g0.ticker, g1.ticker);
1100        assert_eq!(g1.ticker, g2.ticker);
1101        assert_eq!(g0.ticker, 2); // 2 blinds posted
1102    }
1103
1104    /// is_everyone_touched works correctly for dealer=1
1105    #[test]
1106    fn touched_with_rotated_dealer() {
1107        let game = Game::root().apply(Action::Fold).continuation().unwrap();
1108        assert_eq!(game.dealer, 1);
1109        assert!(!game.is_everyone_touched()); // just blinds
1110        let game = game.apply(Action::Call(1));
1111        assert!(!game.is_everyone_touched()); // P1 called, P0 hasn't acted
1112        let game = game.apply(Action::Check);
1113        assert!(game.is_everyone_touched()); // both acted
1114        assert!(game.must_deal());
1115    }
1116
1117    /// multi-street hand with rotated dealer
1118    #[test]
1119    fn full_hand_rotated_dealer() {
1120        let game = Game::root().apply(Action::Fold).continuation().unwrap();
1121        assert_eq!(game.dealer, 1);
1122        // preflop: P1 (dealer) calls, P0 checks
1123        let game = game.apply(Action::Call(1)).apply(Action::Check);
1124        assert!(game.must_deal());
1125        // flop
1126        let flop = game.deck().deal(Street::Pref);
1127        let game = game.apply(Action::Draw(flop));
1128        assert_eq!(game.street(), Street::Flop);
1129        assert_eq!(game.turn(), Turn::Choice(0)); // non-dealer first postflop
1130        assert!(!game.is_everyone_touched());
1131        // P0 checks, P1 checks
1132        let game = game.apply(Action::Check).apply(Action::Check);
1133        assert!(game.is_everyone_touched());
1134        assert!(game.must_deal());
1135    }
1136
1137    /// five consecutive hands, verifying state after each
1138    #[test]
1139    fn five_hands_sequence() {
1140        let mut game = Game::root();
1141        for i in 0..5 {
1142            assert_eq!(game.dealer, i % 2);
1143            assert_eq!(game.pot(), Game::sblind() + Game::bblind());
1144            assert_eq!(game.street(), Street::Pref);
1145            assert!(!game.is_everyone_touched());
1146            assert_eq!(game.turn(), Turn::Choice(game.dealer));
1147            game = game.apply(Action::Fold).continuation().unwrap();
1148        }
1149    }
1150
1151    /// call-check sequence works identically for both dealer positions
1152    #[test]
1153    fn symmetric_preflop_action() {
1154        // dealer=0: P0 calls, P1 checks
1155        let g0 = Game::root();
1156        assert_eq!(g0.dealer, 0);
1157        let g0 = g0.apply(Action::Call(1));
1158        assert!(!g0.is_everyone_touched());
1159        let g0 = g0.apply(Action::Check);
1160        assert!(g0.is_everyone_touched());
1161        assert!(g0.must_deal());
1162        // dealer=1: P1 calls, P0 checks
1163        let g1 = Game::root().apply(Action::Fold).continuation().unwrap();
1164        assert_eq!(g1.dealer, 1);
1165        let g1 = g1.apply(Action::Call(1));
1166        assert!(!g1.is_everyone_touched());
1167        let g1 = g1.apply(Action::Check);
1168        assert!(g1.is_everyone_touched());
1169        assert!(g1.must_deal());
1170    }
1171
1172    /// actor position is correct for both dealers on flop
1173    #[test]
1174    fn flop_actor_both_dealers() {
1175        // dealer=0: non-dealer (P1) acts first on flop
1176        let g0 = Game::root().apply(Action::Call(1)).apply(Action::Check);
1177        let flop = g0.deck().deal(Street::Pref);
1178        let g0 = g0.apply(Action::Draw(flop));
1179        assert_eq!(g0.turn(), Turn::Choice(1)); // P1 (non-dealer) first
1180        // dealer=1: non-dealer (P0) acts first on flop
1181        let g1 = Game::root()
1182            .apply(Action::Fold)
1183            .continuation()
1184            .unwrap()
1185            .apply(Action::Call(1))
1186            .apply(Action::Check);
1187        let flop = g1.deck().deal(Street::Pref);
1188        let g1 = g1.apply(Action::Draw(flop));
1189        assert_eq!(g1.turn(), Turn::Choice(0)); // P0 (non-dealer) first
1190    }
1191
1192    /// shove and call leads to showdown
1193    #[test]
1194    fn allin_showdown() {
1195        let game = Game::root();
1196        let shove = game.to_shove(); // dealer's stack = 99
1197        let game = game.apply(Action::Shove(shove));
1198        // BB's to_call (98) == to_shove (98), so must use Shove not Call
1199        let shove = game.to_shove();
1200        let game = game.apply(Action::Shove(shove));
1201        assert!(game.is_everyone_shoving());
1202        assert!(game.must_stop() || game.must_deal());
1203    }
1204
1205    /// shove and fold is terminal
1206    #[test]
1207    fn allin_fold() {
1208        let game = Game::root();
1209        let shove = game.to_shove();
1210        let game = game.apply(Action::Shove(shove)).apply(Action::Fold);
1211        assert!(game.must_stop());
1212        assert!(game.is_everyone_folding());
1213    }
1214
1215    /// raise-reraise sequence keeps action open
1216    #[test]
1217    fn raise_reraise() {
1218        let g0 = Game::root();
1219        let r1 = g0.to_raise();
1220        let g1 = g0.apply(Action::Raise(r1));
1221        let r2 = g1.to_raise();
1222        let g2 = g1.apply(Action::Raise(r2));
1223        assert!(!g2.must_deal()); // betting not closed
1224        assert!(!g2.is_everyone_alright()); // stakes unmatched
1225        assert_eq!(g2.turn(), Turn::Choice(0)); // back to dealer
1226        assert!(g2.may_raise() || g2.may_call()); // can continue
1227    }
1228
1229    /// stacks update correctly after fold (before new blinds)
1230    #[test]
1231    fn stacks_after_fold() {
1232        let game = Game::root().apply(Action::Fold);
1233        assert!(game.must_stop());
1234        // check settlements before next hand
1235        let settlements = game.settlements();
1236        // reward() is total received, won() is net (reward - risked)
1237        assert_eq!(settlements[0].pnl().reward(), 0); // dealer folded
1238        assert_eq!(settlements[1].pnl().reward(), 3); // BB wins pot
1239        assert_eq!(settlements[0].won(), -1); // lost SB
1240        assert_eq!(settlements[1].won(), 1); // net gain
1241    }
1242
1243    /// stacks update correctly after flop fold
1244    #[test]
1245    fn stacks_after_flop_bet_fold() {
1246        let game = Game::root().apply(Action::Call(1)).apply(Action::Check);
1247        let flop = game.deck().deal(Street::Pref);
1248        let game = game.apply(Action::Draw(flop));
1249        // P1 (non-dealer) acts first, raises
1250        let raise = game.to_raise();
1251        let game = game.apply(Action::Raise(raise));
1252        // P0 folds
1253        let game = game.apply(Action::Fold);
1254        assert!(game.must_stop());
1255        let settlements = game.settlements();
1256        // pot is 4 + raise, P1 wins it all
1257        assert_eq!(settlements[0].pnl().reward(), 0); // dealer folded
1258        assert!(settlements[1].pnl().reward() > 0); // BB wins pot
1259        assert_eq!(settlements[0].won(), -2); // lost 2
1260    }
1261
1262    /// multi-hand with betting, not just folds
1263    #[test]
1264    fn multi_hand_with_betting() {
1265        let g0 = Game::root();
1266        // hand 1: call-check, bet-fold on flop
1267        let g0 = g0.apply(Action::Call(1)).apply(Action::Check);
1268        let flop = g0.deck().deal(Street::Pref);
1269        let g0 = g0.apply(Action::Draw(flop));
1270        let raise = g0.to_raise();
1271        let g0 = g0.apply(Action::Raise(raise)).apply(Action::Fold);
1272        let g1 = g0.continuation().unwrap();
1273        assert_eq!(g1.dealer, 1);
1274        // hand 2: raise-call, bet-fold on flop
1275        let r1 = g1.to_raise();
1276        let g1 = g1.apply(Action::Raise(r1));
1277        let c1 = g1.to_call();
1278        let g1 = g1.apply(Action::Call(c1));
1279        let flop = g1.deck().deal(Street::Pref);
1280        let g1 = g1.apply(Action::Draw(flop));
1281        let raise = g1.to_raise();
1282        let g1 = g1.apply(Action::Raise(raise)).apply(Action::Fold);
1283        let g2 = g1.continuation().unwrap();
1284        assert_eq!(g2.dealer, 0);
1285        assert_eq!(g2.pot(), 3);
1286    }
1287
1288    /// legal() returns correct options preflop after blinds
1289    #[test]
1290    fn legal_preflop_options() {
1291        let game = Game::root();
1292        let legal = game.legal();
1293        assert!(legal.contains(&Action::Fold));
1294        assert!(legal.contains(&Action::Call(1)));
1295        assert!(legal.iter().any(|a| matches!(a, Action::Raise(_))));
1296        assert!(legal.iter().any(|a| matches!(a, Action::Shove(_))));
1297        assert!(!legal.contains(&Action::Check)); // can't check facing BB
1298    }
1299
1300    /// legal() after limp allows check
1301    #[test]
1302    fn legal_bb_can_check() {
1303        let game = Game::root().apply(Action::Call(1));
1304        let legal = game.legal();
1305        assert!(legal.contains(&Action::Check));
1306        assert!(!legal.contains(&Action::Fold)); // no need to fold
1307    }
1308
1309    /// legal() on flop
1310    #[test]
1311    fn legal_flop_options() {
1312        let game = Game::root().apply(Action::Call(1)).apply(Action::Check);
1313        let flop = game.deck().deal(Street::Pref);
1314        let game = game.apply(Action::Draw(flop));
1315        let legal = game.legal();
1316        assert!(legal.contains(&Action::Check));
1317        assert!(legal.iter().any(|a| matches!(a, Action::Raise(_))));
1318        assert!(!legal.contains(&Action::Fold)); // no bet to fold to
1319    }
1320
1321    /// terminal via river showdown
1322    #[test]
1323    fn terminal_river_showdown() {
1324        let mut game = Game::root().apply(Action::Call(1)).apply(Action::Check);
1325        for street in [Street::Pref, Street::Flop, Street::Turn] {
1326            let cards = game.deck().deal(street);
1327            game = game
1328                .apply(Action::Draw(cards))
1329                .apply(Action::Check)
1330                .apply(Action::Check);
1331        }
1332        assert_eq!(game.street(), Street::Rive);
1333        assert!(game.must_stop());
1334        assert!(!game.must_deal());
1335    }
1336
1337    /// ten consecutive hands alternate dealers correctly
1338    #[test]
1339    fn ten_hands_alternation() {
1340        let mut game = Game::root();
1341        for i in 0..10 {
1342            assert_eq!(game.dealer, i % 2);
1343            assert_eq!(game.turn(), Turn::Choice(game.dealer));
1344            game = game.apply(Action::Fold).continuation().unwrap();
1345        }
1346    }
1347
1348    /// min raise calculation
1349    #[test]
1350    fn min_raise_size() {
1351        let game = Game::root();
1352        // dealer stake=1, BB stake=2. to_raise = (2-1) + max(2-1, BB) = 1 + 2 = 3
1353        assert_eq!(game.to_raise(), 3);
1354        let game = game.apply(Action::Raise(3));
1355        // dealer stake=4, BB stake=2. to_raise = (4-2) + max(4-2, BB) = 2 + 2 = 4
1356        assert_eq!(game.to_raise(), 4);
1357    }
1358
1359    /// pot size tracks correctly through streets
1360    #[test]
1361    fn pot_tracking() {
1362        let game = Game::root();
1363        assert_eq!(game.pot(), 3);
1364        let game = game.apply(Action::Call(1));
1365        assert_eq!(game.pot(), 4);
1366        let game = game.apply(Action::Raise(4));
1367        assert_eq!(game.pot(), 8);
1368        let game = game.apply(Action::Call(4));
1369        assert_eq!(game.pot(), 12);
1370    }
1371
1372    /// cannot continue if player busts
1373    #[test]
1374    fn bust_prevents_next() {
1375        // create game where one player will bust
1376        let game = Game::root();
1377        let shove = game.to_shove();
1378        let game = game.apply(Action::Shove(shove));
1379        // BB must shove (not call) since to_call == to_shove
1380        let shove = game.to_shove();
1381        let game = game.apply(Action::Shove(shove));
1382        // run to showdown
1383        let mut game = game;
1384        while !game.must_stop() {
1385            if game.must_deal() {
1386                let cards = game.deck().deal(game.street());
1387                game = game.apply(Action::Draw(cards));
1388            }
1389        }
1390        // total pot is 200 (100 from each), winner gets it all
1391        let rewards: Vec<_> = game
1392            .settlements()
1393            .iter()
1394            .map(|s| s.pnl().reward())
1395            .collect();
1396        assert!(rewards.contains(&0) && rewards.contains(&200));
1397    }
1398
1399    /// actor_idx wraps correctly with ticker
1400    #[test]
1401    fn actor_idx_wrapping() {
1402        let game = Game::root();
1403        assert_eq!(game.actor_idx(), 0); // dealer, ticker=2, (0+2)%2=0
1404        let game = game.apply(Action::Call(1));
1405        assert_eq!(game.actor_idx(), 1); // ticker=3, (0+3)%2=1
1406        let game = game.apply(Action::Check);
1407        // must_deal is true, but if we peek at actor_idx...
1408        assert_eq!((game.dealer + game.ticker) % game.n(), 0); // wraps
1409    }
1410
1411    /// snap preserves legal actions unchanged
1412    /// TODO: expand beyond only testing at the root node. apply some pot actions
1413    #[test]
1414    fn snap_legal_unchanged() {
1415        let game = Game::root();
1416        game.legal()
1417            .iter()
1418            .inspect(|&&action| assert_eq!(game.snap(action), action))
1419            .count();
1420    }
1421
1422    /// snap coerces oversized raise to shove
1423    #[test]
1424    fn snap_raise_to_shove_too_large() {
1425        let game = Game::root();
1426        let shove = game.to_shove();
1427        assert_eq!(game.snap(Action::Raise(Chips::MAX)), game.shove());
1428        assert_eq!(game.snap(Action::Raise(shove)), game.shove());
1429    }
1430
1431    /// snap coerces undersized raise to min-raise
1432    #[test]
1433    fn snap_raise_to_minim_too_small() {
1434        let game = Game::root();
1435        let minraise = game.to_raise();
1436        assert_eq!(game.snap(Action::Raise(1)), Action::Raise(minraise));
1437        assert_eq!(game.snap(Action::Raise(0)), Action::Raise(minraise));
1438    }
1439
1440    /// snap coerces fold to check when not facing bet
1441    #[test]
1442    fn snap_fold_to_check_not_facing_bet() {
1443        let game = Game::root().apply(Action::Call(1));
1444        assert!(!game.may_fold());
1445        assert!(game.may_check());
1446        assert_eq!(game.snap(Action::Fold), Action::Check);
1447    }
1448
1449    /// snap coerces check to call when facing bet
1450    #[test]
1451    fn snap_check_to_call_facing_bet() {
1452        let game = Game::root();
1453        assert!(!game.may_check());
1454        assert!(game.may_call());
1455        assert_eq!(game.snap(Action::Check), game.calls());
1456    }
1457}