1use std::fmt;
7
8pub const BOARD_SIZE: usize = 15;
9pub const NUM_CELLS: usize = BOARD_SIZE * BOARD_SIZE; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Stone {
13 Black,
14 White,
15}
16
17impl Stone {
18 pub fn opponent(self) -> Stone {
19 match self {
20 Stone::Black => Stone::White,
21 Stone::White => Stone::Black,
22 }
23 }
24}
25
26#[derive(Clone, Copy, PartialEq, Eq)]
29pub struct BitBoard {
30 pub lo: u128,
31 pub hi: u128,
32}
33
34impl BitBoard {
35 pub const EMPTY: Self = Self { lo: 0, hi: 0 };
36
37 #[inline]
38 pub fn get(&self, idx: usize) -> bool {
39 if idx < 128 {
40 (self.lo >> idx) & 1 != 0
41 } else {
42 (self.hi >> (idx - 128)) & 1 != 0
43 }
44 }
45
46 #[inline]
47 pub fn set(&mut self, idx: usize) {
48 if idx < 128 {
49 self.lo |= 1u128 << idx;
50 } else {
51 self.hi |= 1u128 << (idx - 128);
52 }
53 }
54
55 #[inline]
56 pub fn clear(&mut self, idx: usize) {
57 if idx < 128 {
58 self.lo &= !(1u128 << idx);
59 } else {
60 self.hi &= !(1u128 << (idx - 128));
61 }
62 }
63
64 #[inline]
65 pub fn or(&self, other: &BitBoard) -> BitBoard {
66 BitBoard {
67 lo: self.lo | other.lo,
68 hi: self.hi | other.hi,
69 }
70 }
71
72 #[inline]
73 pub fn count_ones(&self) -> u32 {
74 self.lo.count_ones() + self.hi.count_ones()
75 }
76
77 #[inline]
82 pub fn iter_ones(&self) -> BitBoardIter {
83 BitBoardIter {
84 lo: self.lo,
85 hi: self.hi,
86 }
87 }
88}
89
90pub struct BitBoardIter {
91 lo: u128,
92 hi: u128,
93}
94
95impl Iterator for BitBoardIter {
96 type Item = usize;
97 #[inline]
98 fn next(&mut self) -> Option<usize> {
99 if self.lo != 0 {
100 let idx = self.lo.trailing_zeros() as usize;
101 self.lo &= self.lo - 1;
102 Some(idx)
103 } else if self.hi != 0 {
104 let idx = 128 + self.hi.trailing_zeros() as usize;
105 self.hi &= self.hi - 1;
106 Some(idx)
107 } else {
108 None
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum GameResult {
115 BlackWin,
116 WhiteWin,
117 Draw,
118 Ongoing,
119}
120
121pub type Move = usize;
123
124#[inline]
125pub fn to_rc(idx: usize) -> (usize, usize) {
126 (idx / BOARD_SIZE, idx % BOARD_SIZE)
127}
128
129#[inline]
130pub fn to_idx(row: usize, col: usize) -> usize {
131 row * BOARD_SIZE + col
132}
133
134mod zobrist {
138 use super::{NUM_CELLS, Stone};
139
140 const fn splitmix64(seed: u64) -> u64 {
142 let mut x = seed;
143 x = x.wrapping_add(0x9E3779B97F4A7C15);
144 x = (x ^ (x >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
145 x = (x ^ (x >> 27)).wrapping_mul(0x94D049BB133111EB);
146 x ^ (x >> 31)
147 }
148
149 const fn build_keys() -> [[u64; NUM_CELLS]; 2] {
150 let mut out = [[0u64; NUM_CELLS]; 2];
151 let mut color = 0;
152 while color < 2 {
153 let mut cell = 0;
154 while cell < NUM_CELLS {
155 let seed = (color as u64) * 0x9E3779B97F4A7C15 ^ (cell as u64);
156 out[color][cell] = splitmix64(seed);
157 cell += 1;
158 }
159 color += 1;
160 }
161 out
162 }
163
164 pub const STONE_KEYS: [[u64; NUM_CELLS]; 2] = build_keys();
165 pub const SIDE_TO_MOVE_KEY: u64 = splitmix64(0xCAFE_BABE_DEAD_BEEF);
166
167 #[inline]
168 pub const fn key_for(stone: Stone, cell: usize) -> u64 {
169 let color = match stone {
170 Stone::Black => 0,
171 Stone::White => 1,
172 };
173 STONE_KEYS[color][cell]
174 }
175}
176
177pub use zobrist::SIDE_TO_MOVE_KEY as ZOBRIST_SIDE;
178
179#[inline]
180pub const fn zobrist_stone_key(stone: Stone, cell: usize) -> u64 {
181 zobrist::key_for(stone, cell)
182}
183
184pub type LinePatternState = Box<[[u16; 4]; NUM_CELLS]>;
192
193#[derive(Clone)]
194pub struct Board {
195 pub black: BitBoard,
196 pub white: BitBoard,
197 pub side_to_move: Stone,
198 pub move_count: usize,
199 pub last_move: Option<Move>,
200 pub history: Vec<Move>,
202 pub zobrist: u64,
206 pub line_pattern_ids: LinePatternState,
211 pub exact5: bool,
216}
217
218impl Board {
219 pub fn new() -> Self {
220 let mut b = Self {
221 black: BitBoard::EMPTY,
222 white: BitBoard::EMPTY,
223 side_to_move: Stone::Black,
224 move_count: 0,
225 last_move: None,
226 history: Vec::with_capacity(NUM_CELLS),
227 zobrist: 0,
229 line_pattern_ids: Box::new([[0u16; 4]; NUM_CELLS]),
232 exact5: false,
233 };
234 b.fill_initial_pattern_ids();
235 b
236 }
237
238 fn fill_initial_pattern_ids(&mut self) {
242 const DIRS: [(i32, i32); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
243 for cell in 0..NUM_CELLS {
244 let row = (cell / BOARD_SIZE) as i32;
245 let col = (cell % BOARD_SIZE) as i32;
246 for (dir_idx, &(dr, dc)) in DIRS.iter().enumerate() {
247 let w = crate::pattern_table::read_window(
248 &self.black,
249 &self.white,
250 row,
251 col,
252 dr,
253 dc,
254 );
255 let packed = crate::pattern_table::pack_window(&w);
256 self.line_pattern_ids[cell][dir_idx] =
257 crate::pattern_table::lookup_mapped_id(packed);
258 }
259 }
260 }
261
262 #[inline]
264 pub fn is_empty(&self, idx: usize) -> bool {
265 let occupied = self.black.or(&self.white);
266 !occupied.get(idx)
267 }
268
269 #[inline]
271 pub fn current_stones(&self) -> &BitBoard {
272 match self.side_to_move {
273 Stone::Black => &self.black,
274 Stone::White => &self.white,
275 }
276 }
277
278 #[inline]
280 pub fn opponent_stones(&self) -> &BitBoard {
281 match self.side_to_move {
282 Stone::Black => &self.white,
283 Stone::White => &self.black,
284 }
285 }
286
287 pub fn legal_moves(&self) -> Vec<Move> {
289 let occupied = self.black.or(&self.white);
290 let mut moves = Vec::with_capacity(NUM_CELLS - self.move_count);
291 for idx in 0..NUM_CELLS {
292 if !occupied.get(idx) {
293 moves.push(idx);
294 }
295 }
296 moves
297 }
298
299 pub fn candidate_moves(&self) -> Vec<Move> {
301 if self.move_count == 0 {
302 return vec![to_idx(7, 7)];
304 }
305
306 let occupied = self.black.or(&self.white);
307 let mut seen = [false; NUM_CELLS];
308 let mut moves = Vec::with_capacity(64);
309
310 for idx in 0..NUM_CELLS {
311 if !occupied.get(idx) {
312 continue;
313 }
314 let (r, c) = to_rc(idx);
315 for dr in -2i32..=2 {
316 for dc in -2i32..=2 {
317 if dr == 0 && dc == 0 {
318 continue;
319 }
320 let nr = r as i32 + dr;
321 let nc = c as i32 + dc;
322 if nr < 0 || nr >= BOARD_SIZE as i32 || nc < 0 || nc >= BOARD_SIZE as i32 {
323 continue;
324 }
325 let nidx = to_idx(nr as usize, nc as usize);
326 if !seen[nidx] && !occupied.get(nidx) {
327 seen[nidx] = true;
328 moves.push(nidx);
329 }
330 }
331 }
332 }
333
334 moves
335 }
336
337 pub fn make_move(&mut self, mv: Move) {
339 debug_assert!(mv < NUM_CELLS);
340 debug_assert!(self.is_empty(mv));
341
342 let placed = self.side_to_move;
343 match placed {
344 Stone::Black => self.black.set(mv),
345 Stone::White => self.white.set(mv),
346 }
347 self.zobrist ^= zobrist_stone_key(placed, mv);
349 self.zobrist ^= ZOBRIST_SIDE;
350
351 self.history.push(mv);
352 self.last_move = Some(mv);
353 self.move_count += 1;
354 self.side_to_move = placed.opponent();
355
356 self.update_line_patterns_around(mv);
359 }
360
361 pub fn undo_move(&mut self) {
363 if let Some(mv) = self.history.pop() {
364 self.side_to_move = self.side_to_move.opponent();
365 let placed = self.side_to_move;
366 self.move_count -= 1;
367 match placed {
368 Stone::Black => self.black.clear(mv),
369 Stone::White => self.white.clear(mv),
370 }
371 self.zobrist ^= zobrist_stone_key(placed, mv);
373 self.zobrist ^= ZOBRIST_SIDE;
374
375 self.last_move = self.history.last().copied();
376
377 self.update_line_patterns_around(mv);
380 }
381 }
382
383 #[inline]
387 fn update_line_patterns_around(&mut self, mv: Move) {
388 const DIRS: [(i32, i32); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
389 let row = (mv / BOARD_SIZE) as i32;
390 let col = (mv % BOARD_SIZE) as i32;
391 for (dir_idx, &(dr, dc)) in DIRS.iter().enumerate() {
392 for offset in -5i32..=5 {
393 let r = row + dr * offset;
394 let c = col + dc * offset;
395 if r < 0 || r >= BOARD_SIZE as i32 || c < 0 || c >= BOARD_SIZE as i32 {
396 continue;
397 }
398 let cell = (r as usize) * BOARD_SIZE + c as usize;
399 let w = crate::pattern_table::read_window(
400 &self.black,
401 &self.white,
402 r,
403 c,
404 dr,
405 dc,
406 );
407 let packed = crate::pattern_table::pack_window(&w);
408 self.line_pattern_ids[cell][dir_idx] =
409 crate::pattern_table::lookup_mapped_id(packed);
410 }
411 }
412 }
413
414 pub fn check_win(&self, mv: Move) -> bool {
416 let (row, col) = to_rc(mv);
417 let stone = if self.black.get(mv) {
418 &self.black
419 } else if self.white.get(mv) {
420 &self.white
421 } else {
422 return false;
423 };
424
425 let directions: [(i32, i32); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
427
428 for &(dr, dc) in &directions {
429 let mut count = 1;
430
431 for step in 1..5 {
433 let nr = row as i32 + dr * step;
434 let nc = col as i32 + dc * step;
435 if nr < 0 || nr >= BOARD_SIZE as i32 || nc < 0 || nc >= BOARD_SIZE as i32 {
436 break;
437 }
438 if stone.get(to_idx(nr as usize, nc as usize)) {
439 count += 1;
440 } else {
441 break;
442 }
443 }
444
445 for step in 1..5 {
447 let nr = row as i32 - dr * step;
448 let nc = col as i32 - dc * step;
449 if nr < 0 || nr >= BOARD_SIZE as i32 || nc < 0 || nc >= BOARD_SIZE as i32 {
450 break;
451 }
452 if stone.get(to_idx(nr as usize, nc as usize)) {
453 count += 1;
454 } else {
455 break;
456 }
457 }
458
459 if self.exact5 {
460 if count == 5 {
463 return true;
464 }
465 } else if count >= 5 {
466 return true;
468 }
469 }
470
471 false
472 }
473
474 pub fn game_result(&self) -> GameResult {
476 if let Some(mv) = self.last_move {
477 if self.check_win(mv) {
478 return match self.side_to_move {
480 Stone::Black => GameResult::WhiteWin,
481 Stone::White => GameResult::BlackWin,
482 };
483 }
484 }
485 if self.move_count >= NUM_CELLS {
486 GameResult::Draw
487 } else {
488 GameResult::Ongoing
489 }
490 }
491}
492
493impl fmt::Display for Board {
494 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495 write!(f, " ")?;
496 for c in 0..BOARD_SIZE {
497 write!(f, "{:2}", (b'A' + c as u8) as char)?;
498 }
499 writeln!(f)?;
500
501 for r in 0..BOARD_SIZE {
502 write!(f, "{:2} ", r + 1)?;
503 for c in 0..BOARD_SIZE {
504 let idx = to_idx(r, c);
505 if self.black.get(idx) {
506 write!(f, " X")?;
507 } else if self.white.get(idx) {
508 write!(f, " O")?;
509 } else {
510 write!(f, " .")?;
511 }
512 }
513 writeln!(f)?;
514 }
515 Ok(())
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 #[test]
524 fn test_make_undo_move() {
525 let mut board = Board::new();
526 let mv = to_idx(7, 7);
527 board.make_move(mv);
528 assert!(board.black.get(mv));
529 assert_eq!(board.side_to_move, Stone::White);
530
531 board.undo_move();
532 assert!(!board.black.get(mv));
533 assert_eq!(board.side_to_move, Stone::Black);
534 assert_eq!(board.move_count, 0);
535 }
536
537 #[test]
539 fn zobrist_make_undo_roundtrip() {
540 let mut board = Board::new();
541 let initial = board.zobrist;
542 assert_eq!(initial, 0, "empty board zobrist should be 0");
543
544 let moves = [112, 113, 97, 98, 127, 128, 200, 14];
545 let mut keys = vec![initial];
546 for &m in &moves {
547 board.make_move(m);
548 keys.push(board.zobrist);
549 }
550 let mut sorted = keys.clone();
552 sorted.sort_unstable();
553 sorted.dedup();
554 assert_eq!(sorted.len(), keys.len(), "zobrist sequence collided");
555
556 for i in (1..keys.len()).rev() {
558 board.undo_move();
559 assert_eq!(
560 board.zobrist, keys[i - 1],
561 "zobrist mismatch after undo step {i}"
562 );
563 }
564 assert_eq!(board.zobrist, 0, "zobrist did not return to 0 after full undo");
565 }
566
567 #[test]
571 fn line_pattern_state_make_undo_consistency() {
572 const DIRS: [(i32, i32); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
573 let moves = [112, 113, 97, 98, 127, 128, 200, 14, 0, 224, 7, 217, 50, 100];
574
575 let mut board = Board::new();
576 let initial_ids = board.line_pattern_ids.clone();
577
578 for (i, &mv) in moves.iter().enumerate() {
580 if !board.is_empty(mv) {
581 continue;
582 }
583 board.make_move(mv);
584
585 let mut fresh = Board::new();
587 for &m in &moves[..=i] {
588 if fresh.is_empty(m) {
589 fresh.make_move(m);
590 }
591 }
592 for cell in 0..NUM_CELLS {
596 let row = (cell / BOARD_SIZE) as i32;
597 let col = (cell % BOARD_SIZE) as i32;
598 for (dir_idx, &(dr, dc)) in DIRS.iter().enumerate() {
599 let w = crate::pattern_table::read_window(
600 &board.black,
601 &board.white,
602 row,
603 col,
604 dr,
605 dc,
606 );
607 let packed = crate::pattern_table::pack_window(&w);
608 let expected = crate::pattern_table::lookup_mapped_id(packed);
609 let actual = board.line_pattern_ids[cell][dir_idx];
610 assert_eq!(
611 actual, expected,
612 "mismatch at cell {} dir {} after move {} (ply {})",
613 cell, dir_idx, mv, i + 1
614 );
615 }
616 }
617 }
618
619 for _ in 0..moves.len() {
621 if !board.history.is_empty() {
622 board.undo_move();
623 }
624 }
625 assert_eq!(board.move_count, 0);
626 for cell in 0..NUM_CELLS {
628 for d in 0..4 {
629 assert_eq!(
630 board.line_pattern_ids[cell][d], initial_ids[cell][d],
631 "after full undo: cell {} dir {} not restored",
632 cell, d
633 );
634 }
635 }
636 }
637
638 #[test]
641 fn zobrist_path_independence() {
642 let seq1 = [112, 113, 97, 98]; let seq2 = [112, 98, 97, 113]; let seq2 = [97, 113, 112, 98];
650
651 let mut b1 = Board::new();
652 for &m in &seq1 { b1.make_move(m); }
653 let mut b2 = Board::new();
654 for &m in &seq2 { b2.make_move(m); }
655
656 assert_eq!(b1.black.lo, b2.black.lo);
657 assert_eq!(b1.black.hi, b2.black.hi);
658 assert_eq!(b1.white.lo, b2.white.lo);
659 assert_eq!(b1.white.hi, b2.white.hi);
660 assert_eq!(b1.side_to_move, b2.side_to_move);
661
662 assert_eq!(b1.zobrist, b2.zobrist, "same position should have same zobrist");
663 }
664
665 #[test]
666 fn test_horizontal_win() {
667 let mut board = Board::new();
668 for i in 0..5 {
671 board.make_move(to_idx(7, 3 + i)); if i < 4 {
673 board.make_move(to_idx(8, 3 + i)); }
675 }
676 assert_eq!(board.game_result(), GameResult::BlackWin);
677 }
678
679 #[test]
680 fn test_diagonal_win() {
681 let mut board = Board::new();
682 for i in 0..5 {
685 board.make_move(to_idx(i, i)); if i < 4 {
687 board.make_move(to_idx(i, i + 1)); }
689 }
690 assert_eq!(board.game_result(), GameResult::BlackWin);
691 }
692
693 #[test]
694 fn test_no_win_with_four() {
695 let mut board = Board::new();
696 for i in 0..4 {
697 board.make_move(to_idx(7, 3 + i)); board.make_move(to_idx(8, 3 + i)); }
700 assert_eq!(board.game_result(), GameResult::Ongoing);
701 }
702
703 #[test]
704 fn test_candidate_moves_first() {
705 let board = Board::new();
706 let moves = board.candidate_moves();
707 assert_eq!(moves, vec![to_idx(7, 7)]);
708 }
709}