From e6690a35fe450c2026c2edba8d6b88d68e60ab1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Sun, 31 May 2026 15:42:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Board=20=E6=96=B0=E5=A2=9E=20Zobrist=20?= =?UTF-8?q?=E5=93=88=E5=B8=8C=E5=A2=9E=E9=87=8F=E6=9B=B4=E6=96=B0=20+=203?= =?UTF-8?q?=20=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- core/src/board.rs | 47 ++++++++++++++++++++++++++++++++++++++++++++++- core/src/types.rs | 24 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/core/src/board.rs b/core/src/board.rs index 79a1156..65fb467 100644 --- a/core/src/board.rs +++ b/core/src/board.rs @@ -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, 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 { 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()); + } } diff --git a/core/src/types.rs b/core/src/types.rs index 0589565..5c65f9b 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -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> { + use std::collections::hash_map::RandomState; + use std::hash::BuildHasher; + use std::sync::OnceLock; + static TABLE: OnceLock>> = 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 + }) +}