use super::engine::{IntegerAlgebra, IntegerMV};
use super::forms::{wrap_binary_code, wrap_quadric_fit, PyBinaryCode, PyQuadricFit};
use super::scalars::{
parse_rational, parse_surreal, wrap_rational, PyOrdinal, PyRational, PySurreal,
};
use crate::clifford::CliffordAlgebra;
use crate::games::{
thermography, AbstractGame, Color, Game, GameClifford, GameExterior, GameRelation, Hackenbush,
LoopyGraph, LoopyNimCertificate, LoopyNimber, LoopyPartizanGraph, LoopyPartizanOutcome,
LoopyValue, LoopyWinner, NimLexicode, NimberGame, NumberGame, Outcome, PartizanOutcome,
Quotient,
};
use crate::scalar::{Integer, Rational, SignExpansion, Surreal};
use pyo3::basic::CompareOp;
use pyo3::exceptions::{PyTypeError, PyValueError};
use pyo3::prelude::*;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::Arc;
fn check_succ_bounds(succ: &[Vec<usize>]) -> PyResult<()> {
let n = succ.len();
for (v, neighbors) in succ.iter().enumerate() {
for &w in neighbors {
if w >= n {
return Err(PyValueError::new_err(format!(
"adjacency list out of range: succ[{v}] contains index {w}, \
but the graph has only {n} positions (0..{n})"
)));
}
}
}
Ok(())
}
fn check_partizan_succ_bounds(left: &[Vec<usize>], right: &[Vec<usize>]) -> PyResult<()> {
if left.len() != right.len() {
return Err(PyValueError::new_err(format!(
"left/right move tables must have the same number of positions: left has {}, right has {}",
left.len(),
right.len()
)));
}
check_succ_bounds(left)?;
check_succ_bounds(right)
}
fn rat_to_py(r: Rational) -> PySurreal {
PySurreal::from_inner(Surreal::from_rational(r))
}
fn wrap_pl(inner: thermography::Pl) -> PyPl {
PyPl { inner }
}
fn wrap_thermograph(inner: thermography::Thermograph) -> PyThermograph {
PyThermograph { inner }
}
#[pyclass(name = "GameRelation", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyGameRelation {
inner: GameRelation,
}
#[pymethods]
impl PyGameRelation {
#[new]
fn new(coeffs: Vec<i128>) -> Self {
PyGameRelation {
inner: GameRelation::new(coeffs),
}
}
#[getter]
fn coeffs(&self) -> Vec<i128> {
self.inner.coeffs.clone()
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
let Ok(rel) = other.extract::<PyRef<'_, PyGameRelation>>() else {
return Ok(matches!(op, CompareOp::Ne));
};
match op {
CompareOp::Eq => Ok(self.inner.coeffs == rel.inner.coeffs),
CompareOp::Ne => Ok(self.inner.coeffs != rel.inner.coeffs),
CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => Err(
PyValueError::new_err("GameRelation only supports equality comparisons"),
),
}
}
fn __repr__(&self) -> String {
format!("GameRelation(coeffs={:?})", self.inner.coeffs)
}
}
fn wrap_game_relation(inner: GameRelation) -> PyGameRelation {
PyGameRelation { inner }
}
#[pyclass(name = "GameRelationCertificate", module = "ogdoad")]
struct PyGameRelationCertificate {
inner: crate::games::GameRelationCertificate,
}
#[pymethods]
impl PyGameRelationCertificate {
#[getter]
fn coeffs(&self) -> Vec<i128> {
self.inner.coeffs.clone()
}
#[getter]
fn value_key(&self) -> String {
self.inner.value_key.clone()
}
#[getter]
fn independent(&self) -> bool {
self.inner.independent
}
fn display(&self) -> String {
self.inner.display()
}
fn __repr__(&self) -> String {
self.inner.display()
}
}
fn wrap_game_relation_certificate(
inner: crate::games::GameRelationCertificate,
) -> PyGameRelationCertificate {
PyGameRelationCertificate { inner }
}
#[pyclass(name = "RelationSearchCertificate", module = "ogdoad")]
struct PyRelationSearchCertificate {
inner: crate::games::RelationSearchCertificate,
}
#[pymethods]
impl PyRelationSearchCertificate {
#[getter]
fn bound(&self) -> i128 {
self.inner.bound
}
#[getter]
fn exhaustive(&self) -> bool {
self.inner.exhaustive
}
#[getter]
fn candidate_count(&self) -> Option<usize> {
self.inner.candidate_count
}
#[getter]
fn relations(&self) -> Vec<PyGameRelationCertificate> {
self.inner
.relations
.iter()
.cloned()
.map(wrap_game_relation_certificate)
.collect()
}
fn display(&self) -> String {
self.inner.display()
}
fn __repr__(&self) -> String {
self.inner.display()
}
}
fn wrap_relation_search_certificate(
inner: crate::games::RelationSearchCertificate,
) -> PyRelationSearchCertificate {
PyRelationSearchCertificate { inner }
}
#[pyclass(name = "Pl", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyPl {
inner: thermography::Pl,
}
#[pymethods]
impl PyPl {
#[staticmethod]
fn constant(value: &Bound<'_, PyAny>) -> PyResult<Self> {
Ok(wrap_pl(thermography::Pl::constant(parse_rational(value)?)))
}
fn points(&self) -> Vec<(PyRational, PyRational)> {
self.inner
.points()
.iter()
.map(|(t, v)| (wrap_rational(t.clone()), wrap_rational(v.clone())))
.collect()
}
fn value_at(&self, t: &Bound<'_, PyAny>) -> PyResult<PyRational> {
Ok(wrap_rational(self.inner.value_at(&parse_rational(t)?)))
}
fn oplus_max(&self, other: &PyPl) -> PyPl {
wrap_pl(self.inner.oplus_max(&other.inner))
}
fn oplus_min(&self, other: &PyPl) -> PyPl {
wrap_pl(self.inner.oplus_min(&other.inner))
}
fn otimes(&self, other: &PyPl) -> PyPl {
wrap_pl(self.inner.otimes(&other.inner))
}
fn __repr__(&self) -> String {
format!("Pl({:?})", self.inner.points())
}
}
#[pyclass(name = "Thermograph", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyThermograph {
inner: thermography::Thermograph,
}
#[pymethods]
impl PyThermograph {
#[getter]
fn mean(&self) -> PyRational {
wrap_rational(self.inner.mean())
}
#[getter]
fn temperature(&self) -> PyRational {
wrap_rational(self.inner.temperature.clone())
}
#[getter]
fn left_wall(&self) -> PyPl {
wrap_pl(self.inner.left_wall.clone())
}
#[getter]
fn right_wall(&self) -> PyPl {
wrap_pl(self.inner.right_wall.clone())
}
fn left_stop(&self) -> PyRational {
wrap_rational(self.inner.left_stop())
}
fn right_stop(&self) -> PyRational {
wrap_rational(self.inner.right_stop())
}
fn cooled_stops(&self, t: &Bound<'_, PyAny>) -> PyResult<(PyRational, PyRational)> {
let (l, r) = self.inner.cooled_stops(&parse_rational(t)?);
Ok((wrap_rational(l), wrap_rational(r)))
}
fn __repr__(&self) -> String {
format!(
"Thermograph(mean={:?}, temperature={:?})",
self.inner.mast, self.inner.temperature
)
}
}
#[pyfunction]
fn nim_mul_mex(x: u128, y: u128) -> u128 {
crate::games::nim_mul_mex(x, y)
}
#[derive(Clone, Copy)]
enum CoinFamily {
Singleton,
Turtles,
}
type CompanionFn = fn(u128) -> Vec<u128>;
impl CoinFamily {
fn companions(self) -> CompanionFn {
match self {
CoinFamily::Singleton => crate::games::singleton_companions,
CoinFamily::Turtles => crate::games::turtles_companions,
}
}
}
fn parse_coin_family(name: &str) -> PyResult<CoinFamily> {
match name.trim().to_ascii_lowercase().as_str() {
"singleton" | "singletons" | "turning-corners" | "turning_corners" | "corners" => {
Ok(CoinFamily::Singleton)
}
"turtles" | "turning-turtles" | "turning_turtles" => Ok(CoinFamily::Turtles),
other => Err(PyValueError::new_err(format!(
"unknown coin-turning family {other:?}; expected 'singleton' or 'turtles'"
))),
}
}
fn check_coin_index(n: u128, label: &str) -> PyResult<()> {
if n >= 128 {
Err(PyValueError::new_err(format!(
"{label} must be < 128 because companion sets are u128 bitmasks"
)))
} else {
Ok(())
}
}
fn lower_coin_mask(n: u128, label: &str) -> PyResult<u128> {
check_coin_index(n, label)?;
Ok(if n == 0 { 0 } else { (1u128 << n) - 1 })
}
fn call_u128_moves(callback: &Bound<'_, PyAny>, pos: u128) -> PyResult<Vec<u128>> {
callback.call1((pos,))?.extract()
}
fn call_usize_moves(callback: &Bound<'_, PyAny>, pos: usize) -> PyResult<Vec<usize>> {
callback.call1((pos,))?.extract()
}
fn grundy_u128_inner(
pos: u128,
moves: &Bound<'_, PyAny>,
memo: &mut HashMap<u128, u128>,
) -> PyResult<u128> {
if let Some(&v) = memo.get(&pos) {
return Ok(v);
}
let nexts = call_u128_moves(moves, pos)?;
let mut values = Vec::with_capacity(nexts.len());
for next in nexts {
values.push(grundy_u128_inner(next, moves, memo)?);
}
let g = crate::games::mex(values);
memo.insert(pos, g);
Ok(g)
}
fn misere_is_n_u128_inner(
pos: u128,
moves: &Bound<'_, PyAny>,
memo: &mut HashMap<u128, bool>,
visiting: &mut HashSet<u128>,
) -> PyResult<Option<bool>> {
if let Some(&v) = memo.get(&pos) {
return Ok(Some(v));
}
if !visiting.insert(pos) {
return Ok(None);
}
let nexts = call_u128_moves(moves, pos)?;
let mut result = nexts.is_empty();
if !result {
for next in nexts {
match misere_is_n_u128_inner(next, moves, memo, visiting)? {
Some(false) => {
result = true;
break;
}
Some(true) => {}
None => {
visiting.remove(&pos);
return Ok(None);
}
}
}
}
visiting.remove(&pos);
memo.insert(pos, result);
Ok(Some(result))
}
fn loopy_succ_from_callback(n: usize, moves: &Bound<'_, PyAny>) -> PyResult<Vec<Vec<usize>>> {
let mut succ = Vec::with_capacity(n);
for v in 0..n {
let nexts = call_usize_moves(moves, v)?;
if nexts.iter().any(|&w| w >= n) {
return Err(PyValueError::new_err(format!(
"move callback for position {v} returned an out-of-range target"
)));
}
succ.push(nexts);
}
Ok(succ)
}
#[pyfunction]
fn coin_companions(kind: &str, n: u128) -> PyResult<Vec<u128>> {
check_coin_index(n, "n")?;
Ok((parse_coin_family(kind)?.companions())(n))
}
#[pyfunction]
fn singleton_companions(n: u128) -> PyResult<Vec<u128>> {
check_coin_index(n, "n")?;
Ok(crate::games::singleton_companions(n))
}
#[pyfunction]
fn turtles_companions(n: u128) -> PyResult<Vec<u128>> {
check_coin_index(n, "n")?;
Ok(crate::games::turtles_companions(n))
}
#[pyfunction]
fn grundy_1d(companions: Bound<'_, PyAny>, n: u128) -> PyResult<u128> {
fn inner(
companions: &Bound<'_, PyAny>,
n: u128,
memo: &mut HashMap<u128, u128>,
) -> PyResult<u128> {
let allowed = lower_coin_mask(n, "n")?;
if let Some(&v) = memo.get(&n) {
return Ok(v);
}
let mut seen = HashSet::new();
for s in call_u128_moves(companions, n)? {
if s & !allowed != 0 {
return Err(PyValueError::new_err(
"companion mask contains a coin that is not strictly lower than n",
));
}
let mut acc = 0u128;
let mut ss = s;
while ss != 0 {
let i = ss.trailing_zeros() as u128;
ss &= ss - 1;
acc ^= inner(companions, i, memo)?;
}
seen.insert(acc);
}
let g = crate::games::mex(seen);
memo.insert(n, g);
Ok(g)
}
check_coin_index(n, "n")?;
inner(&companions, n, &mut HashMap::new())
}
#[pyfunction]
fn coin_turning_grundy(kind: &str, n: u128) -> PyResult<u128> {
check_coin_index(n, "n")?;
let mut memo = HashMap::new();
let companions = parse_coin_family(kind)?.companions();
Ok(crate::games::grundy_1d(&companions, n, &mut memo))
}
#[pyfunction]
fn coin_turning_tartan_grundy(kind_a: &str, kind_b: &str, x: u128, y: u128) -> PyResult<u128> {
check_coin_index(x, "x")?;
check_coin_index(y, "y")?;
let comp_a = parse_coin_family(kind_a)?.companions();
let comp_b = parse_coin_family(kind_b)?.companions();
let mut memo = HashMap::new();
Ok(crate::games::tartan_grundy(
&comp_a, &comp_b, x, y, &mut memo,
))
}
#[pyfunction]
fn tartan_grundy(
comp_a: Bound<'_, PyAny>,
comp_b: Bound<'_, PyAny>,
x: u128,
y: u128,
) -> PyResult<u128> {
fn inner(
comp_a: &Bound<'_, PyAny>,
comp_b: &Bound<'_, PyAny>,
x: u128,
y: u128,
memo: &mut HashMap<(u128, u128), u128>,
) -> PyResult<u128> {
let allowed_x = lower_coin_mask(x, "x")?;
let allowed_y = lower_coin_mask(y, "y")?;
if let Some(&v) = memo.get(&(x, y)) {
return Ok(v);
}
let mut seen = HashSet::new();
for ta in call_u128_moves(comp_a, x)? {
if ta & !allowed_x != 0 {
return Err(PyValueError::new_err(
"row companion mask contains a coin that is not strictly lower than x",
));
}
let acells = ta | (1u128 << x);
for tb in call_u128_moves(comp_b, y)? {
if tb & !allowed_y != 0 {
return Err(PyValueError::new_err(
"column companion mask contains a coin that is not strictly lower than y",
));
}
let bcells = tb | (1u128 << y);
let mut acc = 0u128;
let mut aa = acells;
while aa != 0 {
let a = aa.trailing_zeros() as u128;
aa &= aa - 1;
let mut bb = bcells;
while bb != 0 {
let b = bb.trailing_zeros() as u128;
bb &= bb - 1;
if a != x || b != y {
acc ^= inner(comp_a, comp_b, a, b, memo)?;
}
}
}
seen.insert(acc);
}
}
let g = crate::games::mex(seen);
memo.insert((x, y), g);
Ok(g)
}
check_coin_index(x, "x")?;
check_coin_index(y, "y")?;
inner(&comp_a, &comp_b, x, y, &mut HashMap::new())
}
#[pyfunction]
fn grundy(pos: u128, moves: Bound<'_, PyAny>) -> PyResult<u128> {
grundy_u128_inner(pos, &moves, &mut HashMap::new())
}
#[pyfunction]
fn grundy_graph(succ: Vec<Vec<usize>>) -> PyResult<Vec<u128>> {
check_succ_bounds(&succ)?;
crate::games::grundy_graph(&succ)
.ok_or_else(|| PyValueError::new_err("graph has a cycle — Grundy value is undefined"))
}
#[pyfunction]
fn mex(values: Vec<u128>) -> u128 {
crate::games::mex(values)
}
fn outcome_name(o: Outcome) -> String {
match o {
Outcome::Loss => "Loss",
Outcome::Win => "Win",
Outcome::Draw => "Draw",
}
.to_string()
}
#[pyclass(name = "Outcome", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyOutcome {
inner: Outcome,
}
fn wrap_outcome(inner: Outcome) -> PyOutcome {
PyOutcome { inner }
}
fn parse_outcome(obj: &Bound<'_, PyAny>) -> PyResult<Outcome> {
if let Ok(outcome) = obj.cast::<PyOutcome>() {
return Ok(outcome.borrow().inner);
}
Err(PyTypeError::new_err("expected Outcome"))
}
#[pymethods]
impl PyOutcome {
#[staticmethod]
fn loss() -> Self {
wrap_outcome(Outcome::Loss)
}
#[staticmethod]
fn win() -> Self {
wrap_outcome(Outcome::Win)
}
#[staticmethod]
fn draw() -> Self {
wrap_outcome(Outcome::Draw)
}
fn name(&self) -> String {
outcome_name(self.inner)
}
fn is_loss(&self) -> bool {
self.inner == Outcome::Loss
}
fn is_win(&self) -> bool {
self.inner == Outcome::Win
}
fn is_draw(&self) -> bool {
self.inner == Outcome::Draw
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
match op {
CompareOp::Eq => Ok(parse_outcome(other).is_ok_and(|o| o == self.inner)),
CompareOp::Ne => Ok(parse_outcome(other).is_ok_and(|o| o != self.inner)),
CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => Err(
PyValueError::new_err("Outcome only supports equality comparisons"),
),
}
}
fn __str__(&self) -> String {
self.name()
}
fn __repr__(&self) -> String {
format!("Outcome.{}", outcome_name(self.inner))
}
}
fn partizan_outcome_name(o: PartizanOutcome) -> String {
match o {
PartizanOutcome::P => "P",
PartizanOutcome::N => "N",
PartizanOutcome::L => "L",
PartizanOutcome::R => "R",
PartizanOutcome::Draw => "Draw",
}
.to_string()
}
#[pyclass(name = "PartizanOutcome", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyPartizanOutcome {
inner: PartizanOutcome,
}
fn wrap_partizan_outcome(inner: PartizanOutcome) -> PyPartizanOutcome {
PyPartizanOutcome { inner }
}
fn parse_partizan_outcome(obj: &Bound<'_, PyAny>) -> PyResult<PartizanOutcome> {
if let Ok(outcome) = obj.cast::<PyPartizanOutcome>() {
return Ok(outcome.borrow().inner);
}
Err(PyTypeError::new_err("expected PartizanOutcome"))
}
#[pymethods]
impl PyPartizanOutcome {
#[staticmethod]
fn p() -> Self {
wrap_partizan_outcome(PartizanOutcome::P)
}
#[staticmethod]
fn n() -> Self {
wrap_partizan_outcome(PartizanOutcome::N)
}
#[staticmethod]
fn l() -> Self {
wrap_partizan_outcome(PartizanOutcome::L)
}
#[staticmethod]
fn r() -> Self {
wrap_partizan_outcome(PartizanOutcome::R)
}
#[staticmethod]
fn draw() -> Self {
wrap_partizan_outcome(PartizanOutcome::Draw)
}
fn name(&self) -> String {
partizan_outcome_name(self.inner)
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
match op {
CompareOp::Eq => Ok(parse_partizan_outcome(other).is_ok_and(|o| o == self.inner)),
CompareOp::Ne => Ok(parse_partizan_outcome(other).is_ok_and(|o| o != self.inner)),
CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => Err(
PyValueError::new_err("PartizanOutcome only supports equality comparisons"),
),
}
}
fn __str__(&self) -> String {
self.name()
}
fn __repr__(&self) -> String {
format!("PartizanOutcome.{}", partizan_outcome_name(self.inner))
}
}
fn loopy_winner_name(w: LoopyWinner) -> String {
match w {
LoopyWinner::Left => "Left",
LoopyWinner::Right => "Right",
LoopyWinner::Draw => "Draw",
}
.to_string()
}
#[pyclass(name = "LoopyWinner", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyLoopyWinner {
inner: LoopyWinner,
}
fn wrap_loopy_winner(inner: LoopyWinner) -> PyLoopyWinner {
PyLoopyWinner { inner }
}
fn parse_loopy_winner(obj: &Bound<'_, PyAny>) -> PyResult<LoopyWinner> {
if let Ok(winner) = obj.cast::<PyLoopyWinner>() {
return Ok(winner.borrow().inner);
}
Err(PyTypeError::new_err("expected LoopyWinner"))
}
#[pymethods]
impl PyLoopyWinner {
#[staticmethod]
fn left() -> Self {
wrap_loopy_winner(LoopyWinner::Left)
}
#[staticmethod]
fn right() -> Self {
wrap_loopy_winner(LoopyWinner::Right)
}
#[staticmethod]
fn draw() -> Self {
wrap_loopy_winner(LoopyWinner::Draw)
}
fn name(&self) -> String {
loopy_winner_name(self.inner)
}
fn is_left(&self) -> bool {
self.inner == LoopyWinner::Left
}
fn is_right(&self) -> bool {
self.inner == LoopyWinner::Right
}
fn is_draw(&self) -> bool {
self.inner == LoopyWinner::Draw
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
match op {
CompareOp::Eq => Ok(parse_loopy_winner(other).is_ok_and(|w| w == self.inner)),
CompareOp::Ne => Ok(parse_loopy_winner(other).is_ok_and(|w| w != self.inner)),
CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => Err(
PyValueError::new_err("LoopyWinner only supports equality comparisons"),
),
}
}
fn __str__(&self) -> String {
self.name()
}
fn __repr__(&self) -> String {
format!("LoopyWinner.{}", loopy_winner_name(self.inner))
}
}
#[pyclass(name = "LoopyPartizanOutcome", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyLoopyPartizanOutcome {
inner: LoopyPartizanOutcome,
}
fn wrap_loopy_partizan_outcome(inner: LoopyPartizanOutcome) -> PyLoopyPartizanOutcome {
PyLoopyPartizanOutcome { inner }
}
fn parse_loopy_partizan_outcome(obj: &Bound<'_, PyAny>) -> PyResult<LoopyPartizanOutcome> {
if let Ok(outcome) = obj.cast::<PyLoopyPartizanOutcome>() {
return Ok(outcome.borrow().inner);
}
Err(PyTypeError::new_err("expected LoopyPartizanOutcome"))
}
#[pymethods]
impl PyLoopyPartizanOutcome {
#[new]
fn new(left_to_move: &Bound<'_, PyAny>, right_to_move: &Bound<'_, PyAny>) -> PyResult<Self> {
Ok(wrap_loopy_partizan_outcome(LoopyPartizanOutcome::new(
parse_loopy_winner(left_to_move)?,
parse_loopy_winner(right_to_move)?,
)))
}
#[getter]
fn left_to_move(&self) -> PyLoopyWinner {
wrap_loopy_winner(self.inner.left_to_move)
}
#[getter]
fn right_to_move(&self) -> PyLoopyWinner {
wrap_loopy_winner(self.inner.right_to_move)
}
fn partizan_class(&self) -> Option<PyPartizanOutcome> {
self.inner.partizan_class().map(wrap_partizan_outcome)
}
fn has_draw(&self) -> bool {
self.inner.has_draw()
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
match op {
CompareOp::Eq => Ok(parse_loopy_partizan_outcome(other).is_ok_and(|o| o == self.inner)),
CompareOp::Ne => Ok(parse_loopy_partizan_outcome(other).is_ok_and(|o| o != self.inner)),
CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => Err(
PyValueError::new_err("LoopyPartizanOutcome only supports equality comparisons"),
),
}
}
fn __repr__(&self) -> String {
format!(
"LoopyPartizanOutcome(left_to_move={}, right_to_move={})",
loopy_winner_name(self.inner.left_to_move),
loopy_winner_name(self.inner.right_to_move)
)
}
}
#[pyclass(name = "LoopyNimber", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyLoopyNimber {
inner: LoopyNimber,
}
fn wrap_loopy_nimber(inner: LoopyNimber) -> PyLoopyNimber {
PyLoopyNimber { inner }
}
fn parse_loopy_nimber(obj: &Bound<'_, PyAny>) -> PyResult<LoopyNimber> {
if let Ok(value) = obj.cast::<PyLoopyNimber>() {
return Ok(value.borrow().inner);
}
Err(PyTypeError::new_err("expected LoopyNimber"))
}
#[pymethods]
impl PyLoopyNimber {
#[staticmethod]
fn value(n: u128) -> Self {
wrap_loopy_nimber(LoopyNimber::Value(n))
}
#[staticmethod]
fn side() -> Self {
wrap_loopy_nimber(LoopyNimber::Side)
}
fn to_u128(&self) -> PyResult<u128> {
match self.inner {
LoopyNimber::Value(n) => Ok(n),
LoopyNimber::Side => Err(PyValueError::new_err("LoopyNimber.Side has no u128 value")),
}
}
fn is_side(&self) -> bool {
self.inner == LoopyNimber::Side
}
fn is_value(&self) -> bool {
matches!(self.inner, LoopyNimber::Value(_))
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
match op {
CompareOp::Eq => Ok(parse_loopy_nimber(other).is_ok_and(|x| x == self.inner)),
CompareOp::Ne => Ok(parse_loopy_nimber(other).is_ok_and(|x| x != self.inner)),
CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => Err(
PyValueError::new_err("LoopyNimber only supports equality comparisons"),
),
}
}
fn __repr__(&self) -> String {
match self.inner {
LoopyNimber::Value(n) => format!("LoopyNimber.Value({n})"),
LoopyNimber::Side => "LoopyNimber.Side".to_string(),
}
}
}
#[pyfunction]
fn outcomes(succ: Vec<Vec<usize>>) -> PyResult<Vec<PyOutcome>> {
check_succ_bounds(&succ)?;
Ok(crate::games::outcomes(&succ)
.into_iter()
.map(wrap_outcome)
.collect())
}
#[pyfunction]
fn p_positions(succ: Vec<Vec<usize>>) -> PyResult<Vec<usize>> {
check_succ_bounds(&succ)?;
Ok(crate::games::p_positions(&succ))
}
#[pyclass(name = "ScoreInterval", module = "ogdoad")]
struct PyScoreInterval {
inner: crate::games::ScoreInterval,
}
#[pymethods]
impl PyScoreInterval {
#[getter]
fn left(&self) -> i128 {
self.inner.left
}
#[getter]
fn right(&self) -> i128 {
self.inner.right
}
fn __repr__(&self) -> String {
format!(
"ScoreInterval(left={}, right={})",
self.inner.left, self.inner.right
)
}
}
fn wrap_score_interval(inner: crate::games::ScoreInterval) -> PyScoreInterval {
PyScoreInterval { inner }
}
#[pyfunction]
fn scoring_values(
succ: Vec<Vec<usize>>,
terminal_score: Vec<i128>,
) -> PyResult<Vec<PyScoreInterval>> {
check_succ_bounds(&succ)?;
crate::games::scoring_values(&succ, &terminal_score)
.map(|v| v.into_iter().map(wrap_score_interval).collect())
.ok_or_else(|| PyValueError::new_err("graph has a cycle — scoring value is undefined"))
}
#[pyfunction]
fn nim_canonical(heaps: Vec<u128>) -> Vec<u128> {
crate::games::nim_canonical(heaps)
}
#[pyfunction]
fn misere_nim_p_predicted(heaps: Vec<u128>) -> bool {
crate::games::misere_nim_p_predicted(&heaps)
}
#[pyfunction]
fn try_misere_is_n(pos: u128, moves: Bound<'_, PyAny>) -> PyResult<Option<bool>> {
misere_is_n_u128_inner(pos, &moves, &mut HashMap::new(), &mut HashSet::new())
}
#[pyfunction]
fn misere_is_n(pos: u128, moves: Bound<'_, PyAny>) -> PyResult<bool> {
try_misere_is_n(pos, moves)?
.ok_or_else(|| PyValueError::new_err("misere_is_n requires an acyclic move graph"))
}
#[pyfunction]
fn misere_is_p(pos: u128, moves: Bound<'_, PyAny>) -> PyResult<bool> {
Ok(!misere_is_n(pos, moves)?)
}
#[pyfunction]
fn nim_moves(pos: Vec<u128>) -> Vec<Vec<u128>> {
crate::games::nim_moves(&pos)
}
#[pyfunction]
fn octal_moves(code: Vec<u128>, pos: Vec<u128>) -> Vec<Vec<u128>> {
crate::games::octal_moves(&code, &pos)
}
#[pyfunction]
fn octal_misere_quotient(
code: Vec<u128>,
max_heap: usize,
elem_bound: usize,
test_bound: usize,
) -> PyResult<PyQuotient> {
crate::games::octal_misere_quotient(&code, max_heap, elem_bound, test_bound)
.map(|inner| PyQuotient { inner })
.ok_or_else(|| {
PyValueError::new_err("octal_misere_quotient requires an acyclic bounded move graph")
})
}
#[pyfunction]
fn loopy_nim_values(succ: Vec<Vec<usize>>) -> PyResult<Vec<PyLoopyNimber>> {
check_succ_bounds(&succ)?;
crate::games::loopy_nim_values(&succ)
.map(|vs| vs.into_iter().map(wrap_loopy_nimber).collect())
.ok_or_else(|| {
PyValueError::new_err("cyclic non-Draw subgraph has no unique bounded sidling solution")
})
}
#[pyfunction]
fn loopy_decision_sets(n: usize, moves: Bound<'_, PyAny>) -> PyResult<(Vec<usize>, Vec<usize>)> {
let succ = loopy_succ_from_callback(n, &moves)?;
let g = LoopyGraph::new(succ);
Ok((g.loss_set(), g.draw_set()))
}
#[pyfunction]
fn loopy_quadric_probe(
k: usize,
moves: Bound<'_, PyAny>,
) -> PyResult<(Option<PyQuadricFit>, Option<PyQuadricFit>)> {
const MAX_ANF_DIM: usize = 20;
if k > MAX_ANF_DIM {
return Err(PyValueError::new_err(format!(
"loopy_quadric_probe is exponential in k; max supported k is {MAX_ANF_DIM}"
)));
}
let n = 1usize << k;
let (loss, draw) = loopy_decision_sets(n, moves)?;
let loss_u: Vec<u128> = loss.into_iter().map(|v| v as u128).collect();
let draw_u: Vec<u128> = draw.into_iter().map(|v| v as u128).collect();
Ok((
crate::forms::fit_f2_quadratic(&loss_u, k).map(wrap_quadric_fit),
crate::forms::fit_f2_quadratic(&draw_u, k).map(wrap_quadric_fit),
))
}
#[pyclass(name = "LoopyNimCertificate", module = "ogdoad")]
struct PyLoopyNimCertificate {
inner: LoopyNimCertificate,
}
#[pymethods]
impl PyLoopyNimCertificate {
#[getter]
fn outcomes(&self) -> Vec<PyOutcome> {
self.inner
.outcomes
.iter()
.copied()
.map(wrap_outcome)
.collect()
}
#[getter]
fn side_positions(&self) -> Vec<usize> {
self.inner.side_positions.clone()
}
#[getter]
fn used_sidling_solver(&self) -> bool {
self.inner.used_sidling_solver
}
#[getter]
fn sidling_assignments_examined(&self) -> usize {
self.inner.sidling_assignments_examined
}
#[getter]
fn recovery_condition_holds(&self) -> bool {
self.inner.recovery_condition_holds
}
#[getter]
fn recovery_blockers(&self) -> Vec<usize> {
self.inner.recovery_blockers.clone()
}
fn display(&self) -> String {
self.inner.display()
}
fn __repr__(&self) -> String {
self.inner.display()
}
}
#[pyfunction]
fn loopy_nim_values_certified(
succ: Vec<Vec<usize>>,
) -> PyResult<(Vec<PyLoopyNimber>, PyLoopyNimCertificate)> {
check_succ_bounds(&succ)?;
crate::games::loopy_nim_values_certified(&succ)
.map(|(vs, inner)| {
let values = vs.into_iter().map(wrap_loopy_nimber).collect();
(values, PyLoopyNimCertificate { inner })
})
.ok_or_else(|| {
PyValueError::new_err("cyclic non-Draw subgraph has no unique bounded sidling solution")
})
}
#[pyfunction]
fn thermograph(game: &PyGame) -> Option<PyThermograph> {
thermography::thermograph(&game.inner).map(wrap_thermograph)
}
#[pyfunction]
fn thermograph_via_tropical(game: &PyGame) -> Option<PyThermograph> {
crate::games::tropical_thermography::thermograph_via_tropical(&game.inner).map(wrap_thermograph)
}
#[pyfunction]
fn temperature(game: &PyGame) -> Option<PySurreal> {
crate::games::temperature(&game.inner).map(rat_to_py)
}
#[pyfunction]
fn mean_value(game: &PyGame) -> Option<PySurreal> {
crate::games::mean_value(&game.inner).map(rat_to_py)
}
#[pyfunction]
#[pyo3(signature = (game, num, den=1))]
fn heat(game: &PyGame, num: i128, den: i128) -> PyResult<Option<PyGame>> {
let t = Rational::try_new(num, den)
.ok_or_else(|| PyValueError::new_err("zero denominator or bounded i128 overflow"))?;
Ok(crate::games::heat(&game.inner, &t).map(|inner| PyGame { inner }))
}
#[pyfunction]
fn norton_multiply(game: &PyGame, unit: &PyGame) -> Option<PyGame> {
crate::games::norton_multiply(&game.inner, &unit.inner).map(|inner| PyGame { inner })
}
#[pyfunction]
fn overheat(game: &PyGame, s: &PyGame, t: &PyGame) -> Option<PyGame> {
crate::games::overheat(&game.inner, &s.inner, &t.inner).map(|inner| PyGame { inner })
}
#[pyfunction]
fn left_stop(game: &PyGame) -> Option<PySurreal> {
crate::games::left_stop(&game.inner).map(rat_to_py)
}
#[pyfunction]
fn right_stop(game: &PyGame) -> Option<PySurreal> {
crate::games::right_stop(&game.inner).map(rat_to_py)
}
#[pyfunction]
fn atomic_weight(game: &PyGame) -> Option<PyGame> {
crate::games::atomic_weight(&game.inner).map(|inner| PyGame { inner })
}
#[pyfunction]
fn atomic_weight_int(game: &PyGame) -> Option<i128> {
crate::games::atomic_weight_int(&game.inner)
}
#[pyclass(name = "Game", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyGame {
inner: Game,
}
#[pymethods]
impl PyGame {
#[staticmethod]
fn zero() -> PyGame {
PyGame {
inner: Game::zero(),
}
}
#[staticmethod]
fn star() -> PyGame {
PyGame {
inner: Game::star(),
}
}
#[staticmethod]
fn up() -> PyGame {
PyGame { inner: Game::up() }
}
#[staticmethod]
fn integer(n: i128) -> PyGame {
PyGame {
inner: Game::integer(n),
}
}
#[staticmethod]
fn switch(a: i128, b: i128) -> PyGame {
PyGame {
inner: Game::switch(a, b),
}
}
#[staticmethod]
fn of(left: Vec<PyGame>, right: Vec<PyGame>) -> PyGame {
PyGame {
inner: Game::new(
left.into_iter().map(|g| g.inner).collect(),
right.into_iter().map(|g| g.inner).collect(),
),
}
}
fn left(&self) -> Vec<PyGame> {
self.inner
.left()
.iter()
.map(|g| PyGame { inner: g.clone() })
.collect()
}
fn right(&self) -> Vec<PyGame> {
self.inner
.right()
.iter()
.map(|g| PyGame { inner: g.clone() })
.collect()
}
fn __add__(&self, other: &PyGame) -> PyGame {
PyGame {
inner: self.inner.add(&other.inner),
}
}
fn __neg__(&self) -> PyGame {
PyGame {
inner: self.inner.neg(),
}
}
fn __sub__(&self, other: &PyGame) -> PyGame {
PyGame {
inner: self.inner.add(&other.inner.neg()),
}
}
fn le(&self, other: &PyGame) -> bool {
self.inner.le(&other.inner)
}
fn __eq__(&self, other: &PyGame) -> bool {
self.inner.eq(&other.inner)
}
fn fuzzy(&self, other: &PyGame) -> bool {
self.inner.fuzzy(&other.inner)
}
fn birthday(&self) -> u128 {
self.inner.birthday()
}
fn is_number(&self) -> bool {
self.inner.is_number()
}
#[staticmethod]
fn star_n(n: u128) -> PyGame {
PyGame {
inner: Game::nim_heap(n),
}
}
#[staticmethod]
fn nim_heap(n: u128) -> PyGame {
PyGame {
inner: Game::nim_heap(n),
}
}
fn is_all_small(&self) -> bool {
self.inner.is_all_small()
}
fn atomic_weight(&self) -> Option<PyGame> {
crate::games::atomic_weight(&self.inner).map(|inner| PyGame { inner })
}
fn atomic_weight_int(&self) -> Option<i128> {
crate::games::atomic_weight_int(&self.inner)
}
fn times_int(&self, n: i128) -> PyGame {
PyGame {
inner: self.inner.times_int(n),
}
}
fn canonical(&self) -> PyGame {
PyGame {
inner: self.inner.canonical(),
}
}
fn is_canonical(&self) -> bool {
self.inner.is_canonical()
}
fn canonical_string(&self) -> String {
self.inner.canonical_string()
}
fn structural_string(&self) -> String {
self.inner.structural_string()
}
fn structural_eq(&self, other: &PyGame) -> bool {
self.inner.structural_eq(&other.inner)
}
fn display(&self) -> String {
self.inner.display()
}
fn number_value(&self) -> Option<PySurreal> {
self.inner.number_value().map(PySurreal::from_inner)
}
#[staticmethod]
fn from_surreal(s: &Bound<'_, PyAny>) -> PyResult<PyGame> {
let s = parse_surreal(s)?;
Game::from_surreal(&s)
.map(|inner| PyGame { inner })
.ok_or_else(|| PyValueError::new_err("surreal is not a dyadic rational"))
}
fn ordinal_sum(&self, h: &PyGame) -> PyGame {
PyGame {
inner: self.inner.ordinal_sum(&h.inner),
}
}
fn temperature(&self) -> Option<PySurreal> {
thermography::temperature(&self.inner).map(rat_to_py)
}
fn mean_value(&self) -> Option<PySurreal> {
thermography::mean_value(&self.inner).map(rat_to_py)
}
#[pyo3(signature = (num, den=1))]
fn heat(&self, num: i128, den: i128) -> PyResult<Option<PyGame>> {
let t = Rational::try_new(num, den)
.ok_or_else(|| PyValueError::new_err("zero denominator or bounded i128 overflow"))?;
Ok(crate::games::heat(&self.inner, &t).map(|inner| PyGame { inner }))
}
fn norton_multiply(&self, unit: &PyGame) -> Option<PyGame> {
crate::games::norton_multiply(&self.inner, &unit.inner).map(|inner| PyGame { inner })
}
fn overheat(&self, s: &PyGame, t: &PyGame) -> Option<PyGame> {
crate::games::overheat(&self.inner, &s.inner, &t.inner).map(|inner| PyGame { inner })
}
fn left_stop(&self) -> Option<PySurreal> {
thermography::left_stop(&self.inner).map(rat_to_py)
}
fn right_stop(&self) -> Option<PySurreal> {
thermography::right_stop(&self.inner).map(rat_to_py)
}
fn thermograph(&self) -> Option<PyThermograph> {
thermography::thermograph(&self.inner).map(wrap_thermograph)
}
fn thermograph_via_tropical(&self) -> Option<PyThermograph> {
crate::games::tropical_thermography::thermograph_via_tropical(&self.inner)
.map(wrap_thermograph)
}
#[pyo3(signature = (num, den=1))]
fn cooled_stops(&self, num: i128, den: i128) -> PyResult<Option<(PySurreal, PySurreal)>> {
let t = Rational::try_new(num, den)
.ok_or_else(|| PyValueError::new_err("zero denominator or bounded i128 overflow"))?;
Ok(thermography::thermograph(&self.inner).map(|th| {
let (l, r) = th.cooled_stops(&t);
(rat_to_py(l), rat_to_py(r))
}))
}
fn __repr__(&self) -> String {
self.inner.display()
}
}
fn color_name(c: Color) -> String {
match c {
Color::Blue => "blue",
Color::Red => "red",
Color::Green => "green",
}
.to_string()
}
#[pyclass(name = "Color", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyColor {
inner: Color,
}
fn wrap_color(inner: Color) -> PyColor {
PyColor { inner }
}
fn parse_color_obj(obj: &Bound<'_, PyAny>) -> PyResult<Color> {
if let Ok(color) = obj.cast::<PyColor>() {
return Ok(color.borrow().inner);
}
Err(PyTypeError::new_err("expected Color"))
}
#[pymethods]
impl PyColor {
#[staticmethod]
fn blue() -> Self {
wrap_color(Color::Blue)
}
#[staticmethod]
fn red() -> Self {
wrap_color(Color::Red)
}
#[staticmethod]
fn green() -> Self {
wrap_color(Color::Green)
}
fn name(&self) -> String {
color_name(self.inner)
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
match op {
CompareOp::Eq => Ok(parse_color_obj(other).is_ok_and(|c| c == self.inner)),
CompareOp::Ne => Ok(parse_color_obj(other).is_ok_and(|c| c != self.inner)),
CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => Err(
PyValueError::new_err("Color only supports equality comparisons"),
),
}
}
fn __str__(&self) -> String {
self.name()
}
fn __repr__(&self) -> String {
match self.inner {
Color::Blue => "Color.Blue".to_string(),
Color::Red => "Color.Red".to_string(),
Color::Green => "Color.Green".to_string(),
}
}
}
#[pyclass(name = "Hackenbush", module = "ogdoad")]
struct PyHackenbush {
inner: Hackenbush,
}
#[pymethods]
impl PyHackenbush {
#[new]
fn new(edges: Vec<(usize, usize, PyColor)>) -> Self {
let edges = edges.into_iter().map(|(u, v, c)| (u, v, c.inner)).collect();
PyHackenbush {
inner: Hackenbush::new(edges),
}
}
#[staticmethod]
fn string(colors: Vec<PyColor>) -> Self {
let cs = colors.into_iter().map(|c| c.inner).collect::<Vec<_>>();
PyHackenbush {
inner: Hackenbush::string(&cs),
}
}
fn edges(&self) -> Vec<(usize, usize, PyColor)> {
self.inner
.edges()
.iter()
.map(|&(u, v, c)| (u, v, wrap_color(c)))
.collect()
}
fn to_game(&self) -> PyGame {
PyGame {
inner: self.inner.to_game(),
}
}
fn value(&self) -> Option<PySurreal> {
self.inner.value().map(PySurreal::from_inner)
}
fn grundy(&self) -> Option<u128> {
self.inner.grundy()
}
}
#[pyclass(name = "GameExterior", module = "ogdoad")]
struct PyGameExterior {
inner: GameExterior,
alg: Arc<CliffordAlgebra<Integer>>,
}
#[pymethods]
impl PyGameExterior {
#[new]
fn new(gens: Vec<PyGame>) -> Self {
let games: Vec<Game> = gens.iter().map(|g| g.inner.clone()).collect();
PyGameExterior::from_inner(GameExterior::new(games))
}
#[staticmethod]
fn free(gens: Vec<PyGame>) -> Self {
let games: Vec<Game> = gens.iter().map(|g| g.inner.clone()).collect();
PyGameExterior::from_inner(GameExterior::free(games))
}
#[staticmethod]
fn with_relation_bound(gens: Vec<PyGame>, bound: i128) -> Self {
let games: Vec<Game> = gens.iter().map(|g| g.inner.clone()).collect();
PyGameExterior::from_inner(GameExterior::with_relation_search(games, bound))
}
#[staticmethod]
fn with_relation_search(gens: Vec<PyGame>, bound: i128) -> Self {
Self::with_relation_bound(gens, bound)
}
#[staticmethod]
fn with_relations(gens: Vec<PyGame>, relations: Vec<PyGameRelation>) -> Self {
let games: Vec<Game> = gens.iter().map(|g| g.inner.clone()).collect();
let relations = relations.into_iter().map(|rel| rel.inner).collect();
PyGameExterior::from_inner(GameExterior::with_relations(games, relations))
}
#[getter]
fn dim(&self) -> usize {
self.inner.algebra().dim()
}
fn algebra(&self) -> IntegerAlgebra {
IntegerAlgebra {
inner: self.alg.clone(),
}
}
fn relations(&self) -> Vec<PyGameRelation> {
self.inner
.relations()
.iter()
.cloned()
.map(wrap_game_relation)
.collect()
}
fn relation_search_complete(&self) -> bool {
self.inner.relation_search_complete()
}
fn relation_search_certificate(&self) -> PyRelationSearchCertificate {
wrap_relation_search_certificate(self.inner.relation_search_certificate().clone())
}
fn generator(&self, i: usize) -> IntegerMV {
IntegerMV {
alg: self.alg.clone(),
mv: self.inner.generator(i),
}
}
fn game(&self, i: usize) -> PyGame {
PyGame {
inner: self.inner.game(i).clone(),
}
}
fn reduce(&self, mv: &IntegerMV) -> PyResult<IntegerMV> {
self.ensure_mv(mv)?;
Ok(IntegerMV {
alg: self.alg.clone(),
mv: self.inner.reduce(&mv.mv),
})
}
fn add(&self, a: &IntegerMV, b: &IntegerMV) -> PyResult<IntegerMV> {
self.ensure_mv(a)?;
self.ensure_mv(b)?;
Ok(IntegerMV {
alg: self.alg.clone(),
mv: self.inner.add(&a.mv, &b.mv),
})
}
fn scalar_mul(&self, s: i128, mv: &IntegerMV) -> PyResult<IntegerMV> {
self.ensure_mv(mv)?;
Ok(IntegerMV {
alg: self.alg.clone(),
mv: self.inner.scalar_mul(s, &mv.mv),
})
}
fn wedge(&self, a: &IntegerMV, b: &IntegerMV) -> PyResult<IntegerMV> {
self.ensure_mv(a)?;
self.ensure_mv(b)?;
Ok(IntegerMV {
alg: self.alg.clone(),
mv: self.inner.wedge(&a.mv, &b.mv),
})
}
fn is_zero(&self, mv: &IntegerMV) -> PyResult<bool> {
self.ensure_mv(mv)?;
Ok(self.inner.is_zero(&mv.mv))
}
fn value_of_grade1(&self, mv: &IntegerMV) -> PyResult<PyGame> {
self.ensure_mv(mv)?;
let reduced = self.inner.reduce(&mv.mv);
if reduced.terms.keys().any(|blade| blade.count_ones() != 1) {
return Err(PyValueError::new_err("expected a grade-1 element"));
}
Ok(PyGame {
inner: self.inner.value_of_grade1(&reduced),
})
}
}
impl PyGameExterior {
fn from_inner(inner: GameExterior) -> Self {
let alg = Arc::new(inner.algebra().clone());
PyGameExterior { inner, alg }
}
fn ensure_mv(&self, mv: &IntegerMV) -> PyResult<()> {
if self.alg.as_ref() == mv.alg.as_ref() {
Ok(())
} else {
Err(PyValueError::new_err(
"multivector belongs to a different GameExterior algebra",
))
}
}
}
#[pyclass(name = "GameClifford", module = "ogdoad")]
struct PyGameClifford {
inner: GameClifford,
alg: Arc<CliffordAlgebra<Integer>>,
}
#[pymethods]
impl PyGameClifford {
#[new]
#[pyo3(signature = (gens, q, b=None))]
fn new(
gens: Vec<PyGame>,
q: Vec<i128>,
b: Option<Vec<(usize, usize, i128)>>,
) -> PyResult<Self> {
let games: Vec<Game> = gens.iter().map(|g| g.inner.clone()).collect();
let b = parse_game_clifford_bilinear(b);
GameClifford::new(games, q, b)
.map(PyGameClifford::from_inner)
.map_err(game_clifford_error)
}
#[staticmethod]
#[pyo3(signature = (gens, q, b=None))]
fn free(
gens: Vec<PyGame>,
q: Vec<i128>,
b: Option<Vec<(usize, usize, i128)>>,
) -> PyResult<Self> {
let games: Vec<Game> = gens.iter().map(|g| g.inner.clone()).collect();
let b = parse_game_clifford_bilinear(b);
GameClifford::free(games, q, b)
.map(PyGameClifford::from_inner)
.map_err(game_clifford_error)
}
#[staticmethod]
#[pyo3(signature = (gens, bound, q, b=None))]
fn with_relation_bound(
gens: Vec<PyGame>,
bound: i128,
q: Vec<i128>,
b: Option<Vec<(usize, usize, i128)>>,
) -> PyResult<Self> {
let games: Vec<Game> = gens.iter().map(|g| g.inner.clone()).collect();
let b = parse_game_clifford_bilinear(b);
GameClifford::with_relation_search(games, bound, q, b)
.map(PyGameClifford::from_inner)
.map_err(game_clifford_error)
}
#[staticmethod]
#[pyo3(signature = (gens, bound, q, b=None))]
fn with_relation_search(
gens: Vec<PyGame>,
bound: i128,
q: Vec<i128>,
b: Option<Vec<(usize, usize, i128)>>,
) -> PyResult<Self> {
Self::with_relation_bound(gens, bound, q, b)
}
#[staticmethod]
#[pyo3(signature = (gens, relations, q, b=None))]
fn with_quadratic_data(
gens: Vec<PyGame>,
relations: Vec<PyGameRelation>,
q: Vec<i128>,
b: Option<Vec<(usize, usize, i128)>>,
) -> PyResult<Self> {
let games: Vec<Game> = gens.iter().map(|g| g.inner.clone()).collect();
let relations = relations.into_iter().map(|rel| rel.inner).collect();
let b = parse_game_clifford_bilinear(b);
GameClifford::with_quadratic_data(games, relations, q, b)
.map(PyGameClifford::from_inner)
.map_err(game_clifford_error)
}
#[getter]
fn dim(&self) -> usize {
self.inner.algebra().dim()
}
fn algebra(&self) -> IntegerAlgebra {
IntegerAlgebra {
inner: self.alg.clone(),
}
}
fn relations(&self) -> Vec<PyGameRelation> {
self.inner
.relations()
.iter()
.cloned()
.map(wrap_game_relation)
.collect()
}
fn relation_search_complete(&self) -> bool {
self.inner.relation_search_complete()
}
fn relation_search_certificate(&self) -> PyRelationSearchCertificate {
wrap_relation_search_certificate(self.inner.relation_search_certificate().clone())
}
fn generator(&self, i: usize) -> IntegerMV {
IntegerMV {
alg: self.alg.clone(),
mv: self.inner.generator(i),
}
}
fn game(&self, i: usize) -> PyGame {
PyGame {
inner: self.inner.game(i).clone(),
}
}
fn reduce(&self, mv: &IntegerMV) -> PyResult<IntegerMV> {
self.ensure_mv(mv)?;
Ok(IntegerMV {
alg: self.alg.clone(),
mv: self.inner.reduce(&mv.mv),
})
}
fn add(&self, a: &IntegerMV, b: &IntegerMV) -> PyResult<IntegerMV> {
self.ensure_mv(a)?;
self.ensure_mv(b)?;
Ok(IntegerMV {
alg: self.alg.clone(),
mv: self.inner.add(&a.mv, &b.mv),
})
}
fn scalar_mul(&self, s: i128, mv: &IntegerMV) -> PyResult<IntegerMV> {
self.ensure_mv(mv)?;
Ok(IntegerMV {
alg: self.alg.clone(),
mv: self.inner.scalar_mul(s, &mv.mv),
})
}
fn mul(&self, a: &IntegerMV, b: &IntegerMV) -> PyResult<IntegerMV> {
self.ensure_mv(a)?;
self.ensure_mv(b)?;
Ok(IntegerMV {
alg: self.alg.clone(),
mv: self.inner.mul(&a.mv, &b.mv),
})
}
fn wedge(&self, a: &IntegerMV, b: &IntegerMV) -> PyResult<IntegerMV> {
self.ensure_mv(a)?;
self.ensure_mv(b)?;
Ok(IntegerMV {
alg: self.alg.clone(),
mv: self.inner.wedge(&a.mv, &b.mv),
})
}
fn is_zero(&self, mv: &IntegerMV) -> PyResult<bool> {
self.ensure_mv(mv)?;
Ok(self.inner.is_zero(&mv.mv))
}
fn value_of_grade1(&self, mv: &IntegerMV) -> PyResult<PyGame> {
self.ensure_mv(mv)?;
let reduced = self.inner.reduce(&mv.mv);
if reduced.terms.keys().any(|blade| blade.count_ones() != 1) {
return Err(PyValueError::new_err("expected a grade-1 element"));
}
Ok(PyGame {
inner: self.inner.value_of_grade1(&reduced),
})
}
}
impl PyGameClifford {
fn from_inner(inner: GameClifford) -> Self {
let alg = Arc::new(inner.algebra().clone());
PyGameClifford { inner, alg }
}
fn ensure_mv(&self, mv: &IntegerMV) -> PyResult<()> {
if self.alg.as_ref() == mv.alg.as_ref() {
Ok(())
} else {
Err(PyValueError::new_err(
"multivector belongs to a different GameClifford algebra",
))
}
}
}
fn parse_game_clifford_bilinear(
b: Option<Vec<(usize, usize, i128)>>,
) -> BTreeMap<(usize, usize), i128> {
b.unwrap_or_default()
.into_iter()
.map(|(i, j, value)| ((i, j), value))
.collect()
}
fn game_clifford_error(err: crate::games::GameCliffordError) -> PyErr {
PyValueError::new_err(err.to_string())
}
#[pyclass(name = "NumberGame", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyNumberGame {
inner: NumberGame,
}
#[pymethods]
impl PyNumberGame {
#[staticmethod]
fn from_surreal(s: &Bound<'_, PyAny>) -> PyResult<PyNumberGame> {
Ok(PyNumberGame {
inner: NumberGame::from_surreal(&parse_surreal(s)?),
})
}
fn value(&self) -> PySurreal {
PySurreal::from_inner(self.inner.value().clone())
}
fn birthday_finite(&self) -> Option<u128> {
self.inner.birthday().and_then(|o| o.as_finite())
}
fn birthday(&self) -> Option<PyOrdinal> {
self.inner.birthday().map(PyOrdinal::from_inner)
}
fn birthday_ordinal(&self) -> Option<PyOrdinal> {
self.inner.birthday().map(PyOrdinal::from_inner)
}
fn birthday_repr(&self) -> Option<String> {
self.inner.birthday().map(|o| format!("{o:?}"))
}
fn sign_expansion(&self) -> Option<Vec<(bool, PyOrdinal)>> {
self.inner.sign_expansion().map(|se| {
se.runs()
.iter()
.map(|(s, l)| (*s, PyOrdinal::from_inner(l.clone())))
.collect()
})
}
#[staticmethod]
fn from_sign_expansion(runs: Vec<(bool, PyOrdinal)>) -> Option<PyNumberGame> {
let se = SignExpansion::from_runs(
runs.into_iter()
.map(|(sign, len)| (sign, len.as_ordinal().clone()))
.collect(),
);
NumberGame::from_sign_expansion(&se).map(|inner| PyNumberGame { inner })
}
fn to_finite_game(&self) -> Option<PyGame> {
self.inner.to_finite_game().map(|inner| PyGame { inner })
}
fn __add__(&self, other: &PyNumberGame) -> PyNumberGame {
PyNumberGame {
inner: self.inner.add(&other.inner),
}
}
fn __neg__(&self) -> PyNumberGame {
PyNumberGame {
inner: self.inner.neg(),
}
}
fn __richcmp__(&self, other: &PyNumberGame, op: CompareOp) -> bool {
op.matches(self.inner.cmp(&other.inner))
}
fn __repr__(&self) -> String {
format!("NumberGame({:?})", self.inner.value())
}
}
#[pyclass(name = "NimberGame", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyNimberGame {
inner: NimberGame,
}
#[pymethods]
impl PyNimberGame {
#[staticmethod]
fn from_ordinal(o: &PyOrdinal) -> PyNimberGame {
PyNimberGame {
inner: NimberGame::from_ordinal(o.as_ordinal()),
}
}
#[staticmethod]
fn nim_heap(n: u128) -> PyNimberGame {
PyNimberGame {
inner: NimberGame::nim_heap(n),
}
}
fn grundy(&self) -> PyOrdinal {
PyOrdinal::from_inner(self.inner.grundy().clone())
}
fn grundy_finite(&self) -> Option<u128> {
self.inner.grundy().as_finite()
}
fn to_finite_game(&self) -> Option<PyGame> {
self.inner.to_finite_game().map(|inner| PyGame { inner })
}
fn turning_corners(&self, other: &PyNimberGame) -> Option<PyNimberGame> {
self.inner
.turning_corners(&other.inner)
.map(|inner| PyNimberGame { inner })
}
fn __add__(&self, other: &PyNimberGame) -> PyNimberGame {
PyNimberGame {
inner: self.inner.add(&other.inner),
}
}
fn __neg__(&self) -> PyNimberGame {
PyNimberGame {
inner: self.inner.neg(),
}
}
fn __richcmp__(&self, other: &PyNimberGame, op: CompareOp) -> bool {
op.matches(self.inner.cmp(&other.inner))
}
fn __repr__(&self) -> String {
format!("NimberGame(⋆{:?})", self.inner.grundy())
}
}
#[pyclass(name = "Quotient", module = "ogdoad")]
struct PyQuotient {
inner: Quotient,
}
#[pymethods]
impl PyQuotient {
#[getter]
fn elements(&self) -> Vec<Vec<usize>> {
self.inner.elements.clone()
}
#[getter]
fn test_positions(&self) -> Vec<Vec<usize>> {
self.inner.test_positions.clone()
}
#[getter]
fn signatures(&self) -> Vec<Vec<bool>> {
self.inner.signatures.clone()
}
#[getter]
fn class_of(&self) -> Vec<usize> {
self.inner.class_of.clone()
}
#[getter]
fn num_classes(&self) -> usize {
self.inner.num_classes()
}
#[getter]
fn class_rep(&self) -> Vec<Vec<usize>> {
self.inner.class_rep.clone()
}
#[getter]
fn class_is_p(&self) -> Vec<bool> {
self.inner.class_is_p.clone()
}
#[getter]
fn multiplication(&self) -> Option<Vec<Vec<usize>>> {
self.inner.multiplication.clone()
}
#[getter]
fn multiplication_consistent(&self) -> bool {
self.inner.multiplication_consistent
}
#[getter]
fn elements_closed_under_sum(&self) -> bool {
self.inner.elements_closed_under_sum
}
#[getter]
fn has_complete_bounded_monoid(&self) -> bool {
self.inner.has_complete_bounded_monoid()
}
fn class_product(&self, a: usize, b: usize) -> Option<usize> {
self.inner.class_product(a, b)
}
fn signature_of_element(&self, element_index: usize) -> Option<Vec<bool>> {
self.inner
.signature_of_element(element_index)
.map(<[bool]>::to_vec)
}
fn display(&self) -> String {
self.inner.display()
}
fn __repr__(&self) -> String {
self.inner.display()
}
}
#[pyclass(name = "AbstractGame", module = "ogdoad")]
struct PyAbstractGame {
inner: AbstractGame,
}
#[pymethods]
impl PyAbstractGame {
#[new]
fn new(moves: Vec<Vec<usize>>) -> Self {
PyAbstractGame {
inner: AbstractGame { moves },
}
}
fn misere_outcome(&self, pos: Vec<usize>) -> PyResult<bool> {
let mut memo = std::collections::HashMap::new();
self.inner.misere_outcome(&pos, &mut memo).ok_or_else(|| {
PyValueError::new_err("misere_outcome: move graph has a cycle — outcome is undefined")
})
}
fn misere_quotient(
&self,
atoms: Vec<usize>,
elem_bound: usize,
test_bound: usize,
) -> PyResult<PyQuotient> {
crate::games::misere_quotient(&self.inner, &atoms, elem_bound, test_bound)
.map(|inner| PyQuotient { inner })
.ok_or_else(|| {
PyValueError::new_err("misere_quotient requires an acyclic bounded move graph")
})
}
}
#[pyfunction]
fn misere_quotient(
game: &PyAbstractGame,
atoms: Vec<usize>,
elem_bound: usize,
test_bound: usize,
) -> PyResult<PyQuotient> {
crate::games::misere_quotient(&game.inner, &atoms, elem_bound, test_bound)
.map(|inner| PyQuotient { inner })
.ok_or_else(|| {
PyValueError::new_err("misere_quotient requires an acyclic bounded move graph")
})
}
#[pyclass(name = "LoopyValue", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyLoopyValue {
inner: LoopyValue,
}
#[pymethods]
impl PyLoopyValue {
#[staticmethod]
fn zero() -> Self {
PyLoopyValue {
inner: LoopyValue::Zero,
}
}
#[staticmethod]
fn star() -> Self {
PyLoopyValue {
inner: LoopyValue::Star,
}
}
#[staticmethod]
fn on() -> Self {
PyLoopyValue {
inner: LoopyValue::On,
}
}
#[staticmethod]
fn off() -> Self {
PyLoopyValue {
inner: LoopyValue::Off,
}
}
#[staticmethod]
fn over() -> Self {
PyLoopyValue {
inner: LoopyValue::Over,
}
}
#[staticmethod]
fn under() -> Self {
PyLoopyValue {
inner: LoopyValue::Under,
}
}
#[staticmethod]
fn plus_minus() -> Self {
PyLoopyValue {
inner: LoopyValue::PlusMinus,
}
}
#[staticmethod]
fn tis() -> Self {
PyLoopyValue {
inner: LoopyValue::Tis,
}
}
#[staticmethod]
fn tisn() -> Self {
PyLoopyValue {
inner: LoopyValue::Tisn,
}
}
#[staticmethod]
fn onside_offside(onside: i128, offside: i128) -> Self {
PyLoopyValue {
inner: LoopyValue::onside_offside(onside, offside),
}
}
#[staticmethod]
fn dud() -> Self {
PyLoopyValue {
inner: LoopyValue::Dud,
}
}
fn name(&self) -> String {
self.inner.name()
}
fn form(&self) -> String {
self.inner.form()
}
fn outcome(&self) -> PyLoopyPartizanOutcome {
wrap_loopy_partizan_outcome(self.inner.outcome())
}
fn partizan_outcome(&self) -> Option<PyPartizanOutcome> {
self.inner.partizan_outcome().map(wrap_partizan_outcome)
}
fn sides(&self) -> Option<(i128, i128)> {
self.inner.sides()
}
fn __neg__(&self) -> PyLoopyValue {
PyLoopyValue {
inner: self.inner.neg(),
}
}
fn is_stopper(&self) -> bool {
self.inner.is_stopper()
}
fn __add__(&self, other: &PyLoopyValue) -> Option<PyLoopyValue> {
self.inner
.add(&other.inner)
.map(|inner| PyLoopyValue { inner })
}
fn __richcmp__(&self, other: &PyLoopyValue, op: CompareOp) -> bool {
match op {
CompareOp::Eq => self.inner == other.inner,
CompareOp::Ne => self.inner != other.inner,
CompareOp::Lt | CompareOp::Le | CompareOp::Gt | CompareOp::Ge => self
.inner
.partial_cmp(&other.inner)
.is_some_and(|ordering| op.matches(ordering)),
}
}
fn __repr__(&self) -> String {
format!("LoopyValue({:?})", self.inner)
}
}
#[pyclass(name = "LoopyGraph", module = "ogdoad")]
struct PyLoopyGraph {
inner: LoopyGraph,
}
#[pymethods]
impl PyLoopyGraph {
#[new]
fn new(succ: Vec<Vec<usize>>) -> PyResult<Self> {
check_succ_bounds(&succ)?;
Ok(PyLoopyGraph {
inner: LoopyGraph::new(succ),
})
}
#[staticmethod]
fn from_rule(n: usize, moves: Bound<'_, PyAny>) -> PyResult<Self> {
Ok(PyLoopyGraph {
inner: LoopyGraph::new(loopy_succ_from_callback(n, &moves)?),
})
}
fn succ(&self) -> Vec<Vec<usize>> {
self.inner.succ().to_vec()
}
fn outcomes(&self) -> Vec<PyOutcome> {
self.inner
.outcomes()
.into_iter()
.map(wrap_outcome)
.collect()
}
fn loss_set(&self) -> Vec<usize> {
self.inner.loss_set()
}
fn win_set(&self) -> Vec<usize> {
self.inner.win_set()
}
fn draw_set(&self) -> Vec<usize> {
self.inner.draw_set()
}
fn classify(&self, v: usize) -> Option<PyLoopyValue> {
self.inner.classify(v).map(|inner| PyLoopyValue { inner })
}
}
#[pyclass(name = "LoopyPartizanGraph", module = "ogdoad")]
struct PyLoopyPartizanGraph {
inner: LoopyPartizanGraph,
}
#[pymethods]
impl PyLoopyPartizanGraph {
#[new]
fn new(left: Vec<Vec<usize>>, right: Vec<Vec<usize>>) -> PyResult<Self> {
check_partizan_succ_bounds(&left, &right)?;
Ok(PyLoopyPartizanGraph {
inner: LoopyPartizanGraph::new(left, right)
.expect("Python adjacency was checked immediately above"),
})
}
#[staticmethod]
fn from_rules(
n: usize,
left_moves: Bound<'_, PyAny>,
right_moves: Bound<'_, PyAny>,
) -> PyResult<Self> {
let left = loopy_succ_from_callback(n, &left_moves)?;
let right = loopy_succ_from_callback(n, &right_moves)?;
Ok(PyLoopyPartizanGraph {
inner: LoopyPartizanGraph::new(left, right)
.expect("callback adjacency uses the declared node range"),
})
}
fn left(&self) -> Vec<Vec<usize>> {
self.inner.left().to_vec()
}
fn right(&self) -> Vec<Vec<usize>> {
self.inner.right().to_vec()
}
fn outcomes(&self) -> Vec<PyLoopyPartizanOutcome> {
self.inner
.outcomes()
.into_iter()
.map(wrap_loopy_partizan_outcome)
.collect()
}
fn partizan_outcomes(&self) -> Vec<Option<PyPartizanOutcome>> {
self.inner
.partizan_outcomes()
.into_iter()
.map(|o| o.map(wrap_partizan_outcome))
.collect()
}
fn classify(&self, v: usize) -> Option<PyPartizanOutcome> {
self.inner.classify(v).map(wrap_partizan_outcome)
}
fn draw_set(&self) -> Vec<usize> {
self.inner.draw_set()
}
fn nonclassical_set(&self) -> Vec<usize> {
self.inner.nonclassical_set()
}
}
#[pyclass(name = "NimLexicode", module = "ogdoad", from_py_object)]
#[derive(Clone)]
struct PyNimLexicode {
inner: NimLexicode,
}
#[pymethods]
impl PyNimLexicode {
#[getter]
fn base_exp(&self) -> usize {
self.inner.base_exp()
}
#[getter]
fn base(&self) -> u128 {
self.inner.base()
}
fn len(&self) -> usize {
self.inner.len()
}
fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[getter]
fn min_distance(&self) -> usize {
self.inner.min_distance()
}
fn word_count(&self) -> usize {
self.inner.word_count()
}
fn packed_words(&self) -> Vec<u128> {
self.inner.packed_words().to_vec()
}
fn words(&self) -> Vec<Vec<u128>> {
self.inner.words()
}
fn f2_dimension(&self) -> Option<usize> {
self.inner.f2_dimension()
}
fn is_closed_under_nim_add(&self) -> bool {
self.inner.is_closed_under_nim_add()
}
fn is_closed_under_nim_scalars(&self) -> bool {
self.inner.is_closed_under_nim_scalars()
}
fn has_nim_field_base(&self) -> bool {
self.inner.has_nim_field_base()
}
fn __repr__(&self) -> String {
format!(
"NimLexicode(base_exp={}, len={}, min_distance={}, word_count={})",
self.inner.base_exp(),
self.inner.len(),
self.inner.min_distance(),
self.inner.word_count()
)
}
}
#[pyfunction]
fn lexicode(n: usize, d: usize) -> Option<PyBinaryCode> {
crate::games::lexicode(n, d).map(wrap_binary_code)
}
#[pyfunction]
fn lexicode_naive(n: usize, d: usize) -> Option<PyBinaryCode> {
crate::games::lexicode_naive(n, d).map(wrap_binary_code)
}
#[pyfunction]
fn lexicode_bounded(n: usize, d: usize, node_budget: u128) -> Option<PyBinaryCode> {
crate::games::lexicode_bounded(n, d, node_budget).map(wrap_binary_code)
}
#[pyfunction]
fn nim_lexicode_naive(base_exp: usize, n: usize, d: usize) -> Option<PyNimLexicode> {
crate::games::nim_lexicode_naive(base_exp, n, d).map(|inner| PyNimLexicode { inner })
}
#[pyfunction]
fn nim_lexicode_naive_bounded(
base_exp: usize,
n: usize,
d: usize,
node_budget: u128,
) -> Option<PyNimLexicode> {
crate::games::nim_lexicode_naive_bounded(base_exp, n, d, node_budget)
.map(|inner| PyNimLexicode { inner })
}
pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyGame>()?;
m.add_class::<PyOutcome>()?;
m.add_class::<PyPartizanOutcome>()?;
m.add_class::<PyLoopyWinner>()?;
m.add_class::<PyLoopyPartizanOutcome>()?;
m.add_class::<PyLoopyNimber>()?;
m.add_class::<PyColor>()?;
m.add_class::<PyPl>()?;
m.add_class::<PyThermograph>()?;
m.add_class::<PyNumberGame>()?;
m.add_class::<PyNimberGame>()?;
m.add_class::<PyGameExterior>()?;
m.add_class::<PyGameClifford>()?;
m.add_class::<PyGameRelation>()?;
m.add_class::<PyGameRelationCertificate>()?;
m.add_class::<PyRelationSearchCertificate>()?;
m.add_class::<PyScoreInterval>()?;
m.add_class::<PyHackenbush>()?;
m.add_class::<PyQuotient>()?;
m.add_class::<PyAbstractGame>()?;
m.add_class::<PyLoopyValue>()?;
m.add_class::<PyLoopyGraph>()?;
m.add_class::<PyLoopyPartizanGraph>()?;
m.add_class::<PyLoopyNimCertificate>()?;
m.add_class::<PyNimLexicode>()?;
m.add("LEXICODE_NODE_BUDGET", crate::games::LEXICODE_NODE_BUDGET)?;
m.add(
"NIM_LEXICODE_NODE_BUDGET",
crate::games::NIM_LEXICODE_NODE_BUDGET,
)?;
m.add_function(wrap_pyfunction!(nim_mul_mex, m)?)?;
m.add_function(wrap_pyfunction!(lexicode, m)?)?;
m.add_function(wrap_pyfunction!(lexicode_naive, m)?)?;
m.add_function(wrap_pyfunction!(lexicode_bounded, m)?)?;
m.add_function(wrap_pyfunction!(nim_lexicode_naive, m)?)?;
m.add_function(wrap_pyfunction!(nim_lexicode_naive_bounded, m)?)?;
m.add_function(wrap_pyfunction!(coin_companions, m)?)?;
m.add_function(wrap_pyfunction!(singleton_companions, m)?)?;
m.add_function(wrap_pyfunction!(turtles_companions, m)?)?;
m.add_function(wrap_pyfunction!(grundy_1d, m)?)?;
m.add_function(wrap_pyfunction!(coin_turning_grundy, m)?)?;
m.add_function(wrap_pyfunction!(coin_turning_tartan_grundy, m)?)?;
m.add_function(wrap_pyfunction!(tartan_grundy, m)?)?;
m.add_function(wrap_pyfunction!(grundy, m)?)?;
m.add_function(wrap_pyfunction!(grundy_graph, m)?)?;
m.add_function(wrap_pyfunction!(mex, m)?)?;
m.add_function(wrap_pyfunction!(outcomes, m)?)?;
m.add_function(wrap_pyfunction!(p_positions, m)?)?;
m.add_function(wrap_pyfunction!(scoring_values, m)?)?;
m.add_function(wrap_pyfunction!(nim_canonical, m)?)?;
m.add_function(wrap_pyfunction!(misere_nim_p_predicted, m)?)?;
m.add_function(wrap_pyfunction!(try_misere_is_n, m)?)?;
m.add_function(wrap_pyfunction!(misere_is_n, m)?)?;
m.add_function(wrap_pyfunction!(misere_is_p, m)?)?;
m.add_function(wrap_pyfunction!(nim_moves, m)?)?;
m.add_function(wrap_pyfunction!(octal_moves, m)?)?;
m.add_function(wrap_pyfunction!(octal_misere_quotient, m)?)?;
m.add_function(wrap_pyfunction!(misere_quotient, m)?)?;
m.add_function(wrap_pyfunction!(loopy_nim_values, m)?)?;
m.add_function(wrap_pyfunction!(loopy_decision_sets, m)?)?;
m.add_function(wrap_pyfunction!(loopy_quadric_probe, m)?)?;
m.add_function(wrap_pyfunction!(loopy_nim_values_certified, m)?)?;
m.add_function(wrap_pyfunction!(thermograph, m)?)?;
m.add_function(wrap_pyfunction!(thermograph_via_tropical, m)?)?;
m.add_function(wrap_pyfunction!(temperature, m)?)?;
m.add_function(wrap_pyfunction!(mean_value, m)?)?;
m.add_function(wrap_pyfunction!(heat, m)?)?;
m.add_function(wrap_pyfunction!(norton_multiply, m)?)?;
m.add_function(wrap_pyfunction!(overheat, m)?)?;
m.add_function(wrap_pyfunction!(left_stop, m)?)?;
m.add_function(wrap_pyfunction!(right_stop, m)?)?;
m.add_function(wrap_pyfunction!(atomic_weight, m)?)?;
m.add_function(wrap_pyfunction!(atomic_weight_int, m)?)?;
Ok(())
}