feat: Board 新增 Zobrist 哈希增量更新 + 3 测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 15:42:50 +08:00
parent cd44cbfc48
commit e6690a35fe
2 changed files with 70 additions and 1 deletions
+46 -1
View File
@@ -1,4 +1,4 @@
use crate::types::{CellState, Color, Move, MoveError, Position, MAX_BOARD_SIZE};
use crate::types::{CellState, Color, Move, MoveError, Position, ZobristHash, MAX_BOARD_SIZE};
/// 棋盘主体 — 不可变风格, place/undo 返回新 Board
#[derive(Debug, Clone, PartialEq)]
@@ -7,6 +7,7 @@ pub struct Board {
cells: [[CellState; MAX_BOARD_SIZE]; MAX_BOARD_SIZE],
history: Vec<Move>,
current_turn: u32,
pub zobrist_hash: ZobristHash,
}
impl Board {
@@ -22,6 +23,7 @@ impl Board {
cells: [[CellState::Empty; MAX_BOARD_SIZE]; MAX_BOARD_SIZE],
history: Vec::new(),
current_turn: 0,
zobrist_hash: 0,
}
}
@@ -33,6 +35,11 @@ impl Board {
self.cells[pos.x][pos.y]
}
/// 获取当前局面 Zobrist 哈希
pub fn hash(&self) -> ZobristHash {
self.zobrist_hash
}
/// 落子 — 返回新 Board (不可变)
pub fn place(&self, pos: Position, color: Color) -> Result<Board, MoveError> {
if pos.x >= self.size || pos.y >= self.size {
@@ -44,6 +51,9 @@ impl Board {
let mut new_board = self.clone();
new_board.cells[pos.x][pos.y] = CellState::Occupied(color);
let color_idx = if matches!(color, Color::Black) { 0 } else { 1 };
let zobrist = crate::types::init_zobrist_table(self.size);
new_board.zobrist_hash ^= zobrist[pos.x][pos.y][color_idx];
new_board.history.push(Move {
position: pos,
color,
@@ -104,6 +114,14 @@ impl Board {
let mut new_board = self.clone();
let last_move = new_board.history.pop().unwrap();
new_board.cells[last_move.position.x][last_move.position.y] = CellState::Empty;
let last_color_idx = if matches!(last_move.color, Color::Black) {
0
} else {
1
};
let zobrist = crate::types::init_zobrist_table(self.size);
new_board.zobrist_hash ^=
zobrist[last_move.position.x][last_move.position.y][last_color_idx];
new_board.current_turn = self.current_turn.saturating_sub(1);
Ok(new_board)
}
@@ -269,4 +287,31 @@ mod tests {
let _new = board.place(Position::new(7, 7), Color::Black).unwrap();
assert_eq!(board.get(Position::new(7, 7)), CellState::Empty);
}
#[test]
fn test_zobrist_hash_changes_on_place() {
let board = Board::new(15);
let h1 = board.hash();
let board2 = board.place(Position::new(7, 7), Color::Black).unwrap();
assert_ne!(h1, board2.hash());
}
#[test]
fn test_zobrist_hash_restores_on_undo() {
let board = Board::new(15);
let board = board.place(Position::new(7, 7), Color::Black).unwrap();
let h1 = board.hash();
let board = board.place(Position::new(7, 8), Color::White).unwrap();
assert_ne!(h1, board.hash());
let board = board.undo().unwrap();
assert_eq!(h1, board.hash());
}
#[test]
fn test_zobrist_hash_symmetry() {
let board = Board::new(15);
let b1 = board.place(Position::new(7, 7), Color::Black).unwrap();
let b2 = board.place(Position::new(7, 8), Color::Black).unwrap();
assert_ne!(b1.hash(), b2.hash());
}
}
+24
View File
@@ -152,3 +152,27 @@ impl Default for GameConfig {
}
}
}
/// Zobrist 哈希值
pub type ZobristHash = u64;
/// 获取全局 Zobrist 随机表(只初始化一次,使用 MAX_BOARD_SIZE 确保所有棋盘尺寸可用)
pub fn init_zobrist_table(_board_size: usize) -> &'static Vec<Vec<[ZobristHash; 2]>> {
use std::collections::hash_map::RandomState;
use std::hash::BuildHasher;
use std::sync::OnceLock;
static TABLE: OnceLock<Vec<Vec<[ZobristHash; 2]>>> = OnceLock::new();
TABLE.get_or_init(|| {
let size = MAX_BOARD_SIZE;
let rng = RandomState::new();
let mut table = Vec::with_capacity(size);
for x in 0..size {
let mut row = Vec::with_capacity(size);
for _y in 0..size {
row.push([rng.hash_one(&(x, _y, 0u8)), rng.hash_one(&(x, _y, 1u8))]);
}
table.push(row);
}
table
})
}