Skip to main content

competitive/algorithm/game_analyzer/
partisan.rs

1use super::{
2    ControlFlow, EvaluatedMove, GameAnalysis, GameAnalyzer, GameSum, GameValue, Hash, HashMap,
3    HashSet, Misere, Neighbor, PartisanOutcome, Player, canonical_sum, memoized_game,
4    memoized_wins,
5};
6use std::cmp::Ordering;
7
8pub struct Partisan<S, FL, FR> {
9    left: FL,
10    right: FR,
11    values: HashMap<S, GameValue>,
12    outcomes: [HashMap<(S, Player), bool>; 2],
13    sums: [HashMap<(Vec<S>, Player), bool>; 2],
14    sum_ids: HashMap<S, usize>,
15}
16
17impl GameAnalyzer {
18    #[allow(clippy::type_complexity)] // Each adapter preserves the caller's iterator.
19    pub fn partisan<S, FL, FR, IL, IR>(
20        self,
21        mut left: FL,
22        mut right: FR,
23    ) -> GameAnalyzer<
24        Partisan<
25            S,
26            impl FnMut(&S) -> std::iter::Map<IL::IntoIter, fn(S) -> Neighbor<S, ()>>,
27            impl FnMut(&S) -> std::iter::Map<IR::IntoIter, fn(S) -> Neighbor<S, ()>>,
28        >,
29    >
30    where
31        FL: FnMut(&S) -> IL,
32        FR: FnMut(&S) -> IR,
33        IL: IntoIterator<Item = S>,
34        IR: IntoIterator<Item = S>,
35    {
36        let label: fn(S) -> Neighbor<S, ()> = |to| Neighbor::new(to, ());
37        self.partisan_moves(
38            move |s: &S| left(s).into_iter().map(label),
39            move |s: &S| right(s).into_iter().map(label),
40        )
41    }
42
43    pub fn partisan_moves<S, L, FL, FR, IL, IR>(
44        self,
45        left: FL,
46        right: FR,
47    ) -> GameAnalyzer<Partisan<S, FL, FR>>
48    where
49        FL: FnMut(&S) -> IL,
50        FR: FnMut(&S) -> IR,
51        IL: IntoIterator<Item = Neighbor<S, L>>,
52        IR: IntoIterator<Item = Neighbor<S, L>>,
53    {
54        GameAnalyzer {
55            game: Partisan {
56                left,
57                right,
58                values: HashMap::new(),
59                outcomes: std::array::from_fn(|_| HashMap::new()),
60                sums: std::array::from_fn(|_| HashMap::new()),
61                sum_ids: HashMap::new(),
62            },
63        }
64    }
65}
66
67impl<S, FL, FR> GameAnalyzer<Partisan<S, FL, FR>> {
68    pub fn misere(&mut self) -> Misere<'_, Partisan<S, FL, FR>> {
69        Misere {
70            game: &mut self.game,
71        }
72    }
73
74    pub fn sum(&mut self) -> GameSum<'_, Partisan<S, FL, FR>> {
75        GameSum {
76            game: &mut self.game,
77        }
78    }
79
80    pub fn clear_cache(&mut self) {
81        self.game.values.clear();
82        self.game.outcomes.iter_mut().for_each(HashMap::clear);
83        self.game.sums.iter_mut().for_each(HashMap::clear);
84        self.game.sum_ids.clear();
85    }
86}
87
88impl<S, L, FL, FR, IL, IR> GameAnalyzer<Partisan<S, FL, FR>>
89where
90    S: Clone + Eq + Hash,
91    FL: FnMut(&S) -> IL,
92    FR: FnMut(&S) -> IR,
93    IL: IntoIterator<Item = Neighbor<S, L>>,
94    IR: IntoIterator<Item = Neighbor<S, L>>,
95{
96    pub fn value(&mut self) -> GameAnalysis<'_, Partisan<S, FL, FR>, S, GameValue> {
97        GameAnalysis::new(&mut self.game, Partisan::value)
98    }
99
100    pub fn outcome(&mut self) -> GameAnalysis<'_, Partisan<S, FL, FR>, S, PartisanOutcome> {
101        GameAnalysis::new(&mut self.game, |g, s| g.outcome(s, false))
102    }
103
104    /// `combine` receives all Left options followed by all Right options.
105    #[allow(clippy::type_complexity)] // The query owns the fold and its cache.
106    pub fn fold<V, C>(
107        &mut self,
108        mut combine: C,
109    ) -> GameAnalysis<
110        '_,
111        Partisan<S, FL, FR>,
112        S,
113        V,
114        false,
115        impl FnMut(&mut Partisan<S, FL, FR>, &S) -> V,
116    >
117    where
118        V: Clone,
119        C: FnMut(&S, &[(Player, L, V)]) -> V,
120    {
121        let mut cache = HashMap::new();
122        GameAnalysis::new(
123            &mut self.game,
124            move |game: &mut Partisan<S, FL, FR>, state: &S| {
125                memoized_game(
126                    state,
127                    &mut cache,
128                    &mut HashSet::new(),
129                    &mut |s| {
130                        let moves = (game.left)(s)
131                            .into_iter()
132                            .map(|n| (n.to, (Player::Left, n.label)))
133                            .chain(
134                                (game.right)(s)
135                                    .into_iter()
136                                    .map(|n| (n.to, (Player::Right, n.label))),
137                            );
138                        ControlFlow::Continue((moves, Vec::new()))
139                    },
140                    &mut |_, moves| moves.next(),
141                    &mut |values, (player, label), value| {
142                        values.push((player, label, value));
143                        ControlFlow::Continue(())
144                    },
145                    &mut |s, values| combine(s, &values),
146                )
147            },
148        )
149    }
150}
151impl<'a, S, L, FL, FR, IL, IR> Misere<'a, Partisan<S, FL, FR>>
152where
153    S: Clone + Eq + Hash,
154    FL: FnMut(&S) -> IL,
155    FR: FnMut(&S) -> IR,
156    IL: IntoIterator<Item = Neighbor<S, L>>,
157    IR: IntoIterator<Item = Neighbor<S, L>>,
158{
159    pub fn outcome(self) -> GameAnalysis<'a, Partisan<S, FL, FR>, S, PartisanOutcome, true> {
160        GameAnalysis::new(self.game, |g, s| g.outcome(s, true))
161    }
162}
163impl<'a, S, L, FL, FR, IL, IR> GameSum<'a, Partisan<S, FL, FR>>
164where
165    S: Clone + Eq + Hash,
166    FL: FnMut(&S) -> IL,
167    FR: FnMut(&S) -> IR,
168    IL: IntoIterator<Item = Neighbor<S, L>>,
169    IR: IntoIterator<Item = Neighbor<S, L>>,
170{
171    pub fn value(self) -> GameAnalysis<'a, Partisan<S, FL, FR>, [S], GameValue> {
172        GameAnalysis::new(self.game, |g, state: &[S]| {
173            state.iter().map(|s| g.value(s)).sum()
174        })
175    }
176}
177impl<'a, S, L, FL, FR, IL, IR, const MISERE: bool> GameSum<'a, Partisan<S, FL, FR>, MISERE>
178where
179    S: Clone + Eq + Hash,
180    FL: FnMut(&S) -> IL,
181    FR: FnMut(&S) -> IR,
182    IL: IntoIterator<Item = Neighbor<S, L>>,
183    IR: IntoIterator<Item = Neighbor<S, L>>,
184{
185    /// Explores reachable sum positions, including the player to move.
186    /// Reuses cached sums of numbers, ups, nimbers and switches in normal play.
187    pub fn outcome(self) -> GameAnalysis<'a, Partisan<S, FL, FR>, [S], PartisanOutcome, MISERE> {
188        GameAnalysis::new(self.game, |g, s| {
189            match (
190                g.sum_wins(s, Player::Left, MISERE),
191                g.sum_wins(s, Player::Right, MISERE),
192            ) {
193                (true, true) => PartisanOutcome::Next,
194                (false, false) => PartisanOutcome::Previous,
195                (true, false) => PartisanOutcome::Left,
196                (false, true) => PartisanOutcome::Right,
197            }
198        })
199    }
200}
201
202impl<S, L, FL, FR, IL, IR, V, const MISERE: bool, E>
203    GameAnalysis<'_, Partisan<S, FL, FR>, S, V, MISERE, E>
204where
205    FL: FnMut(&S) -> IL,
206    FR: FnMut(&S) -> IR,
207    IL: IntoIterator<Item = Neighbor<S, L>>,
208    IR: IntoIterator<Item = Neighbor<S, L>>,
209    E: FnMut(&mut Partisan<S, FL, FR>, &S) -> V,
210{
211    /// Values describe the destination game for both choices of starting player.
212    pub fn moves(&mut self, state: &S, player: Player) -> Vec<EvaluatedMove<S, L, V>> {
213        partisan_moves(&mut self.game.left, &mut self.game.right, state, player)
214            .map(|next| {
215                let value = self.eval(&next.to);
216                EvaluatedMove { next, value }
217            })
218            .collect()
219    }
220}
221
222impl<S, L, FL, FR, IL, IR> GameAnalysis<'_, Partisan<S, FL, FR>, S, GameValue>
223where
224    FL: FnMut(&S) -> IL,
225    FR: FnMut(&S) -> IR,
226    IL: IntoIterator<Item = Neighbor<S, L>>,
227    IR: IntoIterator<Item = Neighbor<S, L>>,
228{
229    pub fn compare(&mut self, left: &S, right: &S) -> Option<Ordering> {
230        self.eval(left).partial_cmp(&self.eval(right))
231    }
232
233    /// Undominated moves for the selected player; incomparable alternatives remain.
234    pub fn undominated_moves(&mut self, state: &S, player: Player) -> Vec<Neighbor<S, L>> {
235        let moves = self.moves(state, player);
236        let preferred = if player == Player::Left {
237            Ordering::Greater
238        } else {
239            Ordering::Less
240        };
241        let best = moves.iter().try_fold(None, |best, m| {
242            let (q, k, n) = m.value.as_number_up_nimber()?;
243            let pair = (q, k);
244            Some(Some(match best {
245                Some((previous, nimber)) => match pair.cmp(&previous) {
246                    Ordering::Equal => (previous, if nimber == Some(n) { nimber } else { None }),
247                    order if order == preferred => (pair, Some(n)),
248                    _ => (previous, nimber),
249                },
250                None => (pair, Some(n)),
251            }))
252        });
253        if let Some(best) = best {
254            return moves
255                .into_iter()
256                .filter(|m| {
257                    let (q, k, n) = m.value.as_number_up_nimber().unwrap();
258                    best.is_some_and(|((b, l), nimber)| {
259                        q == b && (k == l || k.abs_diff(l) == 1 && nimber == Some(n ^ 1))
260                    })
261                })
262                .map(|m| m.next)
263                .collect();
264        }
265        let mut dominated = vec![false; moves.len()];
266        for i in 0..moves.len() {
267            if dominated[i] {
268                continue;
269            }
270            for j in i + 1..moves.len() {
271                if dominated[j] {
272                    continue;
273                }
274                let order = moves[i].value.partial_cmp(&moves[j].value);
275                if order == Some(preferred) {
276                    dominated[j] = true;
277                } else if order == Some(preferred.reverse()) {
278                    dominated[i] = true;
279                    break;
280                }
281            }
282        }
283        moves
284            .into_iter()
285            .zip(dominated)
286            .filter(|(_, d)| !d)
287            .map(|(m, _)| m.next)
288            .collect()
289    }
290}
291impl<S, L, FL, FR, IL, IR, const MISERE: bool>
292    GameAnalysis<'_, Partisan<S, FL, FR>, S, PartisanOutcome, MISERE>
293where
294    S: Clone + Eq + Hash,
295    FL: FnMut(&S) -> IL,
296    FR: FnMut(&S) -> IR,
297    IL: IntoIterator<Item = Neighbor<S, L>>,
298    IR: IntoIterator<Item = Neighbor<S, L>>,
299{
300    /// Whether the selected starting player can force a win.
301    pub fn wins(&mut self, state: &S, player: Player) -> bool {
302        self.game.wins(state, player, MISERE)
303    }
304
305    pub fn winning_moves(&mut self, state: &S, player: Player) -> Vec<Neighbor<S, L>> {
306        partisan_moves(&mut self.game.left, &mut self.game.right, state, player)
307            .filter(|next| !self.game.wins(&next.to, player.opponent(), MISERE))
308            .collect()
309    }
310}
311
312#[allow(clippy::type_complexity)] // Sum states and labels both carry component information.
313impl<S, L, FL, FR, IL, IR, V, const MISERE: bool, E>
314    GameAnalysis<'_, Partisan<S, FL, FR>, [S], V, MISERE, E>
315where
316    S: Clone,
317    FL: FnMut(&S) -> IL,
318    FR: FnMut(&S) -> IR,
319    IL: IntoIterator<Item = Neighbor<S, L>>,
320    IR: IntoIterator<Item = Neighbor<S, L>>,
321    E: FnMut(&mut Partisan<S, FL, FR>, &[S]) -> V,
322{
323    pub fn moves(
324        &mut self,
325        state: &[S],
326        player: Player,
327    ) -> Vec<EvaluatedMove<Vec<S>, (usize, L), V>> {
328        let mut result = Vec::new();
329        for (i, component) in state.iter().enumerate() {
330            for next in partisan_moves(&mut self.game.left, &mut self.game.right, component, player)
331            {
332                let mut to = state.to_vec();
333                to[i] = next.to;
334                let value = self.eval(&to);
335                result.push(EvaluatedMove {
336                    next: Neighbor::new(to, (i, next.label)),
337                    value,
338                });
339            }
340        }
341        result
342    }
343}
344#[allow(clippy::type_complexity)] // Sum states and labels both carry component information.
345impl<S, L, FL, FR, IL, IR, const MISERE: bool>
346    GameAnalysis<'_, Partisan<S, FL, FR>, [S], PartisanOutcome, MISERE>
347where
348    S: Clone + Eq + Hash,
349    FL: FnMut(&S) -> IL,
350    FR: FnMut(&S) -> IR,
351    IL: IntoIterator<Item = Neighbor<S, L>>,
352    IR: IntoIterator<Item = Neighbor<S, L>>,
353{
354    /// Whether the selected starting player can force a win.
355    pub fn wins(&mut self, state: &[S], player: Player) -> bool {
356        self.game.sum_wins(state, player, MISERE)
357    }
358
359    pub fn winning_moves(
360        &mut self,
361        state: &[S],
362        player: Player,
363    ) -> Vec<Neighbor<Vec<S>, (usize, L)>> {
364        let mut result = Vec::new();
365        for (i, component) in state.iter().enumerate() {
366            for next in partisan_moves(&mut self.game.left, &mut self.game.right, component, player)
367            {
368                let mut to = state.to_vec();
369                to[i] = next.to;
370                if !self.game.sum_wins(&to, player.opponent(), MISERE) {
371                    result.push(Neighbor::new(to, (i, next.label)));
372                }
373            }
374        }
375        result
376    }
377}
378
379impl<S, L, FL, FR, IL, IR> Partisan<S, FL, FR>
380where
381    S: Clone + Eq + Hash,
382    FL: FnMut(&S) -> IL,
383    FR: FnMut(&S) -> IR,
384    IL: IntoIterator<Item = Neighbor<S, L>>,
385    IR: IntoIterator<Item = Neighbor<S, L>>,
386{
387    fn value(&mut self, state: &S) -> GameValue {
388        memoized_game(
389            state,
390            &mut self.values,
391            &mut HashSet::new(),
392            &mut |s| {
393                let moves = (self.left)(s)
394                    .into_iter()
395                    .map(|n| (n.to, Player::Left))
396                    .chain((self.right)(s).into_iter().map(|n| (n.to, Player::Right)));
397                ControlFlow::Continue((moves, (Vec::new(), Vec::new())))
398            },
399            &mut |_, moves| moves.next(),
400            &mut |(left, right), player, value| {
401                if player == Player::Left {
402                    left.push(value)
403                } else {
404                    right.push(value)
405                }
406                ControlFlow::Continue(())
407            },
408            &mut |_, (left, right)| GameValue::from_options(left, right),
409        )
410    }
411
412    fn outcome(&mut self, state: &S, misere: bool) -> PartisanOutcome {
413        match (
414            self.wins(state, Player::Left, misere),
415            self.wins(state, Player::Right, misere),
416        ) {
417            (true, true) => PartisanOutcome::Next,
418            (false, false) => PartisanOutcome::Previous,
419            (true, false) => PartisanOutcome::Left,
420            (false, true) => PartisanOutcome::Right,
421        }
422    }
423
424    fn wins(&mut self, state: &S, player: Player, misere: bool) -> bool {
425        let key = (state.clone(), player);
426        memoized_wins(
427            &key,
428            &mut self.outcomes[usize::from(misere)],
429            |(s, p)| {
430                if !misere && let Some(value) = self.values.get(s) {
431                    return ControlFlow::Break(value.wins(*p));
432                }
433                let next_player = p.opponent();
434                let moves = partisan_moves(&mut self.left, &mut self.right, s, *p)
435                    .map(move |n| (n.to, next_player));
436                ControlFlow::Continue(moves)
437            },
438            |_, moves| moves.next(),
439            misere,
440        )
441    }
442
443    fn sum_wins(&mut self, state: &[S], player: Player, misere: bool) -> bool {
444        match state {
445            [] => return misere,
446            [single] => return self.wins(single, player, misere),
447            _ => {}
448        }
449        let mut normalize = |mut state: Vec<S>| {
450            if !misere {
451                state.retain(|s| {
452                    if let Some(value) = self.values.get(s) {
453                        return value.as_nimber() != Some(0);
454                    }
455                    let mut key = (s.clone(), Player::Left);
456                    if self.outcomes[0].get(&key) != Some(&false) {
457                        return true;
458                    }
459                    key.1 = Player::Right;
460                    self.outcomes[0].get(&key) != Some(&false)
461                });
462            }
463            canonical_sum(state, &mut self.sum_ids)
464        };
465        let state = normalize(state.to_vec());
466        match state.as_slice() {
467            [] => return misere,
468            [single] => return self.wins(single, player, misere),
469            _ => {}
470        }
471        let key = (state, player);
472        memoized_wins(
473            &key,
474            &mut self.sums[usize::from(misere)],
475            |(s, p)| {
476                if !misere
477                    && s.iter().all(|s| {
478                        self.values
479                            .get(s)
480                            .is_some_and(|g| g.as_switch_sum().is_some())
481                    })
482                {
483                    let value: GameValue = s.iter().map(|s| self.values[s].clone()).sum();
484                    return ControlFlow::Break(value.wins(*p));
485                }
486                // Losing as starter in every component implies losing in the sum.
487                // One known win is enough if the opponent loses in every other component.
488                if !misere && (!self.values.is_empty() || !self.outcomes[0].is_empty()) {
489                    let mut all_lose = true;
490                    let mut all_opponent_lose = true;
491                    let mut can_win = false;
492                    for s in s {
493                        let (mut win, mut opponent_win) = (None, None);
494                        if !self.outcomes[0].is_empty() {
495                            let mut key = (s.clone(), *p);
496                            win = self.outcomes[0].get(&key).copied();
497                            key.1 = p.opponent();
498                            opponent_win = self.outcomes[0].get(&key).copied();
499                        }
500                        if let Some(value) = self.values.get(s) {
501                            win.get_or_insert_with(|| value.wins(*p));
502                            opponent_win.get_or_insert_with(|| value.wins(p.opponent()));
503                        }
504                        can_win = can_win && opponent_win == Some(false)
505                            || all_opponent_lose && win == Some(true);
506                        all_lose &= win == Some(false);
507                        all_opponent_lose &= opponent_win == Some(false);
508                        if !all_lose && !all_opponent_lose && !can_win {
509                            break;
510                        }
511                    }
512                    if all_lose || can_win {
513                        return ControlFlow::Break(can_win);
514                    }
515                }
516                ControlFlow::Continue((0, None))
517            },
518            |(s, p), (i, moves)| {
519                while *i < s.len() {
520                    if *i != 0 && s[*i - 1] == s[*i] {
521                        *i += 1;
522                        continue;
523                    }
524                    let iterator = moves.get_or_insert_with(|| {
525                        partisan_moves(&mut self.left, &mut self.right, &s[*i], *p)
526                    });
527                    if let Some(next) = iterator.next() {
528                        let mut to = s.clone();
529                        to[*i] = next.to;
530                        return Some((normalize(to), p.opponent()));
531                    }
532                    *i += 1;
533                    *moves = None;
534                }
535                None
536            },
537            misere,
538        )
539    }
540}
541
542fn partisan_moves<S, L, FL, FR, IL, IR>(
543    left: &mut FL,
544    right: &mut FR,
545    state: &S,
546    player: Player,
547) -> impl Iterator<Item = Neighbor<S, L>> + use<S, L, FL, FR, IL, IR>
548where
549    FL: FnMut(&S) -> IL,
550    FR: FnMut(&S) -> IR,
551    IL: IntoIterator<Item = Neighbor<S, L>>,
552    IR: IntoIterator<Item = Neighbor<S, L>>,
553{
554    let (left, right) = match player {
555        Player::Left => (Some(left(state).into_iter()), None),
556        Player::Right => (None, Some(right(state).into_iter())),
557    };
558    left.into_iter()
559        .flatten()
560        .chain(right.into_iter().flatten())
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use crate::tools::{
567        Xorshift,
568        testutil::{exhaustive_sequences, sequence_cases},
569    };
570    use std::cell::Cell;
571
572    #[test]
573    fn cached_children() {
574        for n in [0usize, 1, 32, 33] {
575            let calls = Cell::new(0);
576            let mut game = GameAnalyzer::new().partisan(
577                |&s: &usize| {
578                    calls.set(calls.get() + 1);
579                    s.checked_sub(1)
580                },
581                |&s: &usize| {
582                    calls.set(calls.get() + 1);
583                    s.checked_sub(1)
584                },
585            );
586            game.value().eval(&n);
587            for player in [Player::Left, Player::Right] {
588                calls.set(0);
589                assert_eq!(game.outcome().wins(&(n + 1), player), n % 2 == 0);
590                assert_eq!(calls.get(), 1);
591                calls.set(0);
592                assert_eq!(game.sum().outcome().wins(&[n + 1, 0], player), n % 2 == 0);
593                // A losing root may also enumerate the terminal component.
594                assert!(calls.get() <= 2);
595            }
596            calls.set(0);
597            assert_eq!(
598                game.misere().outcome().wins(&(n + 1), Player::Left),
599                n % 2 != 0
600            );
601            assert_eq!(calls.get(), n + 2);
602        }
603    }
604
605    #[test]
606    fn values_and_fold() {
607        type Trace = (usize, Vec<(Player, usize)>);
608
609        let mut basic = basic_games();
610        let up_star = basic.len();
611        basic.extend([
612            [vec![0, 3], vec![0]],
613            [vec![0], vec![0, 3]],
614            [vec![0], vec![up_star]],
615            [vec![up_star + 1], vec![0]],
616        ]);
617        let mut rng = Xorshift::default();
618        let nus = [0, 3, 4, 5, 6].into_iter().chain(up_star..basic.len());
619        for moves in sequence_cases(&mut rng, 0..=basic.len() - 1, 0..=16)
620            .chain(exhaustive_sequences(nus, 3..=3))
621        {
622            let mut options = basic.clone();
623            options.push([moves.clone(), moves]);
624            let n = options.len();
625            // Direct play on G-H establishes order independently of GameValue.
626            let mut differences = vec![vec![[false; 2]; n]; n];
627            for a in 0..n {
628                for b in 0..n {
629                    for p in 0..2 {
630                        differences[a][b][p] =
631                            options[a][p].iter().any(|&to| !differences[to][b][p ^ 1])
632                                || options[b][p ^ 1]
633                                    .iter()
634                                    .any(|&to| !differences[a][to][p ^ 1]);
635                    }
636                }
637            }
638            let labeled_moves = |s: usize, p: usize| {
639                options[s][p]
640                    .iter()
641                    .enumerate()
642                    .map(|(label, &to)| Neighbor::new(to, label))
643            };
644            let mut game = GameAnalyzer::new()
645                .partisan_moves(|&s| labeled_moves(s, 0), |&s| labeled_moves(s, 1));
646            let values: Vec<_> = game
647                .value()
648                .table(0..n)
649                .into_iter()
650                .map(|(_, v)| v)
651                .collect();
652            let notation = ["0", "1", "-1", "*", "*2", "↑", "↓", "{1|-1}", "1/2", "0"];
653            assert_eq!(
654                values[..notation.len()]
655                    .iter()
656                    .map(ToString::to_string)
657                    .collect::<Vec<_>>(),
658                notation
659            );
660            game.clear_cache();
661            assert_eq!(game.value().verify(0..n, |&s| values[s].clone()), None);
662            for (a, row) in differences.iter().enumerate() {
663                for (b, &wins) in row.iter().enumerate() {
664                    let expected = match wins {
665                        [true, true] => None,
666                        [false, false] => Some(Ordering::Equal),
667                        [true, false] => Some(Ordering::Greater),
668                        [false, true] => Some(Ordering::Less),
669                    };
670                    assert_eq!(game.value().compare(&a, &b), expected);
671                }
672                for (p, player) in [Player::Left, Player::Right].into_iter().enumerate() {
673                    let expected: Vec<_> = labeled_moves(a, p)
674                        .map(|next| EvaluatedMove {
675                            value: values[next.to].clone(),
676                            next,
677                        })
678                        .collect();
679                    assert_eq!(game.value().moves(&a, player), expected);
680                    let undominated: Vec<_> = expected
681                        .into_iter()
682                        .filter(|m| {
683                            !options[a][p].iter().any(|&b| {
684                                differences[b][m.next.to][p] && !differences[b][m.next.to][p ^ 1]
685                            })
686                        })
687                        .map(|m| m.next)
688                        .collect();
689                    assert_eq!(game.value().undominated_moves(&a, player), undominated);
690                }
691            }
692            assert_eq!(game.sum().value().eval(&[5, 6]), GameValue::from(0));
693            assert_eq!(
694                game.sum().value().moves(&[1, 2], Player::Left),
695                vec![EvaluatedMove {
696                    next: Neighbor::new(vec![0, 2], (0, 0)),
697                    value: GameValue::from(-1),
698                }]
699            );
700            let mut fold = game.fold(|_: &usize, children: &[(Player, usize, Trace)]| {
701                let height = children
702                    .iter()
703                    .map(|(_, _, (h, _))| h + 1)
704                    .max()
705                    .unwrap_or(0);
706                let order: Vec<_> = children
707                    .iter()
708                    .flat_map(|(player, label, (_, path))| {
709                        std::iter::once((*player, *label)).chain(path.iter().copied())
710                    })
711                    .collect();
712                (height, order)
713            });
714            // Direct traversal checks height, side order and move labels together.
715            let expected: Vec<Trace> = (0..n)
716                .map(|s| {
717                    let mut height = 0;
718                    let mut order = Vec::new();
719                    let mut stack = vec![(s, 0, None)];
720                    while let Some((s, depth, edge)) = stack.pop() {
721                        height = height.max(depth);
722                        order.extend(edge);
723                        for (p, player) in
724                            [Player::Left, Player::Right].into_iter().enumerate().rev()
725                        {
726                            for next in labeled_moves(s, p).rev() {
727                                stack.push((next.to, depth + 1, Some((player, next.label))));
728                            }
729                        }
730                    }
731                    (height, order)
732                })
733                .collect();
734            assert_eq!(fold.verify(0..n, |&s| expected[s].clone()), None);
735            for s in 0..n {
736                for (p, player) in [Player::Left, Player::Right].into_iter().enumerate() {
737                    let moves: Vec<_> = labeled_moves(s, p)
738                        .map(|next| EvaluatedMove {
739                            value: expected[next.to].clone(),
740                            next,
741                        })
742                        .collect();
743                    assert_eq!(fold.moves(&s, player), moves);
744                }
745            }
746        }
747    }
748
749    #[test]
750    fn sums_against_play() {
751        // Basic values, every subtraction limit through three, and DAGs of each density.
752        let mut games = vec![(basic_games(), 3)];
753        for left in 0..=3usize {
754            for right in 0..=3usize {
755                games.push((
756                    (0..5usize)
757                        .map(|s| {
758                            [
759                                (s.saturating_sub(left)..s).collect(),
760                                (s.saturating_sub(right)..s).collect(),
761                            ]
762                        })
763                        .collect(),
764                    4,
765                ));
766            }
767        }
768        let mut rng = Xorshift::default();
769        for n in 0..=8 {
770            for degree in 0..=n {
771                let options = (0..n)
772                    .map(|s| {
773                        std::array::from_fn(|_| {
774                            (0..if s == 0 { 0 } else { degree })
775                                .map(|_| rng.rand(s as u64) as usize)
776                                .collect()
777                        })
778                    })
779                    .collect();
780                games.push((options, 3));
781            }
782        }
783        let classify = |wins: [bool; 2]| match wins {
784            [true, true] => PartisanOutcome::Next,
785            [false, false] => PartisanOutcome::Previous,
786            [true, false] => PartisanOutcome::Left,
787            [false, true] => PartisanOutcome::Right,
788        };
789        for (options, max_len) in games {
790            let n = options.len();
791            let mut local = vec![[false; 2]; n];
792            for s in 0..n {
793                for p in 0..2 {
794                    local[s][p] = options[s][p].iter().any(|&to| !local[to][p ^ 1]);
795                }
796            }
797            for len in 0..=max_len {
798                let states: Vec<_> = exhaustive_sequences(0..n, len..=len).collect();
799                let tables = [false, true].map(|misere| {
800                    // Ordered product DP without value arithmetic, sorting or cache shortcuts.
801                    let mut wins = vec![[misere; 2]; states.len()];
802                    for (id, state) in states.iter().enumerate() {
803                        for p in 0..2 {
804                            let mut children = Vec::new();
805                            let mut place = 1;
806                            for &s in state {
807                                children
808                                    .extend(options[s][p].iter().map(|to| id - (s - to) * place));
809                                place *= n;
810                            }
811                            if !children.is_empty() {
812                                wins[id][p] = children.iter().any(|&to| !wins[to][p ^ 1]);
813                            }
814                        }
815                    }
816                    wins
817                });
818                let variants = (0..7)
819                    .map(|cache| (cache, true))
820                    .chain([(0, false), (2, false)]);
821                for (cache, tagged) in variants {
822                    let calls = Cell::new(0);
823                    let mut game = GameAnalyzer::new().partisan_moves(
824                        |&(i, s): &(usize, usize)| {
825                            calls.set(calls.get() + 1);
826                            options[s][0]
827                                .iter()
828                                .enumerate()
829                                .map(move |(label, &to)| Neighbor::new((i, to), label))
830                        },
831                        |&(i, s): &(usize, usize)| {
832                            calls.set(calls.get() + 1);
833                            options[s][1]
834                                .iter()
835                                .enumerate()
836                                .map(move |(label, &to)| Neighbor::new((i, to), label))
837                        },
838                    );
839                    let mut compact = vec![false; n];
840                    for i in 0..len {
841                        for (s, compact) in compact.iter_mut().enumerate() {
842                            match cache {
843                                1 | 6 if cache == 1 || s % 2 == 0 => {
844                                    *compact = game.value().eval(&(i, s)).as_switch_sum().is_some();
845                                }
846                                2 => {
847                                    game.outcome().eval(&(i, s));
848                                }
849                                3 => {
850                                    game.outcome().wins(&(i, s), Player::Left);
851                                }
852                                4 => {
853                                    game.outcome().wins(&(i, s), Player::Right);
854                                    if i == 0 {
855                                        game.outcome().wins(&(i, s), Player::Left);
856                                    }
857                                }
858                                5 if s % 2 == 0 => {
859                                    game.outcome().wins(&(i, s), Player::Left);
860                                }
861                                _ => {}
862                            }
863                        }
864                    }
865                    for (misere, wins) in [false, true].into_iter().zip(&tables) {
866                        for state in states.iter().rev() {
867                            let all_compact =
868                                matches!(cache, 1 | 6) && state.iter().all(|&s| compact[s]);
869                            let mut components: Vec<_> = state
870                                .iter()
871                                .copied()
872                                .enumerate()
873                                .map(|(i, s)| (if tagged { i } else { 0 }, s))
874                                .collect();
875                            for reversed in [false, true] {
876                                if reversed {
877                                    components.reverse();
878                                }
879                                let id = components.iter().rev().fold(0, |id, &(_, s)| id * n + s);
880                                for (p, player) in
881                                    [Player::Left, Player::Right].into_iter().enumerate()
882                                {
883                                    calls.set(0);
884                                    let actual = if misere {
885                                        game.sum().misere().outcome().wins(&components, player)
886                                    } else {
887                                        game.sum().outcome().wins(&components, player)
888                                    };
889                                    assert_eq!(
890                                        actual, wins[id][p],
891                                        "{options:?}, {state:?}, {player:?}, {misere}"
892                                    );
893                                    if !misere {
894                                        let all_lose = state.iter().all(|&s| !local[s][p]);
895                                        let all_opponent_lose =
896                                            state.iter().all(|&s| !local[s][p ^ 1]);
897                                        let known = match cache {
898                                            1 | 6 if all_compact => true,
899                                            1 | 2 => {
900                                                all_lose
901                                                    || state.iter().enumerate().any(|(i, &s)| {
902                                                        local[s][p]
903                                                            && state.iter().enumerate().all(
904                                                                |(j, &t)| {
905                                                                    i == j || !local[t][p ^ 1]
906                                                                },
907                                                            )
908                                                    })
909                                                    || state
910                                                        .iter()
911                                                        .filter(|&&s| local[s] != [false; 2])
912                                                        .count()
913                                                        <= 1
914                                            }
915                                            3 => p == 0 && all_lose,
916                                            4 => {
917                                                p == 1 && all_lose
918                                                    || p == 0
919                                                        && all_opponent_lose
920                                                        && state
921                                                            .first()
922                                                            .is_some_and(|&s| local[s][0])
923                                            }
924                                            _ => false,
925                                        };
926                                        if known {
927                                            assert_eq!(
928                                                calls.get(),
929                                                0,
930                                                "cache={cache}, {state:?}, {player:?}"
931                                            );
932                                        }
933                                    }
934                                    let mut expected = Vec::new();
935                                    let mut place = 1;
936                                    for (i, &(_, s)) in components.iter().enumerate() {
937                                        for (label, &to) in options[s][p].iter().enumerate() {
938                                            let mut next = components.clone();
939                                            next[i].1 = to;
940                                            expected.push((
941                                                Neighbor::new(next, (i, label)),
942                                                wins[id - (s - to) * place],
943                                            ));
944                                        }
945                                        place *= n;
946                                    }
947                                    let moves = if misere {
948                                        game.sum()
949                                            .misere()
950                                            .outcome()
951                                            .winning_moves(&components, player)
952                                    } else {
953                                        game.sum().outcome().winning_moves(&components, player)
954                                    };
955                                    let winning = expected
956                                        .iter()
957                                        .filter(|(_, win)| !win[p ^ 1])
958                                        .map(|(next, _)| next);
959                                    assert!(
960                                        moves.iter().eq(winning.clone()),
961                                        "actual={moves:?}, expected={expected:?}, player={player:?}"
962                                    );
963                                    // Full component caches keep these extra queries out of partial-cache checks.
964                                    if cache == 2 {
965                                        let moves = if misere {
966                                            game.sum().misere().outcome().moves(&components, player)
967                                        } else {
968                                            game.sum().outcome().moves(&components, player)
969                                        };
970                                        let evaluated: Vec<_> = expected
971                                            .iter()
972                                            .map(|(next, win)| EvaluatedMove {
973                                                next: next.clone(),
974                                                value: classify(*win),
975                                            })
976                                            .collect();
977                                        assert_eq!(moves, evaluated);
978                                    }
979                                    if let [single] = components.as_slice() {
980                                        let (win, moves) = if misere {
981                                            let mut analysis = game.misere().outcome();
982                                            (
983                                                analysis.wins(single, player),
984                                                analysis.winning_moves(single, player),
985                                            )
986                                        } else {
987                                            let mut analysis = game.outcome();
988                                            (
989                                                analysis.wins(single, player),
990                                                analysis.winning_moves(single, player),
991                                            )
992                                        };
993                                        assert_eq!(win, wins[id][p]);
994                                        let expected: Vec<_> = winning
995                                            .map(|next| Neighbor::new(next.to[0], next.label.1))
996                                            .collect();
997                                        assert_eq!(moves, expected);
998                                    }
999                                }
1000                                let expected = classify(wins[id]);
1001                                let actual = if misere {
1002                                    game.misere().sum().outcome().eval(&components)
1003                                } else {
1004                                    game.sum().outcome().eval(&components)
1005                                };
1006                                assert_eq!(actual, expected);
1007                                if all_compact && !misere {
1008                                    assert_eq!(
1009                                        game.sum().value().eval(&components).outcome(),
1010                                        expected
1011                                    );
1012                                }
1013                                if let [single] = components.as_slice() {
1014                                    let actual = if misere {
1015                                        game.misere().outcome().eval(single)
1016                                    } else {
1017                                        game.outcome().eval(single)
1018                                    };
1019                                    assert_eq!(actual, expected);
1020                                }
1021                            }
1022                        }
1023                    }
1024                }
1025            }
1026        }
1027    }
1028
1029    #[test]
1030    fn integers() {
1031        let mut integers = GameAnalyzer::new().partisan(
1032            |&n: &i32| if n > 0 { Some(n - 1) } else { None },
1033            |&n: &i32| if n < 0 { Some(n + 1) } else { None },
1034        );
1035        for n in (0..=8192).flat_map(|n| [n, -n]) {
1036            assert_eq!(
1037                integers.value().eval(&n).as_number(),
1038                Some(i64::from(n).into())
1039            );
1040        }
1041    }
1042
1043    fn basic_games() -> Vec<[Vec<usize>; 2]> {
1044        // Numbers, nimbers, infinitesimals, a switch and a nonterminal zero.
1045        vec![
1046            [vec![], vec![]],
1047            [vec![0], vec![]],
1048            [vec![], vec![0]],
1049            [vec![0], vec![0]],
1050            [vec![0, 3], vec![0, 3]],
1051            [vec![0], vec![3]],
1052            [vec![3], vec![0]],
1053            [vec![1], vec![2]],
1054            [vec![0], vec![1]],
1055            [vec![3], vec![3]],
1056        ]
1057    }
1058}