use crate::forms::BinaryCode;
use crate::scalar::nim_mul;
use std::collections::HashSet;
pub const LEXICODE_NODE_BUDGET: u128 = 50_000_000_000;
pub const NIM_LEXICODE_NODE_BUDGET: u128 = 5_000_000_000;
pub const LEXICODE_TURNING_GAME_NODE_BUDGET: u128 = 200_000_000;
const LEXICODE_TURNING_GAME_MAX_GRUNDY_LEN: usize = 20;
const LEXICODE_TURNING_GAME_MAX_EXPLICIT_GRAPH_LEN: usize = 14;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LexicodeTurningGame {
word_len: usize,
min_distance: usize,
}
impl LexicodeTurningGame {
pub fn new(word_len: usize, min_distance: usize) -> Option<Self> {
if word_len == 0 || word_len >= u32::BITS as usize {
return None;
}
Some(Self {
word_len,
min_distance,
})
}
pub fn len(&self) -> usize {
self.word_len
}
pub fn is_empty(&self) -> bool {
self.word_len == 0
}
pub fn min_distance(&self) -> usize {
self.min_distance
}
pub fn position_count(&self) -> u128 {
1u128 << self.word_len
}
pub fn is_position(&self, position: u32) -> bool {
position < (1u32 << self.word_len)
}
pub fn is_legal_move(&self, from: u32, to: u32) -> bool {
if !self.is_position(from) || !self.is_position(to) || to >= from {
return false;
}
let changed = (from ^ to).count_ones() as usize;
changed > 0 && changed < self.min_distance
}
pub fn turning_masks_bounded(&self, node_budget: u128) -> Option<Vec<u32>> {
let mut budget = node_budget;
self.turning_masks_with_budget(&mut budget)
}
pub fn moves_bounded(&self, from: u32, node_budget: u128) -> Option<Vec<u32>> {
if !self.is_position(from) {
return None;
}
let mut out: Vec<u32> = self
.turning_masks_bounded(node_budget)?
.into_iter()
.filter_map(|turn| {
let to = from ^ turn;
(to < from).then_some(to)
})
.collect();
out.sort_unstable();
Some(out)
}
pub fn successors_bounded(&self, node_budget: u128) -> Option<Vec<Vec<usize>>> {
if self.word_len > LEXICODE_TURNING_GAME_MAX_EXPLICIT_GRAPH_LEN {
return None;
}
let size = 1usize << self.word_len;
let mut budget = node_budget;
let turn_masks = self.turning_masks_with_budget(&mut budget)?;
let ops = (size as u128).checked_mul(turn_masks.len() as u128)?;
if ops > budget {
return None;
}
let mut succ = vec![Vec::new(); size];
for (from, moves) in succ.iter_mut().enumerate() {
let from = from as u32;
for &turn in &turn_masks {
let to = from ^ turn;
if to < from {
moves.push(to as usize);
}
}
moves.sort_unstable();
}
Some(succ)
}
pub fn grundy_values_bounded(&self, node_budget: u128) -> Option<Vec<u128>> {
if self.word_len > LEXICODE_TURNING_GAME_MAX_GRUNDY_LEN {
return None;
}
let size = 1usize << self.word_len;
let mut budget = node_budget;
let turn_masks = self.turning_masks_with_budget(&mut budget)?;
let ops = (size as u128).checked_mul(turn_masks.len() as u128)?;
if ops > budget {
return None;
}
let mut values = Vec::with_capacity(size);
for from in 0..size {
let from = from as u32;
let option_values = turn_masks.iter().filter_map(|&turn| {
let to = from ^ turn;
(to < from).then(|| values[to as usize])
});
values.push(crate::games::grundy::mex(option_values));
}
Some(values)
}
pub fn p_positions_bounded(&self, node_budget: u128) -> Option<Vec<u32>> {
Some(
self.grundy_values_bounded(node_budget)?
.into_iter()
.enumerate()
.filter_map(|(position, g)| (g == 0).then_some(position as u32))
.collect(),
)
}
fn turning_masks_with_budget(&self, budget: &mut u128) -> Option<Vec<u32>> {
let max_weight = self.min_distance.saturating_sub(1).min(self.word_len);
let mut masks = Vec::new();
for weight in 1..=max_weight {
collect_turn_masks(self.word_len, weight, 0, 0, &mut masks, budget)?;
}
Some(masks)
}
}
pub fn lexicode_turning_game(n: usize, d: usize) -> Option<LexicodeTurningGame> {
LexicodeTurningGame::new(n, d)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NimLexicode {
base_exp: usize,
word_len: usize,
min_distance: usize,
words: Vec<u128>,
}
impl NimLexicode {
pub fn base_exp(&self) -> usize {
self.base_exp
}
pub fn base(&self) -> u128 {
1u128 << self.base_exp
}
pub fn len(&self) -> usize {
self.word_len
}
pub fn is_empty(&self) -> bool {
self.word_len == 0
}
pub fn min_distance(&self) -> usize {
self.min_distance
}
pub fn word_count(&self) -> usize {
self.words.len()
}
pub fn packed_words(&self) -> &[u128] {
&self.words
}
pub fn words(&self) -> Vec<Vec<u128>> {
self.words
.iter()
.map(|&w| decode_word(w, self.base(), self.word_len))
.collect()
}
pub fn f2_dimension(&self) -> Option<usize> {
self.words
.len()
.is_power_of_two()
.then(|| self.words.len().trailing_zeros() as usize)
}
pub fn is_closed_under_nim_add(&self) -> bool {
let set: HashSet<u128> = self.words.iter().copied().collect();
let base = self.base();
self.words.iter().all(|&a| {
self.words
.iter()
.all(|&b| set.contains(&nim_add_packed(a, b, base, self.word_len)))
})
}
pub fn is_closed_under_nim_scalars(&self) -> bool {
let set: HashSet<u128> = self.words.iter().copied().collect();
let base = self.base();
(0..base).all(|s| {
self.words.iter().all(|&w| {
nim_scalar_mul_packed(s, w, base, self.word_len).is_some_and(|sw| set.contains(&sw))
})
})
}
pub fn has_nim_field_base(&self) -> bool {
self.base_exp.is_power_of_two()
}
}
fn mask_to_row(mask: u32, n: usize) -> Vec<u8> {
(0..n).map(|i| ((mask >> (n - 1 - i)) & 1) as u8).collect()
}
fn checked_pow_u128(base: u128, exp: usize) -> Option<u128> {
let mut acc = 1u128;
for _ in 0..exp {
acc = acc.checked_mul(base)?;
}
Some(acc)
}
fn collect_turn_masks(
n: usize,
weight: usize,
start: usize,
mask: u32,
out: &mut Vec<u32>,
budget: &mut u128,
) -> Option<()> {
if weight == 0 {
if *budget == 0 {
return None;
}
*budget -= 1;
out.push(mask);
return Some(());
}
if n - start < weight {
return Some(());
}
for bit in start..=n - weight {
collect_turn_masks(n, weight - 1, bit + 1, mask | (1u32 << bit), out, budget)?;
}
Some(())
}
fn decode_word(mut code: u128, base: u128, n: usize) -> Vec<u128> {
let mut out = vec![0u128; n];
for slot in out.iter_mut().rev() {
*slot = code % base;
code /= base;
}
out
}
fn hamming_distance_packed(mut a: u128, mut b: u128, base: u128, n: usize) -> usize {
let mut dist = 0usize;
for _ in 0..n {
if a % base != b % base {
dist += 1;
}
a /= base;
b /= base;
}
dist
}
fn nim_add_packed(mut a: u128, mut b: u128, base: u128, n: usize) -> u128 {
let mut out = 0u128;
let mut place = 1u128;
for _ in 0..n {
let digit = (a % base) ^ (b % base);
out += digit * place;
place *= base;
a /= base;
b /= base;
}
out
}
fn nim_scalar_mul_packed(scalar: u128, mut word: u128, base: u128, n: usize) -> Option<u128> {
let mut out = 0u128;
let mut place = 1u128;
for _ in 0..n {
let digit = nim_mul(scalar, word % base);
if digit >= base {
return None;
}
out += digit * place;
place *= base;
word /= base;
}
Some(out)
}
fn bitmask_basis(vectors: &[u32]) -> Vec<u32> {
let mut basis: Vec<u32> = Vec::new();
for &v in vectors {
let mut x = v;
for &b in &basis {
let hb = 31 - b.leading_zeros();
if (x >> hb) & 1 == 1 {
x ^= b;
}
}
if x != 0 {
basis.push(x);
}
}
basis
}
pub fn lexicode_naive(n: usize, d: usize) -> Option<BinaryCode> {
if n == 0 || n > 14 {
return None;
}
let size: u32 = 1u32 << n;
let mut kept: Vec<u32> = Vec::new();
for m in 0..size {
if kept.iter().all(|&c| (m ^ c).count_ones() as usize >= d) {
kept.push(m);
}
}
let set: std::collections::HashSet<u32> = kept.iter().copied().collect();
for &a in &kept {
for &b in &kept {
if !set.contains(&(a ^ b)) {
return None;
}
}
}
let basis = bitmask_basis(&kept);
BinaryCode::new(n, basis.iter().map(|&v| mask_to_row(v, n)).collect())
}
pub fn lexicode(n: usize, d: usize) -> Option<BinaryCode> {
lexicode_bounded(n, d, LEXICODE_NODE_BUDGET)
}
pub fn lexicode_bounded(n: usize, d: usize, node_budget: u128) -> Option<BinaryCode> {
if n == 0 || n > 26 {
return None;
}
let size: usize = 1usize << n;
let mut dist: Vec<u8> = (0..size).map(|v| (v as u32).count_ones() as u8).collect();
let mut basis: Vec<u32> = Vec::new();
let mut budget = node_budget;
let mut cursor: usize = 1; loop {
while cursor < size && (dist[cursor] as usize) < d {
cursor += 1;
}
if cursor >= size {
break;
}
let g = cursor;
basis.push(g as u32);
for v in 0..size {
if budget == 0 {
return None;
}
budget -= 1;
let alt = dist[v ^ g];
if alt < dist[v] {
dist[v] = alt;
}
}
cursor += 1;
}
BinaryCode::new(n, basis.iter().map(|&g| mask_to_row(g, n)).collect())
}
pub fn nim_lexicode_naive(base_exp: usize, n: usize, d: usize) -> Option<NimLexicode> {
nim_lexicode_naive_bounded(base_exp, n, d, NIM_LEXICODE_NODE_BUDGET)
}
pub fn nim_lexicode_naive_bounded(
base_exp: usize,
n: usize,
d: usize,
node_budget: u128,
) -> Option<NimLexicode> {
if base_exp == 0 || base_exp >= u128::BITS as usize || n == 0 {
return None;
}
let base = 1u128 << base_exp;
let size = checked_pow_u128(base, n)?;
let mut budget = node_budget;
let mut kept = Vec::new();
for word in 0..size {
let mut keep = true;
for &c in &kept {
if budget == 0 {
return None;
}
budget -= 1;
if hamming_distance_packed(word, c, base, n) < d {
keep = false;
break;
}
}
if keep {
kept.push(word);
}
}
let code = NimLexicode {
base_exp,
word_len: n,
min_distance: d,
words: kept,
};
code.is_closed_under_nim_add().then_some(code)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::forms::{extended_hamming_code, golay_code, hamming_code};
use crate::games::grundy::{grundy_graph, mex};
fn greedy_masks(n: usize, d: usize) -> Vec<u32> {
let mut kept: Vec<u32> = Vec::new();
for m in 0..(1u32 << n) {
if kept.iter().all(|&c| (m ^ c).count_ones() as usize >= d) {
kept.push(m);
}
}
kept
}
#[test]
fn greedy_step_is_mex_of_the_forbidden_balls() {
let (n, d) = (6usize, 3usize);
let direct = greedy_masks(n, d);
let mut kept: Vec<u32> = Vec::new();
loop {
let forbidden: Vec<u128> = (0..(1u32 << n))
.filter(|&m| kept.iter().any(|&c| ((m ^ c).count_ones() as usize) < d))
.map(u128::from)
.collect();
let next = mex(forbidden);
if next >= (1u128 << n) {
break; }
kept.push(next as u32);
}
assert_eq!(
kept, direct,
"mex reconstruction must equal the greedy scan"
);
}
#[test]
fn turning_game_moves_are_lower_hamming_turns() {
let game = lexicode_turning_game(4, 3).unwrap();
assert_eq!(game.len(), 4);
assert_eq!(game.min_distance(), 3);
assert_eq!(game.position_count(), 16);
assert!(game.is_legal_move(0b1011, 0b1001)); assert!(game.is_legal_move(0b1011, 0b0001)); assert!(!game.is_legal_move(0b1011, 0b1111)); assert!(!game.is_legal_move(0b1011, 0b0000)); assert!(!game.is_legal_move(0b1011, 0b1011));
assert_eq!(
game.moves_bounded(0b1011, LEXICODE_TURNING_GAME_NODE_BUDGET)
.unwrap(),
vec![0b0001, 0b0010, 0b0011, 0b0111, 0b1000, 0b1001, 0b1010]
);
}
#[test]
fn turning_game_successors_match_generic_grundy_graph() {
let game = lexicode_turning_game(5, 3).unwrap();
let succ = game
.successors_bounded(LEXICODE_TURNING_GAME_NODE_BUDGET)
.unwrap();
let direct = game
.grundy_values_bounded(LEXICODE_TURNING_GAME_NODE_BUDGET)
.unwrap();
assert_eq!(grundy_graph(&succ).unwrap(), direct);
}
#[test]
fn turning_game_p_positions_are_the_lexicode() {
for n in 1..=9 {
for d in 1..=4 {
let game = lexicode_turning_game(n, d).unwrap();
let p_positions = game
.p_positions_bounded(LEXICODE_TURNING_GAME_NODE_BUDGET)
.unwrap();
assert_eq!(
p_positions,
greedy_masks(n, d),
"turning-game P-positions vs greedy scan at (n={n}, d={d})"
);
}
}
}
#[test]
fn turning_game_code_witnesses_hamming_examples() {
let h = lexicode_turning_game(7, 3)
.unwrap()
.p_positions_bounded(LEXICODE_TURNING_GAME_NODE_BUDGET)
.unwrap();
assert_eq!(h, greedy_masks(7, 3));
let eh = lexicode_turning_game(8, 4)
.unwrap()
.p_positions_bounded(LEXICODE_TURNING_GAME_NODE_BUDGET)
.unwrap();
assert_eq!(eh, greedy_masks(8, 4));
}
#[test]
fn turning_game_budget_is_honest() {
let game = lexicode_turning_game(8, 4).unwrap();
assert!(game.turning_masks_bounded(1).is_none());
assert!(game.grundy_values_bounded(1).is_none());
}
#[test]
fn naive_and_production_agree_for_small_n() {
for n in 1..=12 {
for d in 1..=4 {
let a = lexicode_naive(n, d);
let b = lexicode(n, d);
assert_eq!(a, b, "lexicode_naive vs lexicode at (n={n}, d={d})");
}
}
}
#[test]
fn distance_one_is_the_full_space_and_two_is_even_weight() {
let full = lexicode(5, 1).unwrap();
assert_eq!(full.len(), 5);
assert_eq!(full.dim(), 5);
let even = lexicode(5, 2).unwrap();
assert_eq!((even.len(), even.dim()), (5, 4));
assert_eq!(even.minimum_distance(), Some(2));
}
#[test]
fn nim_lexicode_repetition_codes_are_nim_add_closed() {
for base_exp in 1..=4 {
let code = nim_lexicode_naive(base_exp, 2, 2).unwrap();
let base = 1usize << base_exp;
assert_eq!(code.word_count(), base);
assert_eq!(code.f2_dimension(), Some(base_exp));
assert!(code.is_closed_under_nim_add());
assert_eq!(
code.words(),
(0..base as u128).map(|a| vec![a, a]).collect::<Vec<_>>()
);
}
}
#[test]
fn nim_lexicode_scalar_linearity_detects_fermat_bases() {
let base4 = nim_lexicode_naive(2, 2, 2).unwrap();
assert!(base4.has_nim_field_base());
assert!(base4.is_closed_under_nim_scalars());
let base16 = nim_lexicode_naive(4, 2, 2).unwrap();
assert!(base16.has_nim_field_base());
assert!(base16.is_closed_under_nim_scalars());
let base8 = nim_lexicode_naive(3, 2, 2).unwrap();
assert!(!base8.has_nim_field_base());
assert!(!base8.is_closed_under_nim_scalars());
}
#[test]
fn lexicode_reproduces_hamming_codes() {
let h = lexicode(7, 3).unwrap();
assert_eq!((h.len(), h.dim(), h.minimum_distance()), (7, 4, Some(3)));
let eh = lexicode(8, 4).unwrap();
assert_eq!((eh.len(), eh.dim(), eh.minimum_distance()), (8, 4, Some(4)));
assert_eq!(h.weight_enumerator(), hamming_code().weight_enumerator());
assert_eq!(
eh.weight_enumerator(),
extended_hamming_code().weight_enumerator()
);
}
#[test]
fn lexicode_24_8_is_golay_and_chains_to_a_lattice_with_roots() {
let g = lexicode(24, 8).expect("lexicode(24,8) within budget");
assert_eq!(g.len(), 24);
assert_eq!(g.dim(), 12);
assert_eq!(g.minimum_distance(), Some(8));
assert!(g.is_doubly_even());
assert!(g.is_self_dual());
assert_eq!(g.weight_enumerator(), golay_code().weight_enumerator());
let lattice = g
.construction_a()
.expect("doubly-even self-dual ⇒ integral Gram");
assert!(lattice.is_even());
assert!(lattice.is_unimodular());
let roots = lattice.short_vectors(2).expect("definite ⇒ enumerable");
assert!(
!roots.is_empty(),
"Golay Construction A has roots, unlike Leech"
);
}
}