use crate::games::Game;
use crate::scalar::Surreal;
use std::collections::{BTreeSet, HashSet};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Color {
Blue,
Red,
Green,
}
#[derive(Clone, Debug)]
pub struct Hackenbush {
edges: Vec<(usize, usize, Color)>,
}
impl Hackenbush {
pub fn new(edges: Vec<(usize, usize, Color)>) -> Hackenbush {
let raw = Hackenbush { edges };
let grounded = raw.grounded();
Hackenbush {
edges: raw
.edges
.into_iter()
.filter(|&(u, v, _)| grounded.contains(&u) && grounded.contains(&v))
.collect(),
}
}
pub fn string(colors: &[Color]) -> Hackenbush {
let edges = colors
.iter()
.enumerate()
.map(|(i, &c)| (i, i + 1, c))
.collect();
Hackenbush { edges }
}
pub fn edges(&self) -> &[(usize, usize, Color)] {
&self.edges
}
fn grounded(&self) -> HashSet<usize> {
let mut reach = HashSet::new();
reach.insert(0usize);
let mut changed = true;
while changed {
changed = false;
for &(u, v, _) in &self.edges {
let (ur, vr) = (reach.contains(&u), reach.contains(&v));
if ur ^ vr {
reach.insert(if ur { v } else { u });
changed = true;
}
}
}
reach
}
fn remove_edge(&self, i: usize) -> Hackenbush {
let mut edges = self.edges.clone();
edges.remove(i);
let pruned = Hackenbush { edges };
let grounded = pruned.grounded();
Hackenbush {
edges: pruned
.edges
.into_iter()
.filter(|&(u, v, _)| grounded.contains(&u) && grounded.contains(&v))
.collect(),
}
}
pub fn to_game(&self) -> Game {
let mut left = Vec::new();
let mut right = Vec::new();
for (i, &(_, _, c)) in self.edges.iter().enumerate() {
let sub = self.remove_edge(i).to_game();
match c {
Color::Blue => left.push(sub),
Color::Red => right.push(sub),
Color::Green => {
left.push(sub.clone());
right.push(sub);
}
}
}
Game::new(left, right)
}
pub fn value(&self) -> Option<Surreal> {
self.to_game().number_value()
}
pub fn grundy(&self) -> Option<u128> {
if self.edges.iter().any(|&(_, _, c)| c != Color::Green) {
return None;
}
Some(self.grundy_green())
}
fn grundy_green(&self) -> u128 {
let reachable: BTreeSet<u128> = (0..self.edges.len())
.map(|i| self.remove_edge(i).grundy_green())
.collect();
let mut m = 0u128;
while reachable.contains(&m) {
m += 1;
}
m
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scalar::{Rational, Surreal};
fn blue_red(colors: &[Color]) -> Hackenbush {
Hackenbush::string(colors)
}
#[test]
fn blue_and_red_strings_are_integers() {
use Color::*;
for n in 0u128..5 {
let blue = Hackenbush::string(&vec![Blue; n as usize]);
assert_eq!(blue.value(), Some(Surreal::from_int(n as i128)));
assert!(blue.to_game().eq(&Game::integer(n as i128)));
let red = Hackenbush::string(&vec![Red; n as usize]);
assert_eq!(red.value(), Some(Surreal::from_int(-(n as i128))));
}
}
#[test]
fn green_strings_are_nim_heaps() {
use Color::*;
for n in 0u128..6 {
let g = Hackenbush::string(&vec![Green; n as usize]);
assert_eq!(g.grundy(), Some(n));
assert!(g.to_game().eq(&Game::nim_heap(n)));
if n >= 1 {
assert_eq!(g.value(), None); }
}
}
#[test]
fn blue_red_strings_are_their_sign_expansion() {
use Color::*;
let cases: [&[Color]; 6] = [
&[Blue, Red], &[Blue, Red, Blue], &[Red, Blue], &[Blue, Blue, Red], &[Blue, Red, Red], &[Red, Blue, Red, Blue], ];
for colors in cases {
let signs: Vec<bool> = colors.iter().map(|&c| c == Blue).collect();
let expected = Surreal::from_sign_expansion(&signs);
assert_eq!(
blue_red(colors).value(),
Some(expected),
"colors {:?}",
colors
);
}
}
#[test]
fn the_unifier_one_structure_three_worlds() {
use Color::*;
assert_eq!(
Hackenbush::string(&[Blue, Blue, Blue]).value(),
Some(Surreal::from_int(3))
);
assert_eq!(Hackenbush::string(&[Green, Green]).grundy(), Some(2));
assert_eq!(
Hackenbush::string(&[Blue, Red]).value(),
Some(Surreal::from_rational(Rational::new(1, 2)))
);
}
#[test]
fn green_cycle_and_mixed() {
use Color::*;
let triangle = Hackenbush::new(vec![(0, 1, Green), (1, 2, Green), (2, 0, Green)]);
assert_eq!(triangle.grundy(), Some(1));
let mixed = Hackenbush::new(vec![(0, 1, Green), (1, 2, Blue)]);
assert_eq!(mixed.value(), None);
assert!(mixed.grundy().is_none()); }
#[test]
fn branched_blue_red_position_matches_an_independent_ordinal_sum_reconstruction() {
use Color::*;
let branched = Hackenbush::new(vec![(0, 1, Blue), (1, 2, Red), (1, 3, Blue)]);
let branch_a = Hackenbush::string(&[Red]).to_game(); let branch_b = Hackenbush::string(&[Blue]).to_game(); let predicted = Game::integer(1).ordinal_sum(&branch_a.add(&branch_b));
assert!(
branched.to_game().eq(&predicted),
"branched Hackenbush value {} should match the independent ordinal-sum \
reconstruction {}",
branched.to_game().display(),
predicted.display()
);
let value = branched
.value()
.expect("blue/red-only position is a number");
assert_eq!(value, Surreal::from_int(1));
let rebuilt = Game::from_surreal(&value).expect("integer values are dyadic");
assert!(rebuilt.canonical().structural_eq(&predicted.canonical()));
}
#[test]
fn floating_edges_are_pruned_at_construction() {
use Color::*;
let h = Hackenbush::new(vec![(1, 2, Blue)]);
assert!(
h.edges().is_empty(),
"floating edge should be pruned from the position"
);
assert_eq!(
h.value(),
Some(Surreal::from_int(0)),
"position with no grounded edges is the empty game, value 0"
);
}
#[test]
fn partially_floating_edges_are_pruned() {
use Color::*;
let h = Hackenbush::new(vec![
(0, 1, Blue),
(1, 2, Blue),
(2, 3, Blue),
(4, 5, Red), ]);
assert_eq!(
h.edges().len(),
3,
"only the 3 grounded edges should survive"
);
assert_eq!(h.value(), Some(Surreal::from_int(3)));
}
}