use crate::scalar::{Scalar, Surreal};
use std::cmp::Ordering;
use std::fmt;
use std::sync::Arc;
#[derive(Clone)]
pub struct Game(Arc<GameData>);
struct GameData {
left: Vec<Game>,
right: Vec<Game>,
}
impl Game {
pub fn new(left: Vec<Game>, right: Vec<Game>) -> Game {
Game(Arc::new(GameData { left, right }))
}
pub fn left(&self) -> &[Game] {
&self.0.left
}
pub fn right(&self) -> &[Game] {
&self.0.right
}
pub fn ptr_eq(&self, other: &Game) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
pub fn ptr_id(&self) -> usize {
Arc::as_ptr(&self.0) as usize
}
pub fn zero() -> Game {
Game::new(vec![], vec![])
}
pub fn star() -> Game {
let z = Game::zero();
Game::new(vec![z.clone()], vec![z])
}
pub fn nim_heap(n: u128) -> Game {
let opts: Vec<Game> = (0..n).map(Game::nim_heap).collect();
Game::new(opts.clone(), opts)
}
pub fn integer(n: i128) -> Game {
if n == 0 {
Game::zero()
} else if n > 0 {
Game::new(vec![Game::integer(n - 1)], vec![])
} else {
Game::new(vec![], vec![Game::integer(n + 1)])
}
}
pub fn up() -> Game {
Game::new(vec![Game::zero()], vec![Game::star()])
}
pub fn switch(a: i128, b: i128) -> Game {
Game::new(vec![Game::integer(a)], vec![Game::integer(b)])
}
pub fn neg(&self) -> Game {
Game::new(
self.right().iter().map(|g| g.neg()).collect(),
self.left().iter().map(|g| g.neg()).collect(),
)
}
pub fn add(&self, other: &Game) -> Game {
let mut left = Vec::new();
for gl in self.left() {
left.push(gl.add(other));
}
for hl in other.left() {
left.push(self.add(hl));
}
let mut right = Vec::new();
for gr in self.right() {
right.push(gr.add(other));
}
for hr in other.right() {
right.push(self.add(hr));
}
Game::new(left, right)
}
pub fn le(&self, other: &Game) -> bool {
self.left().iter().all(|gl| !other.le(gl)) && other.right().iter().all(|hr| !hr.le(self))
}
#[allow(clippy::should_implement_trait)]
pub fn eq(&self, other: &Game) -> bool {
self.le(other) && other.le(self)
}
pub fn fuzzy(&self, other: &Game) -> bool {
!self.le(other) && !other.le(self)
}
pub fn birthday(&self) -> u128 {
self.left()
.iter()
.chain(self.right())
.map(|g| g.birthday())
.max()
.map_or(0, |m| m + 1)
}
pub fn times_int(&self, n: i128) -> Game {
if n == 0 {
Game::zero()
} else if n > 0 {
let mut acc = self.clone();
for _ in 1..n {
acc = acc.add(self);
}
acc
} else {
self.neg().times_int(-n)
}
}
pub fn ordinal_sum(&self, h: &Game) -> Game {
let mut left: Vec<Game> = self.left().to_vec();
for hl in h.left() {
left.push(self.ordinal_sum(hl));
}
let mut right: Vec<Game> = self.right().to_vec();
for hr in h.right() {
right.push(self.ordinal_sum(hr));
}
Game::new(left, right)
}
pub fn display(&self) -> String {
self.to_string()
}
pub fn is_number(&self) -> bool {
self.left().iter().all(|g| g.is_number())
&& self.right().iter().all(|g| g.is_number())
&& self
.left()
.iter()
.all(|gl| self.right().iter().all(|gr| gl.le(gr) && !gr.le(gl)))
}
pub fn is_all_small(&self) -> bool {
if self.left().is_empty() != self.right().is_empty() {
return false;
}
self.left()
.iter()
.chain(self.right())
.all(|g| g.is_all_small())
}
pub fn canonical(&self) -> Game {
let left: Vec<Game> = self.left().iter().map(Game::canonical).collect();
let right: Vec<Game> = self.right().iter().map(Game::canonical).collect();
let mut cur = Game::new(left, right);
loop {
let (bypassed, bypassed_any) = cur.bypass_reversible_once();
let reduced = bypassed.remove_dominated();
let removed_any = reduced.left().len() != bypassed.left().len()
|| reduced.right().len() != bypassed.right().len();
cur = reduced;
if !bypassed_any && !removed_any {
return cur;
}
}
}
fn bypass_reversible_once(&self) -> (Game, bool) {
let mut changed = false;
let mut new_left = Vec::new();
for l in self.left() {
if let Some(lr) = l.right().iter().find(|lr| lr.le(self)) {
changed = true;
new_left.extend(lr.left().iter().cloned());
} else {
new_left.push(l.clone());
}
}
let mut new_right = Vec::new();
for r in self.right() {
if let Some(rl) = r.left().iter().find(|rl| self.le(rl)) {
changed = true;
new_right.extend(rl.right().iter().cloned());
} else {
new_right.push(r.clone());
}
}
(Game::new(new_left, new_right), changed)
}
fn remove_dominated(&self) -> Game {
Game::new(maximal_games(self.left()), minimal_games(self.right()))
}
pub fn structural_string(&self) -> String {
let mut l: Vec<String> = self.left().iter().map(Game::structural_string).collect();
let mut r: Vec<String> = self.right().iter().map(Game::structural_string).collect();
l.sort();
r.sort();
format!("{{{}|{}}}", l.join(","), r.join(","))
}
pub fn canonical_string(&self) -> String {
self.canonical().structural_string()
}
pub fn structural_eq(&self, other: &Game) -> bool {
self.structural_string() == other.structural_string()
}
pub fn is_canonical(&self) -> bool {
self.structural_eq(&self.canonical())
}
pub fn number_value(&self) -> Option<Surreal> {
if !self.is_number() {
return None;
}
let lvals: Vec<Surreal> = self
.left()
.iter()
.map(Game::number_value)
.collect::<Option<_>>()?;
let rvals: Vec<Surreal> = self
.right()
.iter()
.map(Game::number_value)
.collect::<Option<_>>()?;
let lmax = lvals
.into_iter()
.reduce(|a, b| if a.cmp(&b) == Ordering::Less { b } else { a });
let rmin = rvals
.into_iter()
.reduce(|a, b| if a.cmp(&b) == Ordering::Greater { b } else { a });
match (lmax, rmin) {
(None, None) => Some(Surreal::zero()),
(Some(l), None) => l.simplest_above(),
(None, Some(r)) => r.simplest_below(),
(Some(l), Some(r)) => Surreal::simplest_between(&l, &r),
}
}
pub fn from_surreal(s: &Surreal) -> Option<Game> {
let (num, k) = s.as_dyadic()?;
Some(game_of_dyadic(num, k))
}
}
impl std::ops::Add for Game {
type Output = Game;
fn add(self, rhs: Game) -> Game {
Game::add(&self, &rhs)
}
}
impl std::ops::Neg for Game {
type Output = Game;
fn neg(self) -> Game {
Game::neg(&self)
}
}
impl fmt::Display for Game {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.left().is_empty() && self.right().is_empty() {
return write!(f, "0");
}
let l: Vec<String> = self.left().iter().map(|g| g.to_string()).collect();
let r: Vec<String> = self.right().iter().map(|g| g.to_string()).collect();
write!(f, "{{{}|{}}}", l.join(","), r.join(","))
}
}
pub(crate) fn integer_value(g: &Game) -> Option<i128> {
let (num, k) = g.number_value()?.as_dyadic()?;
(k == 0).then_some(num)
}
fn maximal_games(opts: &[Game]) -> Vec<Game> {
let mut kept: Vec<Game> = Vec::new();
for cand in opts {
if kept.iter().any(|k| cand.le(k)) {
continue; }
kept.retain(|k| !k.le(cand)); kept.push(cand.clone());
}
kept
}
fn minimal_games(opts: &[Game]) -> Vec<Game> {
let mut kept: Vec<Game> = Vec::new();
for cand in opts {
if kept.iter().any(|k| k.le(cand)) {
continue; }
kept.retain(|k| !cand.le(k)); kept.push(cand.clone());
}
kept
}
fn reduce_dyadic_pair(mut num: i128, mut k: u128) -> (i128, u128) {
while k > 0 && num % 2 == 0 {
num /= 2;
k -= 1;
}
(num, k)
}
fn game_of_dyadic(num: i128, k: u128) -> Game {
if k == 0 {
return Game::integer(num);
}
let (ln, lk) = reduce_dyadic_pair(num - 1, k);
let (rn, rk) = reduce_dyadic_pair(num + 1, k);
Game::new(vec![game_of_dyadic(ln, lk)], vec![game_of_dyadic(rn, rk)])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn game_group_basics() {
assert!(Game::integer(1).add(&Game::integer(-1)).eq(&Game::zero()));
assert!(Game::integer(2)
.add(&Game::integer(3))
.eq(&Game::integer(5)));
assert!(Game::integer(1).le(&Game::integer(2)));
assert!(Game::star().fuzzy(&Game::zero()));
assert!(!Game::star().is_number());
assert!(Game::star().add(&Game::star()).eq(&Game::zero()));
assert!(Game::zero().le(&Game::up()) && !Game::up().le(&Game::zero()));
assert!(!Game::up().is_number());
assert_eq!(Game::zero().birthday(), 0);
assert_eq!(Game::star().birthday(), 1);
assert_eq!(Game::integer(3).birthday(), 3);
}
#[test]
fn operator_traits_forward_to_game_group_operations() {
let sum = Game::integer(2) + Game::integer(3);
assert!(sum.eq(&Game::integer(5)));
let up = Game::up();
let cancelled = up.clone() + -up;
assert!(cancelled.eq(&Game::zero()));
let star_zero = Game::star() + Game::star();
assert!(star_zero.eq(&Game::zero()));
}
#[test]
fn ordinal_sum_basics() {
assert!(Game::zero().ordinal_sum(&Game::up()).eq(&Game::up()));
assert!(Game::switch(2, -1)
.ordinal_sum(&Game::zero())
.structural_eq(&Game::switch(2, -1)));
let star2 = Game::new(
vec![Game::integer(0), Game::star()],
vec![Game::integer(0), Game::star()],
);
assert!(Game::star().ordinal_sum(&Game::star()).eq(&star2));
assert!(Game::integer(1)
.ordinal_sum(&Game::integer(1))
.eq(&Game::integer(2)));
assert!(!Game::integer(1)
.ordinal_sum(&Game::star())
.eq(&Game::star().ordinal_sum(&Game::integer(1))));
}
#[test]
fn canonical_removes_dominated_options() {
let g = Game::new(vec![Game::integer(0), Game::integer(-1)], vec![]);
assert!(g.canonical().structural_eq(&Game::integer(1)));
let g = Game::new(vec![Game::integer(0)], vec![Game::integer(2)]);
assert!(g.canonical().structural_eq(&Game::integer(1)));
}
#[test]
fn canonical_fixes_the_already_simple_games() {
for g in [
Game::star(),
Game::up(),
Game::switch(1, -1),
Game::integer(4),
] {
assert!(g.is_canonical(), "{} should be canonical", g.display());
assert!(g.canonical().structural_eq(&g));
}
}
#[test]
fn canonical_of_g_minus_g_is_zero() {
for g in [
Game::up(),
Game::switch(3, -2),
Game::star(),
Game::integer(2),
] {
let z = g.add(&g.neg());
assert!(z.eq(&Game::zero()));
assert!(z.canonical().structural_eq(&Game::zero()));
}
}
#[test]
fn canonical_is_idempotent_and_value_preserving() {
let g = Game::new(
vec![Game::integer(0), Game::integer(-1), Game::switch(2, 0)],
vec![Game::integer(3)],
);
let c = g.canonical();
assert!(c.eq(&g)); assert!(c.canonical().structural_eq(&c)); }
fn subsets_up_to(pool: &[Game], max_size: usize) -> Vec<Vec<Game>> {
fn rec(
pool: &[Game],
start: usize,
max_size: usize,
current: &mut Vec<Game>,
out: &mut Vec<Vec<Game>>,
) {
out.push(current.clone());
if current.len() == max_size {
return;
}
for i in start..pool.len() {
current.push(pool[i].clone());
rec(pool, i + 1, max_size, current, out);
current.pop();
}
}
let mut out = Vec::new();
let mut current = Vec::new();
rec(pool, 0, max_size.min(pool.len()), &mut current, &mut out);
out
}
fn sweep_games(pool: &[Game], max_opts: usize) -> Vec<Game> {
let subsets = subsets_up_to(pool, max_opts);
let mut out = Vec::with_capacity(subsets.len() * subsets.len());
for l in &subsets {
for r in &subsets {
out.push(Game::new(l.clone(), r.clone()));
}
}
out
}
fn assert_canonical_string_is_a_value_key(
candidates: &[Game],
reps: &mut std::collections::BTreeMap<String, Game>,
) {
for g in candidates {
let key = g.canonical_string();
if let Some(existing) = reps.get(&key) {
assert!(
g.eq(existing),
"same canonical_string {key} but different value: {} vs {}",
g.display(),
existing.display()
);
} else {
for (other_key, other) in reps.iter() {
assert!(
!g.eq(other),
"different canonical_string ({key} vs {other_key}) but equal value: \
{} vs {}",
g.display(),
other.display()
);
}
reps.insert(key, g.clone());
}
}
}
#[test]
fn canonical_string_is_a_value_key_on_a_bounded_day_le_3_sweep() {
let day1_pool = vec![
Game::zero(),
Game::integer(1),
Game::integer(-1),
Game::star(),
];
let mut reps = std::collections::BTreeMap::new();
let day2_candidates = sweep_games(&day1_pool, day1_pool.len());
assert_canonical_string_is_a_value_key(&day2_candidates, &mut reps);
assert_eq!(
reps.len(),
22,
"day-≤2 census should match the known 22 canonical values (Conway/ONAG)"
);
let day2_pool: Vec<Game> = reps.values().cloned().collect();
let day3_candidates = sweep_games(&day2_pool, 2);
assert_canonical_string_is_a_value_key(&day3_candidates, &mut reps);
}
#[test]
fn number_value_round_trips_through_games() {
use crate::scalar::{Rational, Surreal};
let dy = |n: i128, d: i128| Surreal::from_rational(Rational::new(n, d));
for s in [dy(0, 1), dy(1, 1), dy(-3, 1), dy(1, 2), dy(3, 4), dy(-5, 8)] {
let g = Game::from_surreal(&s).unwrap();
assert_eq!(g.number_value(), Some(s.clone()));
assert!(g.is_canonical()); assert_eq!(s.dyadic_birthday(), Some(g.birthday()));
}
let g = Game::new(vec![Game::integer(0)], vec![Game::integer(1)]); assert_eq!(g.number_value(), Some(dy(1, 2)));
assert_eq!(Game::star().number_value(), None);
assert_eq!(Game::up().number_value(), None);
assert_eq!(Game::switch(1, -1).number_value(), None);
}
}