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)] 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 #[allow(clippy::type_complexity)] 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 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 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 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 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)] impl<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)] impl<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 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 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 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 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 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 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 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 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 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}