rbp_gameplay/perfect.rs
1//! Complete-information game history for **training time**.
2//!
3//! # Information Boundary
4//!
5//! | Type | Perspective | Context |
6//! |------|-------------|---------|
7//! | `Partial` | Hero only | Inference (strategy lookup) |
8//! | `Perfect` | Both hands | Training (CFR traversal) |
9//!
10//! During CFR training, we traverse the game tree knowing both players' cards
11//! (god's view), but strategies are indexed only by `NlheInfo` (public edges +
12//! private bucket). `Perfect` stores the complete root state needed for reach
13//! probability computation and counterfactual value calculation.
14//!
15//! # Conversions
16//!
17//! ```text
18//! Perfect::from((partial, hole)) ────► Perfect (add opponent info)
19//! ◄────
20//! perfect.partial(hero) (erase opponent info)
21//!
22//! partial.histories() ─────► Vec<(Obs, Perfect)> (iterate all opponents)
23//! ```
24//!
25//! # Blind Handling
26//!
27//! Like `Partial`, blinds are constant and NOT stored in `actions`.
28//! The `root` field stores a POST-blind game state.
29use super::*;
30use rbp_cards::*;
31
32/// Complete game history with both players' cards known.
33///
34/// Stores root game state (POST-blind, with all cards set) and action sequence
35/// (excluding blinds). Game states are derived by applying actions to root.
36#[derive(Debug, Clone)]
37pub struct Perfect {
38 root: Game,
39 actions: Vec<Action>,
40}
41
42impl From<(&Partial, Hole)> for Perfect {
43 /// Creates history from partial with assumed opponent hole.
44 ///
45 /// Hero is derived from `partial.turn()`. The root game has:
46 /// - Hero's cards from `partial.seen()`
47 /// - Opponent's cards from `hole` parameter
48 /// - Blinds already posted (POST-blind state)
49 fn from((partial, hole): (&Partial, Hole)) -> Self {
50 debug_assert!(partial.base().n() == 2);
51 let preblind = partial.base().assume(partial.turn(), hole);
52 let root = Game::blinds()
53 .into_iter()
54 .fold(preblind, |mut g, a| g.consume(a));
55 Self {
56 root,
57 actions: partial.actions().to_vec(),
58 }
59 }
60}
61
62impl Recall for Perfect {
63 fn root(&self) -> Game {
64 self.root
65 }
66 fn actions(&self) -> &[Action] {
67 &self.actions
68 }
69}
70
71#[allow(dead_code)]
72impl Perfect {
73 /// Erases opponent information, returning hero's perspective.
74 ///
75 /// Extracts hero's hole cards and board from root, discarding
76 /// opponent's cards. Inverse of construction from `(&Partial, Hole)`.
77 fn erase(&self, hero: Turn) -> Partial {
78 let hole = self.root.seats()[hero.position()].cards();
79 let board = Hand::from(self.root.board());
80 let observation = Observation::from((Hand::from(hole), board));
81 let actions = self
82 .actions
83 .iter()
84 .filter(|a| a.is_choice())
85 .cloned()
86 .collect();
87 Partial::from((hero, observation, actions))
88 }
89}