//! Canonical form of a short game use crate::{ display, macros::if_chain, nom_utils::{impl_from_str_via_nom, lexeme}, numeric::dyadic_rational_number::DyadicRationalNumber, numeric::nimber::Nimber, numeric::rational::Rational, short::partizan::thermograph::Thermograph, short::partizan::trajectory::Trajectory, }; use auto_ops::impl_op_ex; use nom::{ branch::alt, character::complete::{char, one_of, u32}, error::ErrorKind, multi::separated_list0, }; use std::{ cmp::Ordering, fmt::{self, Display, Write}, hash::Hash, }; /// A number-up-star game position that is a sum of a number, up and, nimber. #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Nus { number: DyadicRationalNumber, up_multiple: i32, nimber: Nimber, } impl Nus { /// Create new number-up-start sum #[inline] pub const fn new(number: DyadicRationalNumber, up_multiple: i32, nimber: Nimber) -> Self { Self { number, up_multiple, nimber, } } /// Create new number-up-star game equal to an integer. #[inline] pub const fn new_integer(integer: i64) -> Self { Self::new( DyadicRationalNumber::new_integer(integer), 0, Nimber::new(0), ) } /// Create new number-up-star game equal to an rational. #[inline] pub const fn new_number(number: DyadicRationalNumber) -> Self { Self::new(number, 0, Nimber::new(0)) } /// Create new number-up-star game equal to an rational. #[inline] pub const fn new_nimber(nimber: Nimber) -> Self { Self::new(DyadicRationalNumber::new_integer(0), 0, nimber) } /// Get number part of the NUS sum #[inline] pub const fn number(self) -> DyadicRationalNumber { self.number } /// Get up/down part of the NUS sum. Positive for up, negative for down. #[inline] pub const fn up_multiple(self) -> i32 { self.up_multiple } /// Get nimber part of the NUS sum #[inline] pub const fn nimber(self) -> Nimber { self.nimber } /// Check if the game has only number part (i.e. up multiple and nimber are zero). #[inline] pub fn is_number(self) -> bool { self.up_multiple() == 0 && self.nimber() == Nimber::from(0) } /// Check if the game has only integer number part #[inline] pub fn is_integer(self) -> bool { self.is_number() && self.number().to_integer().is_some() } /// Check if the game is a nimber. #[inline] pub fn is_nimber(self) -> bool { self.number() == DyadicRationalNumber::from(0) && self.up_multiple() == 0 } fn to_moves(self) -> Moves { // Case: Just a number if self.is_number() { if self.number() == DyadicRationalNumber::from(0) { return Moves { left: vec![], right: vec![], }; } if let Some(integer) = self.number().to_integer() { let sign = if integer >= 0 { 1 } else { -1 }; let prev = CanonicalForm::new_nus(Self::new_integer(integer - sign)); if integer >= 0 { return Moves { left: vec![prev], right: vec![], }; } else if sign < 0 { return Moves { left: vec![], right: vec![prev], }; } } else { let rational = self.number(); let left_move = CanonicalForm::new_nus(Self::new_number(rational.step(-1))); let right_move = CanonicalForm::new_nus(Self::new_number(rational.step(1))); return Moves { left: vec![left_move], right: vec![right_move], }; } } // Case: number + nimber but no up/down if self.up_multiple() == 0 { let rational = self.number(); let nimber = self.nimber(); let mut moves = Moves::empty(); for i in 0..nimber.value() { let new_nus = Self { number: rational, up_multiple: 0, nimber: Nimber::from(i), }; moves.left.push(CanonicalForm::new_nus(new_nus)); moves.right.push(CanonicalForm::new_nus(new_nus)); } return moves; } // Case: number-up-star let number_move = Self::new_number(self.number()); let sign = if self.up_multiple() >= 0 { 1 } else { -1 }; let prev_up = self.up_multiple() - sign; let up_parity: u32 = (self.up_multiple() & 1) as u32; let prev_nimber = self.nimber().value() ^ up_parity ^ (prev_up as u32 & 1); let moves; if self.up_multiple() == 1 && self.nimber() == Nimber::from(1) { // Special case: n^* let star_move = CanonicalForm::new_nus(Self { number: self.number(), up_multiple: 0, nimber: Nimber::from(1), }); moves = Moves { left: vec![CanonicalForm::new_nus(number_move), star_move], right: vec![CanonicalForm::new_nus(number_move)], }; } else if self.up_multiple() == -1 && self.nimber() == Nimber::from(1) { // Special case: nv* let star_move = CanonicalForm::new_nus(Self { number: self.number(), up_multiple: 0, nimber: Nimber::from(1), }); moves = Moves { left: vec![CanonicalForm::new_nus(number_move)], right: vec![CanonicalForm::new_nus(number_move), star_move], }; } else if self.up_multiple() > 0 { let prev_nus = CanonicalForm::new_nus(Self { number: self.number(), up_multiple: prev_up, nimber: Nimber::from(prev_nimber), }); moves = Moves { left: vec![CanonicalForm::new_nus(number_move)], right: vec![prev_nus], }; } else { let prev_nus = CanonicalForm::new_nus(Self { number: self.number(), up_multiple: prev_up, nimber: Nimber::from(prev_nimber), }); moves = Moves { left: vec![prev_nus], right: vec![CanonicalForm::new_nus(number_move)], }; } moves } /// Parse nus from string, using notation without pluses between number, up, and star components /// /// Pattern: `\d*([v^]\d*)?(\*\d*)` #[cfg_attr(feature = "cargo-clippy", allow(clippy::missing_errors_doc))] pub fn parse(input: &str) -> nom::IResult<&str, Self> { let full_input = input; // This flag is set if we explicitly parse a number, rather than set it to zero if // it is omitted. It makes expressions like `*` a valid input, however it also makes // empty input parse to a zero game, which is undesired. We handle that case explicitly. let parsed_number: bool; let (input, number) = if let Ok((input, number)) = lexeme(DyadicRationalNumber::parse)(input) { parsed_number = true; (input, number) } else { parsed_number = false; (input, DyadicRationalNumber::from(0)) }; let (input, up_multiple) = match lexeme(one_of::<_, _, (&str, ErrorKind)>("^v"))(input) { Ok((input, chr)) => { let (input, up_multiple) = lexeme(u32::<_, (&str, ErrorKind)>)(input).unwrap_or((input, 1)); ( input, if chr == 'v' { -(up_multiple as i32) } else { up_multiple as i32 }, ) } Err(_) => (input, 0), }; let (input, star_multiple) = match lexeme(char::<_, (&str, ErrorKind)>('*'))(input) { Ok((input, _)) => lexeme(u32::<_, (&str, ErrorKind)>)(input).unwrap_or((input, 1)), Err(_) => (input, 0), }; let nus = Self { number, up_multiple, nimber: Nimber::from(star_multiple), }; if nus == Self::new_integer(0) && !parsed_number { return Err(nom::Err::Error(nom::error::Error::new( full_input, ErrorKind::Fail, ))); } Ok((input, nus)) } } impl_from_str_via_nom!(Nus); impl_op_ex!(+|lhs: &Nus, rhs: &Nus| -> Nus { Nus { number: lhs.number() + rhs.number(), up_multiple: lhs.up_multiple() + rhs.up_multiple(), nimber: lhs.nimber() + rhs.nimber(), } }); impl_op_ex!(-|lhs: &Nus| -> Nus { Nus { number: -lhs.number(), up_multiple: -lhs.up_multiple(), nimber: lhs.nimber(), // Nimber is its own negative } }); impl Display for Nus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if self.number() == DyadicRationalNumber::from(0) && self.up_multiple() == 0 && self.nimber() == Nimber::from(0) { write!(f, "0")?; return Ok(()); } if self.number() != DyadicRationalNumber::from(0) { write!(f, "{}", self.number())?; } if self.up_multiple() == 1 { write!(f, "^")?; } else if self.up_multiple() == -1 { write!(f, "v")?; } else if self.up_multiple() > 0 { write!(f, "^{}", self.up_multiple())?; } else if self.up_multiple() < 0 { write!(f, "v{}", self.up_multiple().abs())?; } if self.nimber() != Nimber::from(0) { write!(f, "{}", self.nimber())?; } Ok(()) } } /// Left and Right moves from a given position #[derive(Debug, Hash, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Moves { /// Left player's moves pub left: Vec, /// Right player's moves pub right: Vec, } impl PartialOrd for Moves { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Moves { fn cmp(&self, other: &Self) -> Ordering { let left = self .left .iter() .map(|cf| &cf.inner) .cmp(other.left.iter().map(|cf| &cf.inner)); if left.is_eq() { self.right .iter() .map(|cf| &cf.inner) .cmp(other.right.iter().map(|cf| &cf.inner)) } else { left } } } impl Moves { #[inline] const fn empty() -> Self { Self { left: vec![], right: vec![], } } #[inline] fn eliminate_duplicates(&mut self) { self.left.sort_by(|lhs, rhs| lhs.inner.cmp(&rhs.inner)); self.left.dedup_by(|lhs, rhs| lhs.inner == rhs.inner); self.right.sort_by(|lhs, rhs| lhs.inner.cmp(&rhs.inner)); self.right.dedup_by(|lhs, rhs| lhs.inner == rhs.inner); } /// Construct a canoical form of arbitrary moves. /// It is an alias of [`CanonicalForm::new_from_moves`] #[inline] pub fn canonical_form(self) -> CanonicalForm { CanonicalForm::new_from_moves(self) } /// Try converting moves to NUS. Returns [None] if moves do not form a NUS // Macro expands to loads of ifs #[cfg_attr(feature = "cargo-clippy", allow(clippy::cognitive_complexity))] pub fn to_nus(&self) -> Option { let mut result = Nus::new_integer(0); let num_lo = self.left.len(); let num_ro = self.right.len(); if_chain! { if num_lo == 0; if num_ro == 0; then { // Case: {|} // No left or right moves so the game is 0 result.number = DyadicRationalNumber::from(0); result.up_multiple = 0; result.nimber = Nimber::from(0); }; if num_lo == 0; then { // Case: n-1 = {|n} // We assume that entry is normalized, no left moves, thus there must be only one // right entry that's a number debug_assert!(num_ro == 1, "Entry not normalized"); result.number = self.right[0].to_nus_unchecked().number() - DyadicRationalNumber::from(1); result.up_multiple = 0; result.nimber = Nimber::from(0); }; if num_ro == 0; then { // Case: n+1 = {n|} // We assume that entry is normalized, no left moves, thus there must be only one // right entry that's a number debug_assert!(num_lo == 1, "Entry not normalized"); result.number = self.left[0].to_nus_unchecked().number() + DyadicRationalNumber::from(1); result.up_multiple = 0; result.nimber = Nimber::from(0); }; if let [left_move] = &self.left[..]; if let [right_move] = &self.right[..]; if let Some(left_number) = left_move.to_number(); if let Some(right_number) = right_move.to_number(); if left_number < right_number; then { // Case: {n|m}, n < m // We're a number but not an integer. Conveniently, since the option lists are // canonicalized, the value of this game is the mean of its left & right moves. result.number = DyadicRationalNumber::mean(&left_number, &right_number); result.up_multiple = 0; result.nimber = Nimber::from(0); }; if let [left_move1, left_move2] = &self.left[..]; if let [right_move] = &self.right[..]; if let Some(left_number) = left_move1.to_number(); if left_move1 == right_move; if let Some(left_nus) = left_move2.to_nus(); if left_number == left_nus.number(); if left_nus.up_multiple() == 0; if left_nus.nimber() == Nimber::new(1); then { // Case: {n,n*|n} result.number = left_number; result.up_multiple = 1; result.nimber = Nimber::from(1); }; if let [left_move] = &self.left[..]; if let [right_move1, right_move2] = &self.right[..]; if let Some(right_number) = right_move1.to_number(); if left_move == right_move1; if let Some(right_nus) = right_move2.to_nus(); if right_number == right_nus.number(); if right_nus.up_multiple() == 0; if right_nus.nimber() == Nimber::new(1); then { // Inverse of the previous one result.number = right_number; result.up_multiple = -1; result.nimber = Nimber::from(1); }; if let [left_move] = &self.left[..]; if let [right_move] = &self.right[..]; if let Some(left_number) = left_move.to_number(); if let Some(right_nus) = right_move.to_nus(); if !right_nus.is_number(); if left_number == right_nus.number(); if right_nus.up_multiple() >= 0; then { // Case: n + {0|G}, G is a number-up-star of up multiple >= 0 result.number = right_nus.number(); result.up_multiple = right_nus.up_multiple() + 1; result.nimber = right_nus.nimber() + Nimber::from(1); }; if let [left_move] = &self.left[..]; if let [right_move] = &self.right[..]; if let Some(left_nus) = left_move.to_nus(); if let Some(right_number) = right_move.to_number(); if !left_nus.is_number(); if right_number == left_nus.number(); if left_nus.up_multiple() <= 0; then { // Inverse of the previous one result.number = left_nus.number(); result.up_multiple = left_nus.up_multiple() - 1; result.nimber = left_nus.nimber() + Nimber::from(1); }; if num_lo >= 1; if num_lo == num_ro; if let Some(left_number) = self.left[0].to_number(); if self.left[0] == self.right[0]; then { // Case: n + *k // If doesn't hold then it's not a NUS for i in 0..num_lo { let l = &self.left[i]; let r = &self.right[i]; if l != r || !l.is_number_up_star() || l.to_nus_unchecked().number() != r.to_nus_unchecked().number() { return None; } if l.to_nus_unchecked().up_multiple() != 0 || l.to_nus_unchecked().nimber().value() != (i as u32) { return None; } } // It's a nimber result.number = left_number; result.up_multiple = 0; result.nimber = Nimber::from(num_lo as u32); }; else { return None; }; }; Some(result) } // TODO: Rewrite it to work on mutable vec and not clone fn eliminate_dominated_moves( moves: &[CanonicalForm], eliminate_smaller_moves: bool, ) -> Vec { let mut moves: Vec> = moves.iter().cloned().map(Some).collect(); 'outer: for i in 0..moves.len() { 'inner: for j in 0..i { let Some(move_i) = &moves[i] else { continue 'outer; }; let Some(move_j) = &moves[j] else { continue 'inner; }; // Split from ifs because borrow checker is sad let remove_i = (eliminate_smaller_moves && move_i <= move_j) || (!eliminate_smaller_moves && move_j <= move_i); let remove_j = (eliminate_smaller_moves && move_j <= move_i) || (!eliminate_smaller_moves && move_i <= move_j); if remove_i { moves[i] = None; } if remove_j { moves[j] = None; } } } moves.iter().flatten().cloned().collect() } /// Return false if `H <= GL` for some left option `GL` of `G` or `HR <= G` for some right /// option `HR` of `H`. Otherwise return true. fn leq_arrays( game: &CanonicalForm, left_moves: &[Option], right_moves: &[Option], ) -> bool { for r_opt in right_moves.iter().flatten() { if r_opt <= game { return false; } } let game_moves = game.to_moves(); for l_move in &game_moves.left { if Self::geq_arrays(l_move, left_moves, right_moves) { return false; } } true } fn geq_arrays( game: &CanonicalForm, left_moves: &[Option], right_moves: &[Option], ) -> bool { for l_opt in left_moves.iter().flatten() { if game <= l_opt { return false; } } let game_moves = game.to_moves(); for r_move in &game_moves.right { if Self::leq_arrays(r_move, left_moves, right_moves) { return false; } } true } fn bypass_reversible_moves_l(&self) -> Self { let mut i: i64 = 0; let mut left_moves: Vec> = self.left.iter().cloned().map(Some).collect(); let right_moves: Vec> = self.right.iter().cloned().map(Some).collect(); loop { if (i as usize) >= left_moves.len() { break; } let g_l = match &left_moves[i as usize] { None => { i += 1; continue; } Some(g) => g.clone(), }; for g_lr in g_l.to_moves().right { if Self::leq_arrays(&g_lr, &left_moves, &right_moves) { let g_lr_moves = g_lr.to_moves(); let mut new_left_moves: Vec> = vec![None; left_moves.len() + g_lr_moves.left.len() - 1]; new_left_moves[..(i as usize)].clone_from_slice(&left_moves[..(i as usize)]); new_left_moves[(i as usize)..(left_moves.len() - 1)] .clone_from_slice(&left_moves[(i as usize + 1)..]); for (k, g_lrl) in g_lr_moves.left.iter().enumerate() { if left_moves.contains(&Some(g_lrl.clone())) { new_left_moves[left_moves.len() + k - 1] = None; } else { new_left_moves[left_moves.len() + k - 1] = Some(g_lrl.clone()); } } left_moves = new_left_moves; i -= 1; break; } } i += 1; } Self { left: left_moves.iter().flatten().cloned().collect(), right: self.right.clone(), } } fn bypass_reversible_moves_r(&self) -> Self { let mut i: i64 = 0; let left_moves: Vec> = self.left.iter().cloned().map(Some).collect(); let mut right_moves: Vec> = self.right.iter().cloned().map(Some).collect(); loop { if (i as usize) >= right_moves.len() { break; } let g_r = match &right_moves[i as usize] { None => { i += 1; continue; } Some(game) => game.clone(), }; for g_rl in g_r.to_moves().left { if Self::geq_arrays(&g_rl, &left_moves, &right_moves) { let g_rl_moves = g_rl.to_moves(); let mut new_right_moves: Vec> = vec![None; right_moves.len() + g_rl_moves.right.len() - 1]; new_right_moves[..(i as usize)].clone_from_slice(&right_moves[..(i as usize)]); new_right_moves[(i as usize)..(right_moves.len() - 1)] .clone_from_slice(&right_moves[(i as usize + 1)..]); for (k, g_rlr) in g_rl_moves.right.iter().enumerate() { if right_moves.contains(&Some(g_rlr.clone())) { new_right_moves[right_moves.len() + k - 1] = None; } else { new_right_moves[right_moves.len() + k - 1] = Some(g_rlr.clone()); } } right_moves = new_right_moves; i -= 1; break; } } i += 1; } Self { left: self.left.clone(), right: right_moves.iter().flatten().cloned().collect(), } } fn canonicalize(&self) -> Self { let moves = self.bypass_reversible_moves_l(); let moves = moves.bypass_reversible_moves_r(); let left = Self::eliminate_dominated_moves(&moves.left, true); let right = Self::eliminate_dominated_moves(&moves.right, false); Self { left, right } } fn thermograph(&self) -> Thermograph { let mut left_scaffold = Trajectory::new_constant(Rational::NegativeInfinity); let mut right_scaffold = Trajectory::new_constant(Rational::PositiveInfinity); for left_move in &self.left { left_scaffold = left_scaffold.max(&CanonicalForm::thermograph(left_move).right_wall); } for right_move in &self.right { right_scaffold = right_scaffold.min(&CanonicalForm::thermograph(right_move).left_wall); } left_scaffold.tilt(Rational::from(-1)); right_scaffold.tilt(Rational::from(1)); Thermograph::thermographic_intersection(left_scaffold, right_scaffold) } /// Print moves with NUS unwrapped using `{G^L | G^R}` notation #[cfg_attr(feature = "cargo-clippy", allow(clippy::missing_errors_doc))] pub fn print_deep(&self, f: &mut impl Write) -> fmt::Result { display::braces(f, |f| { for (idx, l) in self.left.iter().enumerate() { if idx != 0 { write!(f, ",")?; } Self::print_deep(&l.to_moves(), f)?; } write!(f, "|")?; for (idx, r) in self.right.iter().enumerate() { if idx != 0 { write!(f, ",")?; } Self::print_deep(&r.to_moves(), f)?; } Ok(()) }) } /// Print moves to string with NUS unwrapped using `{G^L | G^R}` notation // Write to `String` never panics #[cfg_attr(feature = "cargo-clippy", allow(clippy::missing_panics_doc))] pub fn print_deep_to_str(&self) -> String { let mut buf = String::new(); Self::print_deep(self, &mut buf).unwrap(); buf } /// Parse comma-separated games, ie. the underlined part: /// /// `{a,b,...|c,d,...}` /// /// ` ^^^^^^^` fn parse_list(input: &str) -> nom::IResult<&str, Vec> { separated_list0(lexeme(nom::bytes::complete::tag(",")), |input| { CanonicalForm::parse(input) })(input) } /// Parse game using `{a,b,...|c,d,...}` notation fn parse(input: &str) -> nom::IResult<&str, Self> { let (input, _) = lexeme(char('{'))(input)?; let (input, left) = Self::parse_list(input)?; let (input, _) = lexeme(char('|'))(input)?; let (input, right) = Self::parse_list(input)?; let (input, _) = lexeme(char('}'))(input)?; let moves = Self { left, right }; Ok((input, moves)) } } impl Display for Moves { /// Print moves using `{G^L | G^R}` notation fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { display::braces(f, |f| { display::commas(f, &self.left)?; write!(f, "|")?; display::commas(f, &self.right) }) } } impl_from_str_via_nom!(Moves); /// Canonical game form /// /// Note that ordering is defined structurally for the sake of data structures. For proper partial /// ordering see instance for [`CanonicalForm`]. #[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] enum CanonicalFormInner { /// Number Up Star sum Nus(Nus), /// Not a NUS - list of left/right moves Moves(Moves), } /// Canonical game form #[repr(transparent)] #[derive(Debug, Hash, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct CanonicalForm { inner: CanonicalFormInner, } impl CanonicalForm { /// Construct NUS with only integer #[inline] pub const fn new_integer(integer: i64) -> Self { Self::new_nus(Nus::new_integer(integer)) } /// Construct NUS with only dyadic rational #[inline] pub const fn new_dyadic(dyadic: DyadicRationalNumber) -> Self { Self::new_nus(Nus::new_number(dyadic)) } /// Construct NUS with only nimber #[inline] pub const fn new_nimber(number: DyadicRationalNumber, nimber: Nimber) -> Self { Self::new_nus(Nus { number, up_multiple: 0, nimber, }) } /// Construct NUS #[inline] #[must_use] pub const fn new_nus(nus: Nus) -> Self { Self::from_inner(CanonicalFormInner::Nus(nus)) } /// Construct negative.0 of a game. Alias for negation [`-`] operator #[must_use] pub fn construct_negative(&self) -> Self { match &self.inner { CanonicalFormInner::Nus(nus) => Self::new_nus(-nus), CanonicalFormInner::Moves(moves) => { let new_left_moves = moves .left .iter() .map(Self::construct_negative) .collect::>(); let new_right_moves = moves .right .iter() .map(Self::construct_negative) .collect::>(); let new_moves = Moves { left: new_left_moves, right: new_right_moves, }; Self::construct_from_canonical_moves(new_moves) } } } /// Construct a sum of two games. Alias for [`+`] operator pub fn construct_sum(g: &Self, h: &Self) -> Self { if let (CanonicalFormInner::Nus(g_nus), CanonicalFormInner::Nus(h_nus)) = (&g.inner, &h.inner) { return Self::new_nus(g_nus + h_nus); } // We want to return { GL+H, G+HL | GR+H, G+HR } // By the number translation theorem let mut moves = Moves::empty(); if !g.is_number() { let g_moves = g.to_moves(); for g_l in &g_moves.left { moves.left.push(Self::construct_sum(g_l, h)); } for g_r in &g_moves.right { moves.right.push(Self::construct_sum(g_r, h)); } } if !h.is_number() { let h_moves = h.to_moves(); for h_l in &h_moves.left { moves.left.push(Self::construct_sum(g, h_l)); } for h_r in &h_moves.right { moves.right.push(Self::construct_sum(g, h_r)); } } Self::new_from_moves(moves) } /// VERY INTERNAL fn construct_from_canonical_moves(mut moves: Moves) -> Self { moves.left.sort_by(|lhs, rhs| lhs.inner.cmp(&rhs.inner)); moves.right.sort_by(|lhs, rhs| lhs.inner.cmp(&rhs.inner)); if let Some(nus) = moves.to_nus() { return Self::new_nus(nus); } // Game is not a nus Self::from_inner(CanonicalFormInner::Moves(moves)) } /// Safe function to construct a game from possible moves pub fn new_from_moves(mut moves: Moves) -> Self { moves.eliminate_duplicates(); moves = moves.canonicalize(); Self::construct_from_canonical_moves(moves) } #[inline] const fn from_inner(inner: CanonicalFormInner) -> Self { Self { inner } } /// Get left and right moves from a canonical form pub fn to_moves(&self) -> Moves { match &self.inner { CanonicalFormInner::Nus(nus) => nus.to_moves(), CanonicalFormInner::Moves(moves) => moves.clone(), } } /// Check if game is a Number Up Star sum #[inline] pub const fn is_number_up_star(&self) -> bool { matches!(self.inner, CanonicalFormInner::Nus(_)) } /// Check if a game is only a number #[inline] pub fn is_number(&self) -> bool { matches!(self.inner, CanonicalFormInner::Nus(nus) if nus.is_number()) } /// Check if a game is only a nimber #[inline] pub fn is_nimber(&self) -> bool { matches!(self.inner, CanonicalFormInner::Nus(nus) if nus.is_nimber()) } /// Convert game to NUS if it is a NUS #[inline] pub const fn to_nus(&self) -> Option { match self.inner { CanonicalFormInner::Nus(nus) => Some(nus), // Don't call Moves::to_nus here, because (a) it's already canonical and (b) // it calls here. CanonicalFormInner::Moves(_) => None, } } #[inline] fn to_nus_unchecked(&self) -> Nus { self.to_nus().expect("Not a nus") } /// Convert game to number if it is only a number (i.e. [`Self::is_number`]) #[inline] pub fn to_number(&self) -> Option { self.to_nus() .and_then(|nus| nus.is_number().then_some(nus.number())) } /// Less than or equals comparison on two games pub fn leq(lhs_game: &Self, rhs_game: &Self) -> bool { // NOTE: There is a possible optimization. // Lessons in Play: Lemma 5.35: Let x be a number and G and H be games. If G.right_stop() > x // then G > x. If RS(G) > LS(H) then G > H. if lhs_game == rhs_game { return true; } if let (Some(lhs_nus), Some(rhs_nus)) = (&lhs_game.to_nus(), &rhs_game.to_nus()) { match lhs_nus.number().cmp(&rhs_nus.number()) { Ordering::Less => return true, Ordering::Greater => return false, Ordering::Equal => { if lhs_nus.up_multiple() < rhs_nus.up_multiple() - 1 { return true; } else if lhs_nus.up_multiple() < rhs_nus.up_multiple() { return (lhs_nus.nimber() + rhs_nus.nimber()) != Nimber::from(1); } return false; } } } if !lhs_game.is_number() { let lhs_game_moves = lhs_game.to_moves(); for lhs_l in &lhs_game_moves.left { if Self::leq(rhs_game, lhs_l) { return false; } } } if !rhs_game.is_number() { let rhs_game_moves = rhs_game.to_moves(); for rhs_r in &rhs_game_moves.right { if Self::leq(rhs_r, lhs_game) { return false; } } } true } /// Calculate temperature of the game. Avoids computing a thermograph is game is a NUS #[cfg_attr(feature = "cargo-clippy", allow(clippy::missing_panics_doc))] pub fn temperature(&self) -> DyadicRationalNumber { match self.inner { CanonicalFormInner::Nus(nus) => { if nus.is_number() { // It's a number k/2^n, so the temperature is -1/2^n DyadicRationalNumber::new(-1, nus.number().denominator_exponent()) } else { // It's a number plus a nonzero infinitesimal, thus the temperature is 0 DyadicRationalNumber::from(0) } } CanonicalFormInner::Moves(ref moves) => moves.thermograph().temperature(), } } /// Construct a thermograph of a game, using thermographic intersection of /// left and right scaffolds pub fn thermograph(&self) -> Thermograph { match self.inner { CanonicalFormInner::Moves(ref moves) => moves.thermograph(), CanonicalFormInner::Nus(nus) => { if let Some(nus_integer) = nus.number().to_integer() { if nus.is_number() { return Thermograph::with_mast(Rational::new(nus_integer, 1)); } } if nus.up_multiple() == 0 || (nus.nimber() == Nimber::from(1) && nus.up_multiple().abs() == 1) { // This looks like 0 or * (depending on whether nimberPart is 0 or 1). let new_game = Self::new_nus(Nus { number: nus.number(), up_multiple: 0, nimber: Nimber::from(nus.nimber().value().cmp(&0) as u32), // signum(nus.nimber) }); let new_game_moves = new_game.to_moves(); new_game_moves.thermograph() } else { let new_game = Self::new_nus(Nus { number: nus.number(), up_multiple: nus.up_multiple().cmp(&0) as i32, // signum(nus.up_multiple) nimber: Nimber::from(0), }); let new_game_moves = new_game.to_moves(); new_game_moves.thermograph() } } } } /// The number reached when Left plays first. pub fn left_stop(&self) -> DyadicRationalNumber { if let Some(number) = self.to_number() { return number; } self.to_moves() .left .iter() .map(Self::right_stop) .max() .expect("Not a number so must have moves") } /// The number reached when Right plays first. pub fn right_stop(&self) -> DyadicRationalNumber { if let Some(number) = self.to_number() { return number; } self.to_moves() .right .iter() .map(Self::left_stop) .max() .expect("Not a number so must have moves") } /// Confusion interval is the region between Left and Right stops pub fn confusion_interval(&self) -> (DyadicRationalNumber, DyadicRationalNumber) { (self.left_stop(), self.right_stop()) } /// Compute the mean value of the position /// /// Mean value is the result of cooling a position by value greater than temperature pub fn mean(&self) -> DyadicRationalNumber { match self.inner { CanonicalFormInner::Nus(nus) => nus.number(), CanonicalFormInner::Moves(ref moves) => { let mast = moves.thermograph().get_mast(); DyadicRationalNumber::from_rational(mast) .expect("Thermograph mast to have a finite dyadic value") } } } /// Cool the position by `temperature` /// /// Position `G` cooled by `t` is `G_t = {G^L_t - t | G^R_t + t}` unless there exists a /// temperature `t' < t` for which `G_t'` is infinitesimally close to a number #[must_use] pub fn cool(&self, temperature: DyadicRationalNumber) -> Self { if let Some(nus) = self.to_nus() { if nus.is_integer() { return self.clone(); } } if self.temperature() < temperature { return Self::new_dyadic(self.mean()); } let temperature_game = Self::new_dyadic(temperature); let moves = self.to_moves(); let mut new_left_moves = Vec::with_capacity(moves.left.len()); for left_move in moves.left { new_left_moves.push(left_move.cool(temperature) - &temperature_game); } let mut new_right_moves = Vec::with_capacity(moves.right.len()); for right_move in moves.right { new_right_moves.push(right_move.cool(temperature) + &temperature_game); } let new_moves = Moves { left: new_left_moves, right: new_right_moves, }; new_moves.canonical_form() } /// Heat position by given `temperature`. /// /// Heating is the inverse of cooling, defined as `\int^t G = G` if `G` is a number, or /// `\int^t G = {\int^t G^L + t | \int^t G^R - t}` otherwise #[must_use] pub fn heat(&self, temperature: DyadicRationalNumber) -> Self { if let Some(nus) = self.to_nus() { if nus.is_number() { return self.clone(); } } let temperature_game = Self::new_dyadic(temperature); let moves = self.to_moves(); let mut new_left_moves = Vec::with_capacity(moves.left.len()); for left_move in moves.left { new_left_moves.push(left_move.heat(temperature) + &temperature_game); } let mut new_right_moves = Vec::with_capacity(moves.right.len()); for right_move in moves.right { new_right_moves.push(right_move.heat(temperature) - &temperature_game); } let new_moves = Moves { left: new_left_moves, right: new_right_moves, }; new_moves.canonical_form() } /// A remote star of game `g` is a nimber `*N` if no position of `g` including `g` has value `N*` #[must_use] #[cfg_attr(feature = "cargo-clippy", allow(clippy::or_fun_call))] pub fn far_star(&self) -> Nimber { if let CanonicalFormInner::Nus(ref nus) = self.inner { if nus.is_nimber() { return Nimber::from(nus.nimber().value() + 1); } }; let moves = self.to_moves(); moves .left .iter() .chain(moves.right.iter()) .map(Self::far_star) .max() .unwrap_or(Nimber::from(1)) } // FIXME: Handle cases when atomic weight does not exist /// Atmoic weight of a position, sometimes called "uppitiness" #[must_use] pub fn atomic_weight(&self) -> Self { match self.inner { CanonicalFormInner::Nus(nus) => Self::new_integer(nus.up_multiple() as i64), CanonicalFormInner::Moves(ref moves) => { let new_moves = Moves { left: moves .left .iter() .map(|left_move| left_move.atomic_weight() - Self::new_integer(2)) .collect::>(), right: moves .right .iter() .map(|right_move| right_move.atomic_weight() + Self::new_integer(2)) .collect::>(), }; let new_game = Self::new_from_moves(new_moves.clone()); let CanonicalFormInner::Nus(new_nus) = new_game.inner else { return new_game; }; if !new_nus.is_integer() { return new_game; } let far_star = Self::new_nimber(DyadicRationalNumber::from(0), self.far_star()); let less_than_far_star = self <= &far_star; let greater_than_far_star = self >= &far_star; if less_than_far_star && !greater_than_far_star { let max_least = new_moves .left .iter() .map(|left_move| { let least = left_move.right_stop().ceil(); if &Self::new_integer(least) <= left_move { least + 1 } else { least } }) .max() .unwrap_or(0); Self::new_integer(max_least) } else if !less_than_far_star && greater_than_far_star { let min_greatest = new_moves .right .iter() .map(|right_move| { let greatest = right_move.left_stop().round(); if right_move <= &Self::new_integer(greatest) { greatest - 1 } else { greatest } }) .min() .unwrap_or(0); Self::new_integer(min_greatest) } else { new_game } } } } /// Parse game using `{a,b,...|c,d,...}` notation #[cfg_attr(feature = "cargo-clippy", allow(clippy::missing_errors_doc))] pub fn parse(input: &str) -> nom::IResult<&str, Self> { alt(( |input| Nus::parse(input).map(|(input, nus)| (input, Self::new_nus(nus))), |input| Moves::parse(input).map(|(input, moves)| (input, Self::new_from_moves(moves))), ))(input) } } impl PartialOrd for CanonicalForm { #[inline] fn partial_cmp(&self, other: &Self) -> Option { if self == other { Some(Ordering::Equal) } else if Self::leq(self, other) { Some(Ordering::Less) } else if Self::leq(other, self) { Some(Ordering::Greater) } else { None } } fn le(&self, other: &Self) -> bool { Self::leq(self, other) } fn ge(&self, other: &Self) -> bool { Self::leq(other, self) } } impl_op_ex!(+|g: &CanonicalForm, h: &CanonicalForm| -> CanonicalForm { CanonicalForm::construct_sum(g, h) }); impl_op_ex!(+=|g: &mut CanonicalForm, h: &CanonicalForm| { *g = CanonicalForm::construct_sum(g, h) }); impl_op_ex!(-|g: &CanonicalForm| -> CanonicalForm { CanonicalForm::construct_negative(g) }); impl_op_ex!(-|g: &CanonicalForm, h: &CanonicalForm| -> CanonicalForm { CanonicalForm::construct_sum(g, &CanonicalForm::construct_negative(h)) }); impl_op_ex!(-=|g: &mut CanonicalForm, h: &CanonicalForm| { *g = CanonicalForm::construct_sum(g, &CanonicalForm::construct_negative(h)); }); impl Display for CanonicalForm { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.inner { CanonicalFormInner::Nus(nus) => nus.fmt(f), CanonicalFormInner::Moves(moves) => moves.fmt(f), } } } impl_from_str_via_nom!(CanonicalForm); #[cfg(test)] mod tests { use super::*; use quickcheck::{Arbitrary, Gen, QuickCheck}; use std::{ops::Neg, str::FromStr}; macro_rules! parse_nus_roundtrip { ($inp: expr) => { assert_eq!( &format!( "{}", Nus::from_str($inp).expect(&format!("Could not parse: '{}'", $inp)) ), $inp ); }; } macro_rules! parse_nus_succeed { ($inp: expr) => { let res = Nus::from_str($inp); if let Err(err) = res { eprintln!("Parse error: {}", err); panic!("Parse should succeed"); } }; } macro_rules! parse_nus_fail { ($inp: expr) => { assert!(Nus::from_str($inp).is_err(), "Parse should fail"); }; } #[test] fn parse_nus() { parse_nus_fail!(""); // this shoult NOT parse to 0 parse_nus_fail!("42 foo"); parse_nus_roundtrip!("42"); parse_nus_roundtrip!("1/2"); parse_nus_roundtrip!("-8"); parse_nus_roundtrip!("13^"); parse_nus_roundtrip!("123v"); parse_nus_roundtrip!("13^3"); parse_nus_roundtrip!("123v58"); parse_nus_roundtrip!("13^3*"); parse_nus_roundtrip!("123v58*"); parse_nus_roundtrip!("13^3*8"); parse_nus_roundtrip!("123v58*34"); parse_nus_roundtrip!("-13^3*"); parse_nus_roundtrip!("-123v58*"); parse_nus_succeed!(" 123 v 58 * 43784"); } // TODO: Rewrite with proptest impl Arbitrary for Nus { fn arbitrary(g: &mut quickcheck::Gen) -> Self { macro_rules! arbitrary_mod { ($n: expr, $g: expr) => {{ let res: i64 = Arbitrary::arbitrary($g); res.rem_euclid($n).try_into().unwrap() }}; } Self { number: arbitrary_sign( DyadicRationalNumber::new(arbitrary_mod!(1000, g), arbitrary_mod!(16, g)), g, ), up_multiple: arbitrary_sign(arbitrary_mod!(1000, g), g), nimber: Nimber::new(arbitrary_mod!(1000, g)), } } } #[test] fn nus_moves_nus_roundtrip() { // We really need to stress test it to hit all branches. // Confirmed with // cargo tarpaulin --out html -- short::partizan::canonical_form::tests --nocapture let tests = 250_000; let mut qc = QuickCheck::new() .max_tests(tests) .min_tests_passed(tests) .tests(tests); qc.quickcheck(nus_moves_nus_roundtrip_impl as fn(Nus)); } fn nus_moves_nus_roundtrip_impl(nus: Nus) { let moves = nus.to_moves(); let nus_from_moves = moves.to_nus().expect("Should be a NUS"); assert_eq!(nus, nus_from_moves, "Should be equal"); } fn arbitrary_sign(n: T, g: &mut Gen) -> T where T: Neg, { if Arbitrary::arbitrary(g) { n } else { -n } } #[test] fn constructs_integers() { let eight = CanonicalForm::new_integer(8); assert_eq!(&eight.to_string(), "8"); let eight_moves = eight.to_moves(); assert_eq!(&eight_moves.to_string(), "{7|}"); assert_eq!( &eight_moves.print_deep_to_str(), "{{{{{{{{{|}|}|}|}|}|}|}|}|}" ); let minus_forty_two = CanonicalForm::new_integer(-42); assert_eq!(&minus_forty_two.to_string(), "-42"); } #[test] fn constructs_rationals() { let rational = DyadicRationalNumber::new(3, 4); let three_sixteenth = CanonicalForm::new_dyadic(rational); assert_eq!(&three_sixteenth.to_string(), "3/16"); let duplicate = CanonicalForm::new_dyadic(rational); assert_eq!(three_sixteenth, duplicate); } #[test] fn constructs_nimbers() { let star = CanonicalForm::new_nus(Nus::new_nimber(Nimber::from(1))); assert_eq!(&star.to_string(), "*"); let star_moves = star.to_moves(); assert_eq!(&star_moves.to_string(), "{0|0}"); assert_eq!(&star_moves.print_deep_to_str(), "{{|}|{|}}"); let star_three = CanonicalForm::new_nus(Nus::new_nimber(Nimber::from(3))); assert_eq!(&star_three.to_string(), "*3"); let star_three_moves = star_three.to_moves(); assert_eq!(star_three_moves.to_string(), "{0, *, *2|0, *, *2}"); let one_star_two = CanonicalForm::new_nus(Nus { number: DyadicRationalNumber::from(1), up_multiple: 0, nimber: (Nimber::from(2)), }); assert_eq!(&one_star_two.to_string(), "1*2"); let one_star_two_moves = one_star_two.to_moves(); assert_eq!(&one_star_two_moves.to_string(), "{1, 1*|1, 1*}"); } #[test] fn constructs_up() { let up = CanonicalForm::new_nus(Nus { number: DyadicRationalNumber::from(0), up_multiple: 1, nimber: Nimber::from(0), }); assert_eq!(&up.to_string(), "^"); let up_star = CanonicalForm::new_nus(Nus { number: DyadicRationalNumber::from(0), up_multiple: 1, nimber: Nimber::from(1), }); assert_eq!(&up_star.to_string(), "^*"); let down = CanonicalForm::new_nus(Nus { number: DyadicRationalNumber::from(0), up_multiple: -3, nimber: Nimber::from(0), }); assert_eq!(&down.to_string(), "v3"); } macro_rules! assert_negative_eq { ($inp:expr, $out:expr) => { let inp = CanonicalForm::from_str($inp).unwrap(); assert_eq!((-inp).to_string(), $out); }; } #[test] fn negative() { assert_negative_eq!("0", "0"); assert_negative_eq!("42", "-42"); assert_negative_eq!("-42", "42"); assert_negative_eq!("{^|*}", "{v|*}"); } #[test] fn nimber_is_its_negative() { let star = CanonicalForm::new_nimber(DyadicRationalNumber::from(0), Nimber::from(4)); assert_eq!(&star.to_string(), "*4"); let neg_star = star.construct_negative(); assert_eq!(star, neg_star); } #[test] fn gets_moves() { let down_moves = CanonicalForm::new_nus(Nus::from_str("v").unwrap()).to_moves(); assert_eq!(down_moves.to_string(), "{*|0}"); assert_eq!(&down_moves.print_deep_to_str(), "{{{|}|{|}}|{|}}"); let up_moves = CanonicalForm::new_nus(Nus::from_str("^").unwrap()).to_moves(); assert_eq!(&up_moves.to_string(), "{0|*}"); assert_eq!(up_moves.print_deep_to_str(), "{{|}|{{|}|{|}}}"); let moves = Moves { left: vec![CanonicalForm::new_nus(Nus::from_str("v").unwrap())], right: vec![CanonicalForm::new_nus(Nus::from_str("-2").unwrap())], }; assert_eq!(&moves.to_string(), "{v|-2}"); assert_eq!(&moves.print_deep_to_str(), "{{{{|}|{|}}|{|}}|{|{|{|}}}}"); } #[test] fn simplifies_moves() { let one = CanonicalForm::new_nus(Nus::from_str("1").unwrap()); let star = CanonicalForm::new_nus(Nus::from_str("*").unwrap()); let moves_l = Moves { left: vec![one], right: vec![star], }; let left_id = CanonicalForm::new_from_moves(moves_l); assert_eq!(&left_id.to_string(), "{1|*}"); let weird = Moves { left: vec![CanonicalForm::new_nus(Nus::from_str("1v2*").unwrap())], right: vec![CanonicalForm::new_nus(Nus::from_str("1").unwrap())], }; let weird = CanonicalForm::new_from_moves(weird); assert_eq!(&weird.to_string(), "1v3"); let weird_moves = weird.to_moves(); assert_eq!(&weird_moves.to_string(), "{1v2*|1}"); assert_eq!(&weird_moves.left[0].to_string(), "1v2*"); assert_eq!(&weird_moves.left[0].to_moves().to_string(), "{1v|1}"); assert_eq!( &weird_moves.print_deep_to_str(), "{{{{{{|}|}|{{|}|}}|{{|}|}}|{{|}|}}|{{|}|}}" ); // Another case: let weird_right = Moves { left: vec![CanonicalForm::new_nus(Nus::from_str("^").unwrap())], right: vec![CanonicalForm::new_nus(Nus::from_str("-2").unwrap())], }; let weird_right = CanonicalForm::new_from_moves(weird_right); assert_eq!(&weird_right.to_string(), "{^|-2}"); let weird_right_moves = weird_right.to_moves(); assert_eq!(&weird_right_moves.to_string(), "{^|-2}"); assert_eq!( &weird_right_moves.print_deep_to_str(), "{{{|}|{{|}|{|}}}|{|{|{|}}}}" ); let weird = Moves { left: vec![], right: vec![weird_right], }; assert_ne!( &Moves::print_deep_to_str(&weird.canonicalize()), "{|{{{|}|{{|}|{|}}}|{|{|{|}}}}}" ); assert_eq!(&weird.canonicalize().to_string(), "{|}"); let weird = CanonicalForm::new_from_moves(weird); let weird_moves = weird.to_moves(); assert_eq!(&weird_moves.to_string(), "{|}"); assert_eq!(&weird.to_string(), "0"); } #[test] fn sum_works() { let zero = CanonicalForm::new_integer(0); let one = CanonicalForm::new_integer(1); let one_zero = CanonicalForm::new_from_moves(Moves { left: vec![one.clone()], right: vec![zero.clone()], }); let zero_one = CanonicalForm::new_from_moves(Moves { left: vec![zero], right: vec![one], }); let sum = one_zero + zero_one; assert_eq!(&sum.to_string(), "{3/2|1/2}"); } #[test] fn temp_of_one_minus_one_is_one() { let one = CanonicalForm::new_integer(1); let negative_one = CanonicalForm::new_integer(-1); let moves = Moves { left: vec![one], right: vec![negative_one], }; let g = CanonicalForm::new_from_moves(moves); assert_eq!(g.temperature(), DyadicRationalNumber::from(1)); } #[test] fn parse_games() { macro_rules! test_game_parse { ($inp: expr, $expected: expr) => {{ let g = CanonicalForm::parse($inp).expect("Could not parse").1; assert_eq!($expected, g.to_string()); }}; } test_game_parse!("{|}", "0"); test_game_parse!("{1,2|}", "3"); test_game_parse!("{42|*}", "{42|*}"); test_game_parse!("123", "123"); test_game_parse!("{1/2|2}", "1"); test_game_parse!("{3/4|7/8}", "13/16"); test_game_parse!("{6/8|7/8}", "13/16"); test_game_parse!("{12/16|14/16}", "13/16"); test_game_parse!("{0|2}", "1"); test_game_parse!("{0,*,*2|0,*,*2}", "*3"); } #[test] fn ordering_works() { macro_rules! test_ordering { ($lhs:expr, $rhs:expr, $expected:expr) => { assert_eq!( PartialOrd::partial_cmp( &CanonicalForm::from_str($lhs).unwrap(), &CanonicalForm::from_str($rhs).unwrap() ), $expected ) }; } test_ordering!("0", "*", None); test_ordering!("*", "*", Some(Ordering::Equal)); test_ordering!("*2", "*", None); test_ordering!("*2", "*2", Some(Ordering::Equal)); test_ordering!("*", "*2", None); test_ordering!("1", "2", Some(Ordering::Less)); test_ordering!("-1", "*", Some(Ordering::Less)); test_ordering!("1", "*", Some(Ordering::Greater)); } macro_rules! assert_stops { ($cf:expr, $left:expr, $right:expr) => { let g = CanonicalForm::from_str($cf).unwrap(); let (left_stop, right_stop) = g.confusion_interval(); assert_eq!( left_stop, DyadicRationalNumber::from_str($left).expect("Could not parse left stop"), "Invalid left stop" ); assert_eq!( right_stop, DyadicRationalNumber::from_str($right).expect("Could not parse right stop"), "Invalid right stop" ); assert!( left_stop >= right_stop, "Left stop shold be geq than right stop" ); }; } #[test] fn stops_work() { assert_stops!("{{3|2}|0}", "2", "0"); assert_stops!("v", "0", "0"); assert_stops!("*", "0", "0"); assert_stops!("^", "0", "0"); } macro_rules! assert_cooled { ($cf:expr, $temp:expr, $expected:expr) => { let g = CanonicalForm::from_str($cf).unwrap(); let temp = DyadicRationalNumber::from_str($temp).unwrap(); let cooled = g.cool(temp); assert_eq!( cooled.to_string(), CanonicalForm::from_str($expected).unwrap().to_string() ); }; } #[test] fn cooling_works() { assert_cooled!("{2|-1}", "0", "{2|-1}"); assert_cooled!("{2|-1}", "1/2", "{3/2|-1/2}"); assert_cooled!("{2|-1}", "1", "{1|0}"); assert_cooled!("{2|-1}", "3/2", "1/2*"); assert_cooled!("{2|-1}", "2", "1/2"); assert_cooled!("{2|-1}", "3", "1/2"); assert_cooled!("{2|-1}", "42", "1/2"); } #[test] fn heating_numbers() { let g = CanonicalForm::new_dyadic(DyadicRationalNumber::from(42)); let heated = g.heat(DyadicRationalNumber::from(1)); assert_eq!(g, heated); } #[test] fn cooling_heating_roundtrip() { let g = CanonicalForm::from_str("{2|-1}").unwrap(); let t = DyadicRationalNumber::from_str("3/2").unwrap(); let cooled = g.cool(t); let frozen = g.cool(t + DyadicRationalNumber::from(1)); let particle = &cooled - &frozen; let heated = particle.heat(t); assert_eq!(heated.to_string(), "{3/2|-3/2}"); assert_eq!(g, &frozen + &heated); } macro_rules! assert_atomic_weight_eq { ($inp:expr, $atomic:expr) => { let cf = CanonicalForm::from_str($inp).unwrap(); let atomic = CanonicalForm::from_str($atomic).unwrap(); assert_eq!(cf.atomic_weight().to_string(), atomic.to_string()); }; } #[test] fn atomic_weight() { assert_atomic_weight_eq!("*3", "0"); assert_atomic_weight_eq!("^", "1"); assert_atomic_weight_eq!("v", "-1"); assert_atomic_weight_eq!("v2", "-2"); assert_atomic_weight_eq!("{^2|v}", "1/2"); assert_atomic_weight_eq!("{^2|v2}", "*"); assert_atomic_weight_eq!("{^3|v3}", "{1|-1}"); assert_atomic_weight_eq!("{^2|*}", "1"); assert_atomic_weight_eq!("{^2,{^|*}|*}", "1"); assert_atomic_weight_eq!("{*|v2}", "-1"); } }