Skip to main content

rbp_gameplay/
path.rs

1use crate::*;
2use rbp_core::*;
3
4/// A compact sequence of abstract edges packed into 64 bits.
5///
6/// `Path` encodes up to 16 edges in a single `u64`, using 4 bits per edge.
7/// This enables efficient storage and comparison of action sequences without
8/// heap allocation.
9///
10/// # Encoding
11///
12/// Each edge maps to a 4-bit nibble (values 1–15, with 0 reserved for empty).
13/// Edges are stored least-significant first, so the first action occupies
14/// bits 0–3.
15///
16/// # Use Cases
17///
18/// - Information set keys (abstraction + path = unique info state)
19/// - Strategy table lookups
20/// - Subgame depth tracking (counting trailing raises)
21#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq, Ord, PartialOrd)]
22pub struct Path(u64);
23
24impl Path {
25    const SEPARATOR: &'static str = "/";
26    /// Number of edges in this path.
27    pub fn length(&self) -> usize {
28        (67 - self.0.leading_zeros() as usize) / 4
29    }
30    /// Aggression: count of trailing aggressive edges (for bet sizing grid selection).
31    /// kinda wanna deprecate, dangerous if truncated
32    pub fn aggression(&self) -> usize {
33        self.into_iter()
34            .rev()
35            .take_while(|e| e.is_choice())
36            .filter(|e| e.is_aggro())
37            .count()
38    }
39    /// Street derived from counting Draw edges.
40    /// 0 draws = Pref, 1 = Flop, 2 = Turn, 3+ = River.
41    pub fn street(&self) -> rbp_cards::Street {
42        match self.into_iter().filter(|e| e.is_chance()).count() {
43            0 => rbp_cards::Street::Pref,
44            1 => rbp_cards::Street::Flop,
45            2 => rbp_cards::Street::Turn,
46            _ => rbp_cards::Street::Rive,
47        }
48    }
49}
50
51impl Arbitrary for Path {
52    fn random() -> Self {
53        Self::from(rand::random::<u64>())
54    }
55}
56
57/// Vec<Edge> isomorphism
58/// we (un)pack the byte representation of the edges in a Path(u64) sequence
59impl From<Path> for Vec<Edge> {
60    fn from(path: Path) -> Self {
61        path.into_iter().collect()
62    }
63}
64
65impl From<Vec<Edge>> for Path {
66    fn from(edges: Vec<Edge>) -> Self {
67        edges.into_iter().collect()
68    }
69}
70
71/// u64 isomorphism
72/// trivial unpacking and packing
73impl From<u64> for Path {
74    fn from(value: u64) -> Self {
75        Self(value)
76    }
77}
78impl From<Path> for u64 {
79    fn from(path: Path) -> Self {
80        path.0
81    }
82}
83impl From<Path> for i64 {
84    fn from(path: Path) -> Self {
85        path.0 as i64
86    }
87}
88impl From<i64> for Path {
89    fn from(value: i64) -> Self {
90        Self(value as u64)
91    }
92}
93
94impl TryFrom<&str> for Path {
95    type Error = anyhow::Error;
96    fn try_from(s: &str) -> Result<Self, Self::Error> {
97        s.split(Self::SEPARATOR)
98            .map(Edge::try_from)
99            .collect::<Result<Vec<_>, _>>()
100            .map(Self::from)
101    }
102}
103
104impl std::fmt::Display for Path {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        write!(
107            f,
108            "{}",
109            self.clone()
110                .into_iter()
111                .map(|e| e.to_string())
112                .collect::<Vec<_>>()
113                .join(Self::SEPARATOR)
114        )
115    }
116}
117
118impl Iterator for Path {
119    type Item = Edge;
120    fn next(&mut self) -> Option<Self::Item> {
121        let x = (self.0 & 0xF) as u8;
122        if self.0 == 0 {
123            None
124        } else if x == 0 {
125            None
126        } else {
127            self.0 >>= 4;
128            Some(Edge::from(x))
129        }
130    }
131}
132
133impl DoubleEndedIterator for Path {
134    fn next_back(&mut self) -> Option<Self::Item> {
135        let shift = ((63u32.saturating_sub(self.0.leading_zeros())) / 4) * 4;
136        let bloop = (self.0 >> shift) & 0xF;
137        if self.0 == 0 {
138            None
139        } else if bloop == 0 {
140            None
141        } else {
142            self.0 &= !(0xF << shift);
143            Some(Edge::from(bloop as u8))
144        }
145    }
146}
147
148impl std::iter::FromIterator<Edge> for Path {
149    fn from_iter<T>(iter: T) -> Self
150    where
151        T: IntoIterator<Item = Edge>,
152    {
153        iter.into_iter()
154            .take(rbp_core::MAX_DEPTH_SUBGAME)
155            .map(u8::from)
156            .map(|byte| byte as u64)
157            .enumerate()
158            .map(|(i, byte)| byte << (i * 4))
159            .fold(0u64, |acc, bits| acc | bits)
160            .into()
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    #[test]
168    fn bijective_path_empty() {
169        let edges = vec![];
170        let paths = Vec::<Edge>::from(Path::from(edges.clone()));
171        assert_eq!(edges, paths);
172    }
173
174    #[test]
175    fn bijective_path_edges() {
176        let edges = (0..)
177            .map(|_| Edge::random())
178            .take(rbp_core::MAX_DEPTH_SUBGAME)
179            .collect::<Vec<Edge>>();
180        let paths = Vec::<Edge>::from(Path::from(edges.clone()));
181        assert_eq!(edges, paths);
182    }
183
184    #[test]
185    fn bijective_path_collect() {
186        let edges = (0..).map(|_| Edge::random()).take(5).collect::<Vec<Edge>>();
187        let collected = Path::from(edges.clone()).into_iter().collect::<Vec<Edge>>();
188        assert_eq!(edges, collected);
189    }
190
191    #[test]
192    fn length() {
193        let n = rand::random::<u64>() % (rbp_core::MAX_DEPTH_SUBGAME + 1) as u64;
194        let n = n as usize;
195        let path = (0..).map(|_| Edge::random()).take(n).collect::<Path>();
196        assert_eq!(path.length(), n);
197    }
198
199    #[test]
200    fn double_ended_iterator() {
201        let path = (0..).map(|_| Edge::random()).take(5).collect::<Path>();
202        let forward = path.clone();
203        let reverse = path
204            .into_iter()
205            .rev()
206            .collect::<Vec<Edge>>()
207            .into_iter()
208            .rev()
209            .collect::<Path>();
210        assert_eq!(forward, reverse);
211    }
212
213    #[test]
214    fn subgame_aggression() {
215        let path = [
216            // this one is a late street some aggressions
217            Edge::Draw,
218            Edge::Raise(Odds::new(1, 2)),
219            Edge::Call,
220            Edge::Call,
221            // new street
222            Edge::Draw,
223            Edge::Check,
224            Edge::Check,
225            Edge::Check,
226            // new street
227            Edge::Draw,
228            Edge::Raise(Odds::new(1, 1)),
229            Edge::Shove,
230            Edge::Fold,
231        ]
232        .into_iter()
233        .collect::<Path>();
234        assert_eq!(path.aggression(), 2);
235        let path = [
236            // this one has no aggressions, new street
237            Edge::Draw,
238            Edge::Check,
239            Edge::Check,
240            Edge::Check,
241        ]
242        .into_iter()
243        .collect::<Path>();
244        assert_eq!(path.aggression(), 0);
245    }
246}