use crate::games::grundy::mex;
use crate::games::kernel::{self, Outcome};
use std::fmt;
const MAX_SIDLING_ASSIGNMENTS: usize = 200_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopyNimber {
Value(u128),
Side,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoopyNimCertificate {
pub outcomes: Vec<Outcome>,
pub side_positions: Vec<usize>,
pub used_sidling_solver: bool,
pub sidling_assignments_examined: usize,
pub recovery_condition_holds: bool,
pub recovery_blockers: Vec<usize>,
}
impl LoopyNimCertificate {
pub fn display(&self) -> String {
self.to_string()
}
}
impl fmt::Display for LoopyNimCertificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let side = self.side_positions.len();
let finite = self.outcomes.len() - side;
let status = if self.recovery_condition_holds {
"certified additive".to_string()
} else {
let n = self.recovery_blockers.len();
format!(
"uncertified ({n} recovery blocker{})",
if n == 1 { "" } else { "s" }
)
};
write!(
f,
"LoopyNimCertificate({finite} finite-valued / {side} Side(∞), {status})"
)
}
}
pub fn loopy_nim_values(succ: &[Vec<usize>]) -> Option<Vec<LoopyNimber>> {
loopy_nim_values_certified(succ).map(|(values, _)| values)
}
pub fn loopy_nim_values_certified(
succ: &[Vec<usize>],
) -> Option<(Vec<LoopyNimber>, LoopyNimCertificate)> {
let n = succ.len();
let out = kernel::outcomes(succ);
let is_side: Vec<bool> = out.iter().map(|o| *o == Outcome::Draw).collect();
let mut val = vec![0u128; n];
let mut state = vec![0u128; n]; let mut needs_sidling = false;
fn dfs(
succ: &[Vec<usize>],
is_side: &[bool],
v: usize,
state: &mut [u128],
val: &mut [u128],
) -> Option<()> {
match state[v] {
2 => return Some(()),
1 => return None, _ => {}
}
state[v] = 1;
let mut opts = Vec::new();
for &w in &succ[v] {
if is_side[w] {
continue; }
dfs(succ, is_side, w, state, val)?;
opts.push(val[w]);
}
val[v] = mex(opts);
state[v] = 2;
Some(())
}
for v in 0..n {
if !is_side[v] && dfs(succ, &is_side, v, &mut state, &mut val).is_none() {
needs_sidling = true;
break;
}
}
let mut assignments = 0usize;
if needs_sidling {
let (sidled, count) = solve_mex_sidling(succ, &is_side)?;
val = sidled;
assignments = count;
}
let values: Vec<LoopyNimber> = (0..n)
.map(|v| {
if is_side[v] {
LoopyNimber::Side
} else {
LoopyNimber::Value(val[v])
}
})
.collect();
let recovery_blockers: Vec<usize> = (0..n)
.filter(|&v| !is_side[v] && succ[v].iter().any(|&w| is_side[w]))
.collect();
let cert = LoopyNimCertificate {
outcomes: out,
side_positions: is_side
.iter()
.enumerate()
.filter_map(|(i, &side)| side.then_some(i))
.collect(),
used_sidling_solver: needs_sidling,
sidling_assignments_examined: assignments,
recovery_condition_holds: recovery_blockers.is_empty(),
recovery_blockers,
};
Some((values, cert))
}
fn solve_mex_sidling(succ: &[Vec<usize>], is_side: &[bool]) -> Option<(Vec<u128>, usize)> {
let n = succ.len();
let finite: Vec<usize> = (0..n).filter(|&v| !is_side[v]).collect();
let mut order = finite.clone();
order.sort_by_key(|&v| succ[v].iter().filter(|&&w| !is_side[w]).count());
let mut assigned = vec![false; n];
for (v, &side) in is_side.iter().enumerate() {
if side {
assigned[v] = true;
}
}
let values = vec![0u128; n];
let max_for: Vec<u128> = (0..n)
.map(|v| succ[v].iter().filter(|&&w| !is_side[w]).count() as u128)
.collect();
let examined = 0usize;
struct Solver<'a> {
order: Vec<usize>,
succ: &'a [Vec<usize>],
is_side: &'a [bool],
max_for: Vec<u128>,
assigned: Vec<bool>,
values: Vec<u128>,
examined: usize,
}
impl Solver<'_> {
fn rec(&mut self, idx: usize, solution: &mut Option<Vec<u128>>) -> Option<bool> {
if self.examined > MAX_SIDLING_ASSIGNMENTS {
return None;
}
if idx == self.order.len() {
if all_mex_equations_hold(self.succ, self.is_side, &self.values) {
if solution.is_some() {
return Some(false); }
*solution = Some(self.values.clone());
}
return Some(true);
}
let v = self.order[idx];
for candidate in 0..=self.max_for[v] {
self.examined += 1;
if self.examined > MAX_SIDLING_ASSIGNMENTS {
return None;
}
self.values[v] = candidate;
self.assigned[v] = true;
if partial_mex_equations_hold(self.succ, self.is_side, &self.assigned, &self.values)
{
match self.rec(idx + 1, solution) {
Some(true) => {}
Some(false) => return Some(false),
None => return None,
}
}
self.assigned[v] = false;
}
Some(true)
}
}
let mut solver = Solver {
order,
succ,
is_side,
max_for,
assigned,
values,
examined,
};
let mut solution = None;
match solver.rec(0, &mut solution) {
Some(true) => solution.map(|values| (values, solver.examined)),
Some(false) | None => None,
}
}
fn partial_mex_equations_hold(
succ: &[Vec<usize>],
is_side: &[bool],
assigned: &[bool],
values: &[u128],
) -> bool {
for v in 0..succ.len() {
if is_side[v] || !assigned[v] {
continue;
}
if succ[v].iter().any(|&w| !is_side[w] && !assigned[w]) {
continue;
}
if values[v] != mex_value(succ, is_side, values, v) {
return false;
}
}
true
}
fn all_mex_equations_hold(succ: &[Vec<usize>], is_side: &[bool], values: &[u128]) -> bool {
(0..succ.len())
.filter(|&v| !is_side[v])
.all(|v| values[v] == mex_value(succ, is_side, values, v))
}
fn mex_value(succ: &[Vec<usize>], is_side: &[bool], values: &[u128], v: usize) -> u128 {
mex(succ[v]
.iter()
.filter_map(|&w| (!is_side[w]).then_some(values[w])))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loopy_nim_certificate_render_fully_certified() {
let succ = vec![vec![], vec![0]];
let (_values, cert) =
loopy_nim_values_certified(&succ).expect("acyclic graph solves without sidling");
assert!(cert.recovery_condition_holds);
assert!(cert.recovery_blockers.is_empty());
assert_eq!(cert.side_positions.len(), 0);
assert_eq!(
cert.to_string(),
"LoopyNimCertificate(2 finite-valued / 0 Side(∞), certified additive)"
);
assert_eq!(cert.display(), cert.to_string());
}
#[test]
fn loopy_nim_certificate_render_uncertified_with_blocker() {
let succ = vec![vec![], vec![2], vec![1], vec![0, 1]];
let (values, cert) =
loopy_nim_values_certified(&succ).expect("the finite half solves without sidling");
assert!(!cert.used_sidling_solver);
assert_eq!(cert.side_positions, vec![1, 2]);
assert_eq!(cert.recovery_blockers, vec![3]);
assert!(!cert.recovery_condition_holds);
assert_eq!(values[1], LoopyNimber::Side);
assert_eq!(values[3], LoopyNimber::Value(1));
assert_eq!(
cert.to_string(),
"LoopyNimCertificate(2 finite-valued / 2 Side(∞), uncertified (1 recovery blocker))"
);
assert_eq!(cert.display(), cert.to_string());
}
}