use std::collections::{HashMap, HashSet};
use std::fmt;
use std::hash::Hash;
fn misere_is_n_inner<P, F>(
pos: &P,
moves: &F,
memo: &mut HashMap<P, bool>,
visiting: &mut HashSet<P>,
) -> Option<bool>
where
P: Clone + Eq + Hash,
F: Fn(&P) -> Vec<P>,
{
if let Some(&v) = memo.get(pos) {
return Some(v);
}
if !visiting.insert(pos.clone()) {
return None;
}
let nexts = moves(pos);
let mut result = nexts.is_empty();
if !result {
for q in &nexts {
match misere_is_n_inner(q, moves, memo, visiting) {
Some(false) => {
result = true;
break;
}
Some(true) => {}
None => {
visiting.remove(pos);
return None;
}
}
}
}
visiting.remove(pos);
memo.insert(pos.clone(), result);
Some(result)
}
pub fn try_misere_is_n<P, F>(pos: &P, moves: &F, memo: &mut HashMap<P, bool>) -> Option<bool>
where
P: Clone + Eq + Hash,
F: Fn(&P) -> Vec<P>,
{
let mut visiting = HashSet::new();
misere_is_n_inner(pos, moves, memo, &mut visiting)
}
pub fn misere_is_p<P, F>(pos: &P, moves: &F, memo: &mut HashMap<P, bool>) -> Option<bool>
where
P: Clone + Eq + Hash,
F: Fn(&P) -> Vec<P>,
{
try_misere_is_n(pos, moves, memo).map(|is_n| !is_n)
}
pub fn nim_canonical(mut heaps: Vec<u128>) -> Vec<u128> {
heaps.retain(|&h| h != 0);
heaps.sort_unstable();
heaps
}
#[allow(clippy::ptr_arg)]
pub fn nim_moves(pos: &Vec<u128>) -> Vec<Vec<u128>> {
let mut out = Vec::new();
for i in 0..pos.len() {
for v in 0..pos[i] {
let mut q = pos.clone();
q[i] = v;
out.push(nim_canonical(q));
}
}
out
}
pub fn misere_nim_p_predicted(heaps: &[u128]) -> bool {
let xor = heaps.iter().fold(0u128, |a, &h| a ^ h);
let max = heaps.iter().copied().max().unwrap_or(0);
if max <= 1 {
heaps.iter().filter(|&&h| h != 0).count() % 2 == 1
} else {
xor == 0
}
}
pub struct AbstractGame {
pub moves: Vec<Vec<usize>>,
}
impl AbstractGame {
fn sum_moves(&self, pos: &[usize]) -> Vec<Vec<usize>> {
let mut out = Vec::new();
for idx in 0..pos.len() {
for &q in &self.moves[pos[idx]] {
let mut np = pos.to_vec();
if q == 0 {
np.remove(idx);
} else {
np[idx] = q;
}
np.sort_unstable();
out.push(np);
}
}
out
}
fn canon(pos: &[usize]) -> Vec<usize> {
let mut v: Vec<usize> = pos.iter().copied().filter(|&p| p != 0).collect();
v.sort_unstable();
v
}
pub fn misere_outcome(
&self,
pos: &[usize],
memo: &mut HashMap<Vec<usize>, bool>,
) -> Option<bool> {
let canon = Self::canon(pos);
try_misere_is_n(&canon, &|p| self.sum_moves(p), memo)
}
}
fn multisets(atoms: &[usize], max: usize) -> Vec<Vec<usize>> {
let mut result = vec![vec![]];
let mut frontier = vec![vec![]];
for _ in 0..max {
let mut next = Vec::new();
for m in &frontier {
let last = m.last().copied().unwrap_or(0);
for &a in atoms.iter().filter(|&&a| a >= last) {
let mut nm = m.clone();
nm.push(a);
next.push(nm);
}
}
result.extend(next.iter().cloned());
frontier = next;
}
result
}
#[derive(Debug, Clone)]
pub struct Quotient {
pub elements: Vec<Vec<usize>>,
pub test_positions: Vec<Vec<usize>>,
pub signatures: Vec<Vec<bool>>,
pub class_of: Vec<usize>,
pub class_rep: Vec<Vec<usize>>,
pub class_is_p: Vec<bool>,
pub multiplication: Option<Vec<Vec<usize>>>,
pub multiplication_consistent: bool,
pub elements_closed_under_sum: bool,
}
impl Quotient {
pub fn num_classes(&self) -> usize {
self.class_rep.len()
}
pub fn class_product(&self, a: usize, b: usize) -> Option<usize> {
self.multiplication
.as_ref()
.and_then(|m| m.get(a))
.and_then(|row| row.get(b))
.copied()
}
pub fn has_complete_bounded_monoid(&self) -> bool {
self.multiplication.is_some() && self.multiplication_consistent
}
pub fn signature_of_element(&self, element_index: usize) -> Option<&[bool]> {
self.signatures.get(element_index).map(Vec::as_slice)
}
pub fn display(&self) -> String {
self.to_string()
}
}
impl fmt::Display for Quotient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let p_classes = self.class_is_p.iter().filter(|&&p| p).count();
let monoid = if self.has_complete_bounded_monoid() {
"complete monoid"
} else {
"partial monoid"
};
write!(
f,
"Quotient(order={}, P-classes={p_classes}, {monoid})",
self.num_classes(),
)
}
}
fn build_quotient(
elements: Vec<Vec<usize>>,
tests: &[Vec<usize>],
mut outcome: impl FnMut(&[usize]) -> Option<bool>,
) -> Option<Quotient> {
let mut signatures: Vec<Vec<bool>> = Vec::with_capacity(elements.len());
for g in &elements {
let mut sig = Vec::with_capacity(tests.len());
for t in tests {
let mut gt = g.clone();
gt.extend_from_slice(t);
sig.push(outcome(>)?);
}
signatures.push(sig);
}
let mut class_of = vec![0usize; elements.len()];
let mut uniq: Vec<Vec<bool>> = Vec::new();
let mut class_rep: Vec<Vec<usize>> = Vec::new();
for (i, sig) in signatures.iter().enumerate() {
match uniq.iter().position(|s| s == sig) {
Some(c) => class_of[i] = c,
None => {
class_of[i] = uniq.len();
uniq.push(sig.clone());
class_rep.push(elements[i].clone());
}
}
}
let mut class_is_p: Vec<bool> = Vec::with_capacity(class_rep.len());
for r in &class_rep {
class_is_p.push(!outcome(r)?);
}
let (multiplication, multiplication_consistent, elements_closed_under_sum) =
build_multiplication(&elements, &class_of, &class_rep, uniq.len());
Some(Quotient {
elements,
test_positions: tests.to_vec(),
signatures,
class_of,
class_rep,
class_is_p,
multiplication,
multiplication_consistent,
elements_closed_under_sum,
})
}
fn sum_multiset(a: &[usize], b: &[usize]) -> Vec<usize> {
let mut out = a.to_vec();
out.extend_from_slice(b);
out.sort_unstable();
out
}
fn build_multiplication(
elements: &[Vec<usize>],
class_of: &[usize],
class_rep: &[Vec<usize>],
num_classes: usize,
) -> (Option<Vec<Vec<usize>>>, bool, bool) {
let element_index: HashMap<Vec<usize>, usize> = elements
.iter()
.cloned()
.enumerate()
.map(|(i, e)| (e, i))
.collect();
let mut table = vec![vec![None; num_classes]; num_classes];
let mut closed_under_sum = true;
for (i, a) in elements.iter().enumerate() {
for (j, b) in elements.iter().enumerate() {
let prod = sum_multiset(a, b);
let Some(&k) = element_index.get(&prod) else {
closed_under_sum = false;
continue;
};
let ca = class_of[i];
let cb = class_of[j];
let cp = class_of[k];
match table[ca][cb] {
Some(prev) if prev != cp => return (None, false, closed_under_sum),
Some(_) => {}
None => {
table[ca][cb] = Some(cp);
table[cb][ca] = Some(cp);
}
}
}
}
for a in 0..num_classes {
for b in 0..num_classes {
if table[a][b].is_none() {
let prod = sum_multiset(&class_rep[a], &class_rep[b]);
let Some(&k) = element_index.get(&prod) else {
return (None, false, closed_under_sum);
};
let cp = class_of[k];
table[a][b] = Some(cp);
table[b][a] = Some(cp);
}
}
}
let table = table
.into_iter()
.map(|row| {
row.into_iter()
.map(|c| c.expect("all class products filled"))
.collect()
})
.collect();
(Some(table), true, closed_under_sum)
}
pub fn misere_quotient(
game: &AbstractGame,
atoms: &[usize],
elem_bound: usize,
test_bound: usize,
) -> Option<Quotient> {
let mut atoms_sorted = atoms.to_vec();
atoms_sorted.sort_unstable();
let elements = multisets(&atoms_sorted, elem_bound);
let tests = multisets(&atoms_sorted, test_bound);
let mut memo: HashMap<Vec<usize>, bool> = HashMap::new();
build_quotient(elements, &tests, |g| game.misere_outcome(g, &mut memo))
}
pub fn octal_moves(code: &[u128], pos: &[u128]) -> Vec<Vec<u128>> {
let mut out = Vec::new();
for idx in 0..pos.len() {
let n = pos[idx];
let base: Vec<u128> = pos
.iter()
.enumerate()
.filter(|&(i, _)| i != idx)
.map(|(_, &h)| h)
.collect();
for k in 1..=n {
let d = *code.get((k - 1) as usize).unwrap_or(&0);
let rem = n - k;
if rem == 0 {
if d & 1 != 0 {
let mut p = base.clone();
p.sort_unstable();
out.push(p);
}
} else {
if d & 2 != 0 {
let mut p = base.clone();
p.push(rem);
p.sort_unstable();
out.push(p);
}
if d & 4 != 0 {
for a in 1..=rem / 2 {
let mut p = base.clone();
p.push(a);
p.push(rem - a);
p.sort_unstable();
out.push(p);
}
}
}
}
}
out
}
pub fn octal_misere_quotient(
code: &[u128],
max_heap: usize,
elem_bound: usize,
test_bound: usize,
) -> Option<Quotient> {
let atoms: Vec<usize> = (1..=max_heap).collect();
let elements = multisets(&atoms, elem_bound);
let tests = multisets(&atoms, test_bound);
let mut memo: HashMap<Vec<u128>, bool> = HashMap::new();
let moves = |p: &Vec<u128>| octal_moves(code, p);
build_quotient(elements, &tests, |g| {
let mut pos: Vec<u128> = g.iter().map(|&x| x as u128).collect();
pos.sort_unstable();
try_misere_is_n(&pos, &moves, &mut memo)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn misere_nim_matches_boutons_theorem() {
let mut memo: HashMap<Vec<u128>, bool> = HashMap::new();
fn rec(prefix: &mut Vec<u128>, depth: usize, memo: &mut HashMap<Vec<u128>, bool>) {
if depth == 0 {
let pos = nim_canonical(prefix.clone());
let is_p = misere_is_p(&pos, &nim_moves, memo);
assert_eq!(
is_p.expect("Nim move graph is acyclic"),
misere_nim_p_predicted(&pos),
"misère Nim mismatch at {pos:?}"
);
return;
}
for h in 0..=4u128 {
prefix.push(h);
rec(prefix, depth - 1, memo);
prefix.pop();
}
}
rec(&mut Vec::new(), 4, &mut memo);
}
#[test]
fn misere_nim_closed_form_ignores_zero_heaps() {
assert!(misere_nim_p_predicted(&[1, 0]));
assert!(!misere_nim_p_predicted(&[0]));
assert_eq!(
misere_is_p(&vec![1], &nim_moves, &mut HashMap::new()),
Some(misere_nim_p_predicted(&[1, 0]))
);
}
#[test]
fn cyclic_game_is_rejected() {
fn self_loop(_: &u128) -> Vec<u128> {
vec![0]
}
let mut memo = HashMap::new();
assert_eq!(try_misere_is_n(&0u128, &self_loop, &mut memo), None);
assert_eq!(misere_is_p(&0u128, &self_loop, &mut HashMap::new()), None);
}
#[test]
fn star_misere_quotient_is_z2() {
let star = AbstractGame {
moves: vec![vec![], vec![0]],
};
let q = misere_quotient(&star, &[1], 5, 3).expect("star quotient search is acyclic");
assert_eq!(q.num_classes(), 2, "⋆ quotient should be order 2 (ℤ/2)");
assert_eq!(
q.test_positions,
vec![vec![], vec![1], vec![1, 1], vec![1, 1, 1]]
);
assert_eq!(q.signatures.len(), q.elements.len());
assert!(q
.signature_of_element(0)
.is_some_and(|sig| sig.len() == q.test_positions.len()));
let empty_class = q.class_of[q.elements.iter().position(|e| e.is_empty()).unwrap()];
let star_class = q.class_of[q.elements.iter().position(|e| e == &vec![1]).unwrap()];
assert!(!q.class_is_p[empty_class]);
assert!(q.class_is_p[star_class]);
let two = q.class_of[q.elements.iter().position(|e| e == &vec![1, 1]).unwrap()];
assert_eq!(two, empty_class);
assert_eq!(q.class_product(star_class, star_class), Some(empty_class));
assert!(q.multiplication_consistent);
assert!(q.has_complete_bounded_monoid());
assert!(!q.elements_closed_under_sum);
assert_eq!(q.class_is_p.iter().filter(|&&p| p).count(), 1);
assert_eq!(
q.to_string(),
"Quotient(order=2, P-classes=1, complete monoid)"
);
assert_eq!(q.display(), q.to_string());
}
#[test]
fn octal_nim_matches_misere_nim() {
let code = [3u128, 3, 3, 3];
let mut memo: HashMap<Vec<u128>, bool> = HashMap::new();
for heaps in [
vec![1u128],
vec![1, 1],
vec![2],
vec![2, 1],
vec![3, 2, 1],
vec![2, 2],
vec![3, 3],
] {
let mut h = heaps.clone();
h.sort_unstable();
let is_n = try_misere_is_n(&h, &|p| octal_moves(&code, p), &mut memo)
.expect("octal Nim move graph is acyclic");
assert_eq!(
is_n,
!misere_nim_p_predicted(&heaps),
"octal Nim ≠ Bouton at {heaps:?}"
);
}
}
#[test]
fn octal_star_quotient_is_z2() {
let q = octal_misere_quotient(&[3, 3, 3], 1, 5, 3)
.expect("octal_moves is always acyclic, so this quotient search is too");
assert_eq!(q.num_classes(), 2);
}
#[test]
fn misere_is_genuinely_nonlinear() {
let mut memo: HashMap<Vec<u128>, bool> = HashMap::new();
let one = nim_canonical(vec![1]); let oneone = nim_canonical(vec![1, 1]); assert!(misere_is_p(&one, &nim_moves, &mut memo).expect("Nim move graph is acyclic"));
assert!(!misere_is_p(&oneone, &nim_moves, &mut memo).expect("Nim move graph is acyclic"));
let empty = nim_canonical(vec![]);
assert!(!misere_is_p(&empty, &nim_moves, &mut memo).expect("Nim move graph is acyclic"));
let three_ones = nim_canonical(vec![1, 1, 1]); assert!(misere_is_p(&three_ones, &nim_moves, &mut memo).expect("Nim move graph is acyclic"));
}
#[test]
fn cyclic_abstract_game_returns_none_not_panic() {
let game = AbstractGame {
moves: vec![vec![], vec![1]], };
let mut memo = HashMap::new();
assert_eq!(
game.misere_outcome(&[1], &mut memo),
None,
"cyclic AbstractGame must return None, not panic"
);
}
#[test]
fn cyclic_abstract_game_quotient_builder_returns_none_not_panic() {
let game = AbstractGame {
moves: vec![vec![], vec![1]], };
assert!(
misere_quotient(&game, &[1], 3, 2).is_none(),
"cyclic AbstractGame through misere_quotient must return None, not panic"
);
}
}