use std::fmt;
pub const BOARD_SIZE: usize = 15;
pub const NUM_CELLS: usize = BOARD_SIZE * BOARD_SIZE;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stone {
Black,
White,
}
impl Stone {
pub fn opponent(self) -> Stone {
match self {
Stone::Black => Stone::White,
Stone::White => Stone::Black,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct BitBoard {
pub lo: u128,
pub hi: u128,
}
impl BitBoard {
pub const EMPTY: Self = Self { lo: 0, hi: 0 };
#[inline]
pub fn get(&self, idx: usize) -> bool {
if idx < 128 {
(self.lo >> idx) & 1 != 0
} else {
(self.hi >> (idx - 128)) & 1 != 0
}
}
#[inline]
pub fn set(&mut self, idx: usize) {
if idx < 128 {
self.lo |= 1u128 << idx;
} else {
self.hi |= 1u128 << (idx - 128);
}
}
#[inline]
pub fn clear(&mut self, idx: usize) {
if idx < 128 {
self.lo &= !(1u128 << idx);
} else {
self.hi &= !(1u128 << (idx - 128));
}
}
#[inline]
pub fn or(&self, other: &BitBoard) -> BitBoard {
BitBoard {
lo: self.lo | other.lo,
hi: self.hi | other.hi,
}
}
#[inline]
pub fn count_ones(&self) -> u32 {
self.lo.count_ones() + self.hi.count_ones()
}
#[inline]
pub fn iter_ones(&self) -> BitBoardIter {
BitBoardIter {
lo: self.lo,
hi: self.hi,
}
}
}
pub struct BitBoardIter {
lo: u128,
hi: u128,
}
impl Iterator for BitBoardIter {
type Item = usize;
#[inline]
fn next(&mut self) -> Option<usize> {
if self.lo != 0 {
let idx = self.lo.trailing_zeros() as usize;
self.lo &= self.lo - 1;
Some(idx)
} else if self.hi != 0 {
let idx = 128 + self.hi.trailing_zeros() as usize;
self.hi &= self.hi - 1;
Some(idx)
} else {
None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GameResult {
BlackWin,
WhiteWin,
Draw,
Ongoing,
}
pub type Move = usize;
#[inline]
pub fn to_rc(idx: usize) -> (usize, usize) {
(idx / BOARD_SIZE, idx % BOARD_SIZE)
}
#[inline]
pub fn to_idx(row: usize, col: usize) -> usize {
row * BOARD_SIZE + col
}
mod zobrist {
use super::{NUM_CELLS, Stone};
const fn splitmix64(seed: u64) -> u64 {
let mut x = seed;
x = x.wrapping_add(0x9E3779B97F4A7C15);
x = (x ^ (x >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
x = (x ^ (x >> 27)).wrapping_mul(0x94D049BB133111EB);
x ^ (x >> 31)
}
const fn build_keys() -> [[u64; NUM_CELLS]; 2] {
let mut out = [[0u64; NUM_CELLS]; 2];
let mut color = 0;
while color < 2 {
let mut cell = 0;
while cell < NUM_CELLS {
let seed = (color as u64) * 0x9E3779B97F4A7C15 ^ (cell as u64);
out[color][cell] = splitmix64(seed);
cell += 1;
}
color += 1;
}
out
}
pub const STONE_KEYS: [[u64; NUM_CELLS]; 2] = build_keys();
pub const SIDE_TO_MOVE_KEY: u64 = splitmix64(0xCAFE_BABE_DEAD_BEEF);
#[inline]
pub const fn key_for(stone: Stone, cell: usize) -> u64 {
let color = match stone {
Stone::Black => 0,
Stone::White => 1,
};
STONE_KEYS[color][cell]
}
}
pub use zobrist::SIDE_TO_MOVE_KEY as ZOBRIST_SIDE;
#[inline]
pub const fn zobrist_stone_key(stone: Stone, cell: usize) -> u64 {
zobrist::key_for(stone, cell)
}
pub type LinePatternState = Box<[[u16; 4]; NUM_CELLS]>;
#[derive(Clone)]
pub struct Board {
pub black: BitBoard,
pub white: BitBoard,
pub side_to_move: Stone,
pub move_count: usize,
pub last_move: Option<Move>,
pub history: Vec<Move>,
pub zobrist: u64,
pub line_pattern_ids: LinePatternState,
pub exact5: bool,
}
impl Board {
pub fn new() -> Self {
let mut b = Self {
black: BitBoard::EMPTY,
white: BitBoard::EMPTY,
side_to_move: Stone::Black,
move_count: 0,
last_move: None,
history: Vec::with_capacity(NUM_CELLS),
zobrist: 0,
line_pattern_ids: Box::new([[0u16; 4]; NUM_CELLS]),
exact5: false,
};
b.fill_initial_pattern_ids();
b
}
fn fill_initial_pattern_ids(&mut self) {
const DIRS: [(i32, i32); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
for cell in 0..NUM_CELLS {
let row = (cell / BOARD_SIZE) as i32;
let col = (cell % BOARD_SIZE) as i32;
for (dir_idx, &(dr, dc)) in DIRS.iter().enumerate() {
let w = crate::pattern_table::read_window(
&self.black,
&self.white,
row,
col,
dr,
dc,
);
let packed = crate::pattern_table::pack_window(&w);
self.line_pattern_ids[cell][dir_idx] =
crate::pattern_table::lookup_mapped_id(packed);
}
}
}
#[inline]
pub fn is_empty(&self, idx: usize) -> bool {
let occupied = self.black.or(&self.white);
!occupied.get(idx)
}
#[inline]
pub fn current_stones(&self) -> &BitBoard {
match self.side_to_move {
Stone::Black => &self.black,
Stone::White => &self.white,
}
}
#[inline]
pub fn opponent_stones(&self) -> &BitBoard {
match self.side_to_move {
Stone::Black => &self.white,
Stone::White => &self.black,
}
}
pub fn legal_moves(&self) -> Vec<Move> {
let occupied = self.black.or(&self.white);
let mut moves = Vec::with_capacity(NUM_CELLS - self.move_count);
for idx in 0..NUM_CELLS {
if !occupied.get(idx) {
moves.push(idx);
}
}
moves
}
pub fn candidate_moves(&self) -> Vec<Move> {
if self.move_count == 0 {
return vec![to_idx(7, 7)];
}
let occupied = self.black.or(&self.white);
let mut seen = [false; NUM_CELLS];
let mut moves = Vec::with_capacity(64);
for idx in 0..NUM_CELLS {
if !occupied.get(idx) {
continue;
}
let (r, c) = to_rc(idx);
for dr in -2i32..=2 {
for dc in -2i32..=2 {
if dr == 0 && dc == 0 {
continue;
}
let nr = r as i32 + dr;
let nc = c as i32 + dc;
if nr < 0 || nr >= BOARD_SIZE as i32 || nc < 0 || nc >= BOARD_SIZE as i32 {
continue;
}
let nidx = to_idx(nr as usize, nc as usize);
if !seen[nidx] && !occupied.get(nidx) {
seen[nidx] = true;
moves.push(nidx);
}
}
}
}
moves
}
pub fn make_move(&mut self, mv: Move) {
debug_assert!(mv < NUM_CELLS);
debug_assert!(self.is_empty(mv));
let placed = self.side_to_move;
match placed {
Stone::Black => self.black.set(mv),
Stone::White => self.white.set(mv),
}
self.zobrist ^= zobrist_stone_key(placed, mv);
self.zobrist ^= ZOBRIST_SIDE;
self.history.push(mv);
self.last_move = Some(mv);
self.move_count += 1;
self.side_to_move = placed.opponent();
self.update_line_patterns_around(mv);
}
pub fn undo_move(&mut self) {
if let Some(mv) = self.history.pop() {
self.side_to_move = self.side_to_move.opponent();
let placed = self.side_to_move;
self.move_count -= 1;
match placed {
Stone::Black => self.black.clear(mv),
Stone::White => self.white.clear(mv),
}
self.zobrist ^= zobrist_stone_key(placed, mv);
self.zobrist ^= ZOBRIST_SIDE;
self.last_move = self.history.last().copied();
self.update_line_patterns_around(mv);
}
}
#[inline]
fn update_line_patterns_around(&mut self, mv: Move) {
const DIRS: [(i32, i32); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
let row = (mv / BOARD_SIZE) as i32;
let col = (mv % BOARD_SIZE) as i32;
for (dir_idx, &(dr, dc)) in DIRS.iter().enumerate() {
for offset in -5i32..=5 {
let r = row + dr * offset;
let c = col + dc * offset;
if r < 0 || r >= BOARD_SIZE as i32 || c < 0 || c >= BOARD_SIZE as i32 {
continue;
}
let cell = (r as usize) * BOARD_SIZE + c as usize;
let w = crate::pattern_table::read_window(
&self.black,
&self.white,
r,
c,
dr,
dc,
);
let packed = crate::pattern_table::pack_window(&w);
self.line_pattern_ids[cell][dir_idx] =
crate::pattern_table::lookup_mapped_id(packed);
}
}
}
pub fn check_win(&self, mv: Move) -> bool {
let (row, col) = to_rc(mv);
let stone = if self.black.get(mv) {
&self.black
} else if self.white.get(mv) {
&self.white
} else {
return false;
};
let directions: [(i32, i32); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
for &(dr, dc) in &directions {
let mut count = 1;
for step in 1..5 {
let nr = row as i32 + dr * step;
let nc = col as i32 + dc * step;
if nr < 0 || nr >= BOARD_SIZE as i32 || nc < 0 || nc >= BOARD_SIZE as i32 {
break;
}
if stone.get(to_idx(nr as usize, nc as usize)) {
count += 1;
} else {
break;
}
}
for step in 1..5 {
let nr = row as i32 - dr * step;
let nc = col as i32 - dc * step;
if nr < 0 || nr >= BOARD_SIZE as i32 || nc < 0 || nc >= BOARD_SIZE as i32 {
break;
}
if stone.get(to_idx(nr as usize, nc as usize)) {
count += 1;
} else {
break;
}
}
if self.exact5 {
if count == 5 {
return true;
}
} else if count >= 5 {
return true;
}
}
false
}
pub fn game_result(&self) -> GameResult {
if let Some(mv) = self.last_move {
if self.check_win(mv) {
return match self.side_to_move {
Stone::Black => GameResult::WhiteWin,
Stone::White => GameResult::BlackWin,
};
}
}
if self.move_count >= NUM_CELLS {
GameResult::Draw
} else {
GameResult::Ongoing
}
}
}
impl fmt::Display for Board {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, " ")?;
for c in 0..BOARD_SIZE {
write!(f, "{:2}", (b'A' + c as u8) as char)?;
}
writeln!(f)?;
for r in 0..BOARD_SIZE {
write!(f, "{:2} ", r + 1)?;
for c in 0..BOARD_SIZE {
let idx = to_idx(r, c);
if self.black.get(idx) {
write!(f, " X")?;
} else if self.white.get(idx) {
write!(f, " O")?;
} else {
write!(f, " .")?;
}
}
writeln!(f)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_undo_move() {
let mut board = Board::new();
let mv = to_idx(7, 7);
board.make_move(mv);
assert!(board.black.get(mv));
assert_eq!(board.side_to_move, Stone::White);
board.undo_move();
assert!(!board.black.get(mv));
assert_eq!(board.side_to_move, Stone::Black);
assert_eq!(board.move_count, 0);
}
#[test]
fn zobrist_make_undo_roundtrip() {
let mut board = Board::new();
let initial = board.zobrist;
assert_eq!(initial, 0, "empty board zobrist should be 0");
let moves = [112, 113, 97, 98, 127, 128, 200, 14];
let mut keys = vec![initial];
for &m in &moves {
board.make_move(m);
keys.push(board.zobrist);
}
let mut sorted = keys.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), keys.len(), "zobrist sequence collided");
for i in (1..keys.len()).rev() {
board.undo_move();
assert_eq!(
board.zobrist, keys[i - 1],
"zobrist mismatch after undo step {i}"
);
}
assert_eq!(board.zobrist, 0, "zobrist did not return to 0 after full undo");
}
#[test]
fn line_pattern_state_make_undo_consistency() {
const DIRS: [(i32, i32); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
let moves = [112, 113, 97, 98, 127, 128, 200, 14, 0, 224, 7, 217, 50, 100];
let mut board = Board::new();
let initial_ids = board.line_pattern_ids.clone();
for (i, &mv) in moves.iter().enumerate() {
if !board.is_empty(mv) {
continue;
}
board.make_move(mv);
let mut fresh = Board::new();
for &m in &moves[..=i] {
if fresh.is_empty(m) {
fresh.make_move(m);
}
}
for cell in 0..NUM_CELLS {
let row = (cell / BOARD_SIZE) as i32;
let col = (cell % BOARD_SIZE) as i32;
for (dir_idx, &(dr, dc)) in DIRS.iter().enumerate() {
let w = crate::pattern_table::read_window(
&board.black,
&board.white,
row,
col,
dr,
dc,
);
let packed = crate::pattern_table::pack_window(&w);
let expected = crate::pattern_table::lookup_mapped_id(packed);
let actual = board.line_pattern_ids[cell][dir_idx];
assert_eq!(
actual, expected,
"mismatch at cell {} dir {} after move {} (ply {})",
cell, dir_idx, mv, i + 1
);
}
}
}
for _ in 0..moves.len() {
if !board.history.is_empty() {
board.undo_move();
}
}
assert_eq!(board.move_count, 0);
for cell in 0..NUM_CELLS {
for d in 0..4 {
assert_eq!(
board.line_pattern_ids[cell][d], initial_ids[cell][d],
"after full undo: cell {} dir {} not restored",
cell, d
);
}
}
}
#[test]
fn zobrist_path_independence() {
let seq1 = [112, 113, 97, 98]; let seq2 = [112, 98, 97, 113];
let seq2 = [97, 113, 112, 98];
let mut b1 = Board::new();
for &m in &seq1 { b1.make_move(m); }
let mut b2 = Board::new();
for &m in &seq2 { b2.make_move(m); }
assert_eq!(b1.black.lo, b2.black.lo);
assert_eq!(b1.black.hi, b2.black.hi);
assert_eq!(b1.white.lo, b2.white.lo);
assert_eq!(b1.white.hi, b2.white.hi);
assert_eq!(b1.side_to_move, b2.side_to_move);
assert_eq!(b1.zobrist, b2.zobrist, "same position should have same zobrist");
}
#[test]
fn test_horizontal_win() {
let mut board = Board::new();
for i in 0..5 {
board.make_move(to_idx(7, 3 + i)); if i < 4 {
board.make_move(to_idx(8, 3 + i)); }
}
assert_eq!(board.game_result(), GameResult::BlackWin);
}
#[test]
fn test_diagonal_win() {
let mut board = Board::new();
for i in 0..5 {
board.make_move(to_idx(i, i)); if i < 4 {
board.make_move(to_idx(i, i + 1)); }
}
assert_eq!(board.game_result(), GameResult::BlackWin);
}
#[test]
fn test_no_win_with_four() {
let mut board = Board::new();
for i in 0..4 {
board.make_move(to_idx(7, 3 + i)); board.make_move(to_idx(8, 3 + i)); }
assert_eq!(board.game_result(), GameResult::Ongoing);
}
#[test]
fn test_candidate_moves_first() {
let board = Board::new();
let moves = board.candidate_moves();
assert_eq!(moves, vec![to_idx(7, 7)]);
}
}