Skip to main content

competitive/algorithm/game_analyzer/
impartial.rs

1use super::{
2    ControlFlow, EvaluatedMove, GameAnalysis, GameAnalyzer, GameOutcome, GameSum, GameValue, Hash,
3    HashMap, HashSet, Misere, Neighbor, canonical_sum, game_mex, memoized_game, memoized_wins,
4};
5
6pub struct Impartial<S, F> {
7    moves: F,
8    grundy: HashMap<S, u64>,
9    outcomes: [HashMap<S, bool>; 2],
10    sums: HashMap<Vec<S>, bool>,
11    sum_ids: HashMap<S, usize>,
12}
13
14impl GameAnalyzer {
15    #[allow(clippy::type_complexity)] // The adapter preserves the caller's iterator.
16    pub fn impartial<S, F, I>(
17        self,
18        mut moves: F,
19    ) -> GameAnalyzer<
20        Impartial<S, impl FnMut(&S) -> std::iter::Map<I::IntoIter, fn(S) -> Neighbor<S, ()>>>,
21    >
22    where
23        F: FnMut(&S) -> I,
24        I: IntoIterator<Item = S>,
25    {
26        let label: fn(S) -> Neighbor<S, ()> = |to| Neighbor::new(to, ());
27        self.impartial_moves(move |s: &S| moves(s).into_iter().map(label))
28    }
29
30    pub fn impartial_moves<S, L, F, I>(self, moves: F) -> GameAnalyzer<Impartial<S, F>>
31    where
32        F: FnMut(&S) -> I,
33        I: IntoIterator<Item = Neighbor<S, L>>,
34    {
35        GameAnalyzer {
36            game: Impartial {
37                moves,
38                grundy: HashMap::new(),
39                outcomes: std::array::from_fn(|_| HashMap::new()),
40                sums: HashMap::new(),
41                sum_ids: HashMap::new(),
42            },
43        }
44    }
45}
46
47impl<S, F> GameAnalyzer<Impartial<S, F>> {
48    pub fn misere(&mut self) -> Misere<'_, Impartial<S, F>> {
49        Misere {
50            game: &mut self.game,
51        }
52    }
53
54    pub fn sum(&mut self) -> GameSum<'_, Impartial<S, F>> {
55        GameSum {
56            game: &mut self.game,
57        }
58    }
59
60    pub fn clear_cache(&mut self) {
61        self.game.grundy.clear();
62        self.game.outcomes.iter_mut().for_each(HashMap::clear);
63        self.game.sums.clear();
64        self.game.sum_ids.clear();
65    }
66}
67
68impl<S, L, F, I> GameAnalyzer<Impartial<S, F>>
69where
70    S: Clone + Eq + Hash,
71    F: FnMut(&S) -> I,
72    I: IntoIterator<Item = Neighbor<S, L>>,
73{
74    pub fn grundy(&mut self) -> GameAnalysis<'_, Impartial<S, F>, S, u64> {
75        GameAnalysis::new(&mut self.game, Impartial::grundy)
76    }
77
78    pub fn value(&mut self) -> GameAnalysis<'_, Impartial<S, F>, S, GameValue> {
79        GameAnalysis::new(&mut self.game, |g, s| GameValue::nimber(g.grundy(s)))
80    }
81
82    pub fn outcome(&mut self) -> GameAnalysis<'_, Impartial<S, F>, S, GameOutcome> {
83        GameAnalysis::new(&mut self.game, |g, s| g.outcome(s, false))
84    }
85
86    /// `combine` receives each move's label and the value of its destination.
87    #[allow(clippy::type_complexity)] // The query owns the fold and its cache.
88    pub fn fold<V, C>(
89        &mut self,
90        mut combine: C,
91    ) -> GameAnalysis<'_, Impartial<S, F>, S, V, false, impl FnMut(&mut Impartial<S, F>, &S) -> V>
92    where
93        V: Clone,
94        C: FnMut(&S, &[(L, V)]) -> V,
95    {
96        let mut cache = HashMap::new();
97        GameAnalysis::new(
98            &mut self.game,
99            move |game: &mut Impartial<S, F>, state: &S| {
100                memoized_game(
101                    state,
102                    &mut cache,
103                    &mut HashSet::new(),
104                    &mut |s| {
105                        ControlFlow::Continue((
106                            (game.moves)(s).into_iter().map(|n| (n.to, n.label)),
107                            Vec::new(),
108                        ))
109                    },
110                    &mut |_, moves| moves.next(),
111                    &mut |values, label, value| {
112                        values.push((label, value));
113                        ControlFlow::Continue(())
114                    },
115                    &mut |s, values| combine(s, &values),
116                )
117            },
118        )
119    }
120}
121
122impl<'a, S, L, F, I> Misere<'a, Impartial<S, F>>
123where
124    S: Clone + Eq + Hash,
125    F: FnMut(&S) -> I,
126    I: IntoIterator<Item = Neighbor<S, L>>,
127{
128    pub fn outcome(self) -> GameAnalysis<'a, Impartial<S, F>, S, GameOutcome, true> {
129        GameAnalysis::new(self.game, |g, s| g.outcome(s, true))
130    }
131}
132impl<'a, S, L, F, I> GameSum<'a, Impartial<S, F>>
133where
134    S: Clone + Eq + Hash,
135    F: FnMut(&S) -> I,
136    I: IntoIterator<Item = Neighbor<S, L>>,
137{
138    pub fn grundy(self) -> GameAnalysis<'a, Impartial<S, F>, [S], u64> {
139        GameAnalysis::new(self.game, Impartial::sum_grundy)
140    }
141}
142impl<'a, S, L, F, I, const MISERE: bool> GameSum<'a, Impartial<S, F>, MISERE>
143where
144    S: Clone + Eq + Hash,
145    F: FnMut(&S) -> I,
146    I: IntoIterator<Item = Neighbor<S, L>>,
147{
148    pub fn outcome(self) -> GameAnalysis<'a, Impartial<S, F>, [S], GameOutcome, MISERE> {
149        GameAnalysis::new(self.game, |g, s| g.sum_outcome(s, MISERE))
150    }
151}
152
153impl<S, L, F, I, V, const MISERE: bool, E> GameAnalysis<'_, Impartial<S, F>, S, V, MISERE, E>
154where
155    F: FnMut(&S) -> I,
156    I: IntoIterator<Item = Neighbor<S, L>>,
157    E: FnMut(&mut Impartial<S, F>, &S) -> V,
158{
159    pub fn moves(&mut self, state: &S) -> Vec<EvaluatedMove<S, L, V>> {
160        (self.game.moves)(state)
161            .into_iter()
162            .map(|next| {
163                let value = self.eval(&next.to);
164                EvaluatedMove { next, value }
165            })
166            .collect()
167    }
168}
169
170impl<S, L, F, I> GameAnalysis<'_, Impartial<S, F>, S, u64>
171where
172    S: Clone + Eq + Hash,
173    F: FnMut(&S) -> I,
174    I: IntoIterator<Item = Neighbor<S, L>>,
175{
176    pub fn winning_moves(&mut self, state: &S) -> Vec<Neighbor<S, L>> {
177        self.game.winning_moves(state, false).collect()
178    }
179}
180impl<S, L, F, I> GameAnalysis<'_, Impartial<S, F>, S, GameValue>
181where
182    S: Clone + Eq + Hash,
183    F: FnMut(&S) -> I,
184    I: IntoIterator<Item = Neighbor<S, L>>,
185{
186    pub fn winning_moves(&mut self, state: &S) -> Vec<Neighbor<S, L>> {
187        self.game.winning_moves(state, false).collect()
188    }
189}
190impl<S, L, F, I, const MISERE: bool> GameAnalysis<'_, Impartial<S, F>, S, GameOutcome, MISERE>
191where
192    S: Clone + Eq + Hash,
193    F: FnMut(&S) -> I,
194    I: IntoIterator<Item = Neighbor<S, L>>,
195{
196    pub fn winning_moves(&mut self, state: &S) -> Vec<Neighbor<S, L>> {
197        self.game.winning_moves(state, MISERE).collect()
198    }
199}
200
201#[allow(clippy::type_complexity)] // Sum states and labels both carry component information.
202impl<S, L, F, I, V, const MISERE: bool, E> GameAnalysis<'_, Impartial<S, F>, [S], V, MISERE, E>
203where
204    S: Clone,
205    F: FnMut(&S) -> I,
206    I: IntoIterator<Item = Neighbor<S, L>>,
207    E: FnMut(&mut Impartial<S, F>, &[S]) -> V,
208{
209    pub fn moves(&mut self, state: &[S]) -> Vec<EvaluatedMove<Vec<S>, (usize, L), V>> {
210        let mut result = Vec::new();
211        for (i, component) in state.iter().enumerate() {
212            for next in (self.game.moves)(component) {
213                let mut to = state.to_vec();
214                to[i] = next.to;
215                let value = self.eval(&to);
216                result.push(EvaluatedMove {
217                    next: Neighbor::new(to, (i, next.label)),
218                    value,
219                });
220            }
221        }
222        result
223    }
224}
225#[allow(clippy::type_complexity)] // Sum states and labels both carry component information.
226impl<S, L, F, I> GameAnalysis<'_, Impartial<S, F>, [S], u64>
227where
228    S: Clone + Eq + Hash,
229    F: FnMut(&S) -> I,
230    I: IntoIterator<Item = Neighbor<S, L>>,
231{
232    pub fn winning_moves(&mut self, state: &[S]) -> Vec<Neighbor<Vec<S>, (usize, L)>> {
233        self.game.sum_winning_moves(state)
234    }
235}
236#[allow(clippy::type_complexity)] // Sum states and labels both carry component information.
237impl<S, L, F, I, const MISERE: bool> GameAnalysis<'_, Impartial<S, F>, [S], GameOutcome, MISERE>
238where
239    S: Clone + Eq + Hash,
240    F: FnMut(&S) -> I,
241    I: IntoIterator<Item = Neighbor<S, L>>,
242{
243    pub fn winning_moves(&mut self, state: &[S]) -> Vec<Neighbor<Vec<S>, (usize, L)>> {
244        if let [single] = state {
245            return self
246                .game
247                .winning_moves(single, MISERE)
248                .map(|next| Neighbor::new(vec![next.to], (0, next.label)))
249                .collect();
250        }
251        if !MISERE {
252            return self.game.sum_winning_moves(state);
253        }
254        let mut result = Vec::new();
255        for (i, component) in state.iter().enumerate() {
256            for next in (self.game.moves)(component) {
257                let mut to = state.to_vec();
258                to[i] = next.to;
259                if self.eval(&to) == GameOutcome::Lose {
260                    result.push(Neighbor::new(to, (i, next.label)));
261                }
262            }
263        }
264        result
265    }
266}
267
268impl<S, L, F, I> Impartial<S, F>
269where
270    S: Clone + Eq + Hash,
271    F: FnMut(&S) -> I,
272    I: IntoIterator<Item = Neighbor<S, L>>,
273{
274    fn grundy(&mut self, state: &S) -> u64 {
275        memoized_game(
276            state,
277            &mut self.grundy,
278            &mut HashSet::new(),
279            &mut |s| {
280                if self.outcomes[0].get(s) == Some(&false) {
281                    return ControlFlow::Break(0);
282                }
283                let moves = (self.moves)(s).into_iter().map(|n| (n.to, ()));
284                ControlFlow::Continue((moves, Vec::new()))
285            },
286            &mut |_, moves| moves.next(),
287            &mut |values, (), value| {
288                values.push(value);
289                ControlFlow::Continue(())
290            },
291            &mut |_, values| game_mex(values),
292        )
293    }
294
295    fn outcome(&mut self, state: &S, misere: bool) -> GameOutcome {
296        let win = memoized_wins(
297            state,
298            &mut self.outcomes[usize::from(misere)],
299            |s| {
300                if !misere && let Some(&value) = self.grundy.get(s) {
301                    return ControlFlow::Break(value != 0);
302                }
303                let moves = (self.moves)(s).into_iter().map(|n| n.to);
304                ControlFlow::Continue(moves)
305            },
306            |_, moves| moves.next(),
307            misere,
308        );
309        GameOutcome::from_win(win)
310    }
311
312    fn winning_moves(&mut self, state: &S, misere: bool) -> impl Iterator<Item = Neighbor<S, L>> {
313        (self.moves)(state).into_iter().filter(move |next| {
314            if !misere && let Some(&value) = self.grundy.get(&next.to) {
315                value == 0
316            } else {
317                self.outcome(&next.to, misere) == GameOutcome::Lose
318            }
319        })
320    }
321
322    fn sum_grundy(&mut self, state: &[S]) -> u64 {
323        match state {
324            [] => return 0,
325            [single] => return self.grundy(single),
326            _ => {}
327        }
328        let (xor, odd) = self.reduce_sum(state);
329        odd.into_iter().fold(xor, |value, s| value ^ self.grundy(s))
330    }
331
332    #[allow(clippy::type_complexity)] // Sum states and labels both carry component information.
333    fn sum_winning_moves(&mut self, state: &[S]) -> Vec<Neighbor<Vec<S>, (usize, L)>> {
334        let xor = self.sum_grundy(state);
335        let mut result = Vec::new();
336        if xor == 0 {
337            return result;
338        }
339        for (i, component) in state.iter().enumerate() {
340            let target = xor ^ self.grundy(component);
341            for next in (self.moves)(component) {
342                if self.grundy(&next.to) == target {
343                    let mut to = state.to_vec();
344                    to[i] = next.to;
345                    result.push(Neighbor::new(to, (i, next.label)));
346                }
347            }
348        }
349        result
350    }
351
352    fn sum_outcome(&mut self, state: &[S], misere: bool) -> GameOutcome {
353        if state.is_empty() {
354            return GameOutcome::from_win(misere);
355        }
356        if let [single] = state {
357            return self.outcome(single, misere);
358        }
359        let win = if misere {
360            if let Some(&win) = self.sums.get(state) {
361                win
362            } else {
363                memoized_wins(
364                    &canonical_sum(state.to_vec(), &mut self.sum_ids),
365                    &mut self.sums,
366                    |_| ControlFlow::Continue((0, None)),
367                    |s, (i, moves)| {
368                        while *i < s.len() {
369                            if *i != 0 && s[*i - 1] == s[*i] {
370                                *i += 1;
371                                continue;
372                            }
373                            let iterator =
374                                moves.get_or_insert_with(|| (self.moves)(&s[*i]).into_iter());
375                            if let Some(next) = iterator.next() {
376                                let mut to = s.clone();
377                                to[*i] = next.to;
378                                return Some(canonical_sum(to, &mut self.sum_ids));
379                            }
380                            *i += 1;
381                            *moves = None;
382                        }
383                        None
384                    },
385                    true,
386                )
387            }
388        } else {
389            let (xor, odd) = self.reduce_sum(state);
390            if xor == 0 && odd.len() == 1 {
391                return self.outcome(odd.into_iter().next().unwrap(), false);
392            }
393            odd.into_iter().fold(xor, |value, s| value ^ self.grundy(s)) != 0
394        };
395        GameOutcome::from_win(win)
396    }
397
398    fn reduce_sum<'a>(&self, state: &'a [S]) -> (u64, HashSet<&'a S>) {
399        let mut xor = 0;
400        let mut odd = HashSet::new();
401        for s in state {
402            if let Some(&value) = self.grundy.get(s) {
403                xor ^= value;
404            } else if self.outcomes[0].get(s) != Some(&false) && !odd.insert(s) {
405                odd.remove(s);
406            }
407        }
408        (xor, odd)
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415    use crate::tools::{Xorshift, testutil::sequence_cases};
416    use std::cell::Cell;
417
418    #[test]
419    fn queries_and_caches() {
420        let mut rng = Xorshift::default();
421        let mut input_rng = Xorshift::default();
422        for takes in sequence_cases(&mut input_rng, 1..=8, 0..=8) {
423            let edges: Vec<Vec<_>> = (0usize..65)
424                .map(|n| {
425                    takes
426                        .iter()
427                        .filter_map(|&take| n.checked_sub(take).map(|to| Neighbor::new(to, take)))
428                        .collect()
429                })
430                .collect();
431            let calls = Cell::new(0);
432            let mut game = GameAnalyzer::new().impartial_moves(|&n: &usize| {
433                calls.set(calls.get() + 1);
434                edges[n].iter().copied()
435            });
436            let mut grundy = [0u64; 65];
437            let mut misere = [true; 65];
438            let mut heights = [0usize; 65];
439            for (n, moves) in edges.iter().enumerate() {
440                grundy[n] = (0..)
441                    .find(|g| moves.iter().all(|m| grundy[m.to] != *g))
442                    .unwrap();
443                misere[n] = moves.is_empty() || moves.iter().any(|m| !misere[m.to]);
444                heights[n] = moves.iter().map(|m| heights[m.to] + 1).max().unwrap_or(0);
445            }
446            let states = [64, 0, 64, rng.random(0..65)];
447            assert_eq!(game.grundy().table(states), states.map(|s| (s, grundy[s])));
448            assert_eq!(game.grundy().verify(0..65, |&s| grundy[s]), None);
449            assert_eq!(calls.get(), 65);
450            let bad = rng.random(1..64);
451            let later = rng.random(bad + 1..65);
452            let mut checked = 0;
453            let mismatch = game
454                .grundy()
455                .verify(0..65, |&s| {
456                    checked += 1;
457                    grundy[s] ^ u64::from(s == bad || s == later)
458                })
459                .unwrap();
460            assert_eq!(
461                (mismatch.state, mismatch.expected, mismatch.actual),
462                (bad, grundy[bad] ^ 1, grundy[bad])
463            );
464            assert_eq!(calls.get(), 65);
465            assert_eq!(checked, bad + 1);
466            for (n, moves) in edges.iter().enumerate() {
467                let [normal, misere_outcome] = [grundy[n] != 0, misere[n]].map(|win| {
468                    if win {
469                        GameOutcome::Win
470                    } else {
471                        GameOutcome::Lose
472                    }
473                });
474                assert_eq!(game.outcome().eval(&n), normal);
475                assert_eq!(game.misere().outcome().eval(&n), misere_outcome);
476                assert_eq!(game.outcome().eval(&n), normal);
477                assert_eq!(game.value().eval(&n).as_nimber(), Some(grundy[n]));
478                let winning: Vec<_> = moves
479                    .iter()
480                    .filter(|m| grundy[m.to] == 0)
481                    .copied()
482                    .collect();
483                assert_eq!(game.grundy().winning_moves(&n), winning);
484                assert_eq!(game.value().winning_moves(&n), winning);
485                assert_eq!(game.outcome().winning_moves(&n), winning);
486                assert_eq!(
487                    game.misere().outcome().winning_moves(&n),
488                    moves
489                        .iter()
490                        .filter(|m| !misere[m.to])
491                        .copied()
492                        .collect::<Vec<_>>()
493                );
494            }
495            let mut height = game.fold(|_: &usize, children: &[(usize, usize)]| {
496                children.iter().map(|(_, h)| h + 1).max().unwrap_or(0)
497            });
498            assert_eq!(height.verify(0..65, |&s| heights[s]), None);
499            for (n, moves) in edges.iter().enumerate() {
500                let expected: Vec<_> = moves
501                    .iter()
502                    .map(|&next| EvaluatedMove {
503                        next,
504                        value: heights[next.to],
505                    })
506                    .collect();
507                assert_eq!(height.moves(&n), expected);
508            }
509            drop(height);
510            let mut order = game.fold(|_: &usize, children: &[(usize, Vec<usize>)]| {
511                children
512                    .iter()
513                    .flat_map(|(label, path)| std::iter::once(*label).chain(path.iter().copied()))
514                    .collect()
515            });
516            // A preorder traversal independently checks the noncommutative fold.
517            for n in 0usize..=4 {
518                let mut expected = Vec::new();
519                let mut stack = vec![(n, 0)];
520                while let Some((s, label)) = stack.pop() {
521                    if label != 0 {
522                        expected.push(label);
523                    }
524                    stack.extend(edges[s].iter().rev().map(|m| (m.to, m.label)));
525                }
526                assert_eq!(order.eval(&n), expected);
527            }
528            drop(order);
529            game.clear_cache();
530            let before = calls.get();
531            assert_eq!(game.grundy().verify(0..65, |&s| grundy[s]), None);
532            assert_eq!(calls.get() - before, 65);
533            game.clear_cache();
534            // Outcome-first queries must agree before Grundy has filled the cache.
535            for n in (0..65).rev() {
536                assert_eq!(game.outcome().eval(&n) == GameOutcome::Win, grundy[n] != 0);
537                assert_eq!(
538                    game.misere().outcome().eval(&n) == GameOutcome::Win,
539                    misere[n]
540                );
541                assert_eq!(game.grundy().eval(&n), grundy[n]);
542            }
543        }
544    }
545
546    #[test]
547    fn cached_children() {
548        for n in [0usize, 1, 32, 33] {
549            let calls = Cell::new(0);
550            let mut game = GameAnalyzer::new().impartial(|&s: &usize| {
551                calls.set(calls.get() + 1);
552                s.checked_sub(1)
553            });
554            game.grundy().eval(&n);
555            calls.set(0);
556            let normal = if n % 2 == 0 {
557                GameOutcome::Win
558            } else {
559                GameOutcome::Lose
560            };
561            assert_eq!(game.outcome().eval(&(n + 1)), normal);
562            assert_eq!(calls.get(), 1);
563            calls.set(0);
564            let misere = if n % 2 == 0 {
565                GameOutcome::Lose
566            } else {
567                GameOutcome::Win
568            };
569            assert_eq!(game.misere().outcome().eval(&(n + 1)), misere);
570            assert_eq!(calls.get(), n + 2);
571            game.clear_cache();
572            game.outcome().eval(&n);
573            calls.set(0);
574            assert_eq!(game.grundy().eval(&(n + 1)), (n as u64 + 1) % 2);
575            assert_eq!(calls.get(), 1 + n % 2);
576        }
577    }
578
579    #[test]
580    fn shortcuts() {
581        for n in [1usize, 2, 31, 32] {
582            let transitions = Cell::new(0);
583            let mut game = GameAnalyzer::new().impartial_moves(|&s: &usize| {
584                let transitions = &transitions;
585                (0..s).map(move |to| {
586                    transitions.set(transitions.get() + 1);
587                    Neighbor::new(to, s - to)
588                })
589            });
590            assert_eq!(game.sum().grundy().eval(&[n, n]), 0);
591            assert_eq!(game.sum().outcome().eval(&[n, n]), GameOutcome::Lose);
592            assert_eq!(transitions.get(), 0);
593            for query in 0..3 {
594                let winning = match query {
595                    0 => game.grundy().winning_moves(&n),
596                    1 => game.value().winning_moves(&n),
597                    _ => game.outcome().winning_moves(&n),
598                };
599                assert_eq!(winning, vec![Neighbor::new(0, n)]);
600                assert_eq!(transitions.get(), 2 * n - 1);
601                game.clear_cache();
602                transitions.set(0);
603            }
604            assert_eq!(
605                game.sum().outcome().winning_moves(&[n]),
606                vec![Neighbor::new(vec![0], (0, n))]
607            );
608            assert_eq!(transitions.get(), 2 * n - 1);
609            game.clear_cache();
610            transitions.set(0);
611            assert_eq!(game.sum().outcome().eval(&[n, 1, n]), GameOutcome::Win);
612            assert_eq!(transitions.get(), 1);
613            assert_eq!(game.sum().grundy().eval(&[n, 1, n]), 1);
614        }
615    }
616}