mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-29 00:45:55 +08:00
feat: Board 新增 Zobrist 哈希增量更新 + 3 测试
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+46
-1
@@ -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
|
/// 棋盘主体 — 不可变风格, place/undo 返回新 Board
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -7,6 +7,7 @@ pub struct Board {
|
|||||||
cells: [[CellState; MAX_BOARD_SIZE]; MAX_BOARD_SIZE],
|
cells: [[CellState; MAX_BOARD_SIZE]; MAX_BOARD_SIZE],
|
||||||
history: Vec<Move>,
|
history: Vec<Move>,
|
||||||
current_turn: u32,
|
current_turn: u32,
|
||||||
|
pub zobrist_hash: ZobristHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
@@ -22,6 +23,7 @@ impl Board {
|
|||||||
cells: [[CellState::Empty; MAX_BOARD_SIZE]; MAX_BOARD_SIZE],
|
cells: [[CellState::Empty; MAX_BOARD_SIZE]; MAX_BOARD_SIZE],
|
||||||
history: Vec::new(),
|
history: Vec::new(),
|
||||||
current_turn: 0,
|
current_turn: 0,
|
||||||
|
zobrist_hash: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +35,11 @@ impl Board {
|
|||||||
self.cells[pos.x][pos.y]
|
self.cells[pos.x][pos.y]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取当前局面 Zobrist 哈希
|
||||||
|
pub fn hash(&self) -> ZobristHash {
|
||||||
|
self.zobrist_hash
|
||||||
|
}
|
||||||
|
|
||||||
/// 落子 — 返回新 Board (不可变)
|
/// 落子 — 返回新 Board (不可变)
|
||||||
pub fn place(&self, pos: Position, color: Color) -> Result<Board, MoveError> {
|
pub fn place(&self, pos: Position, color: Color) -> Result<Board, MoveError> {
|
||||||
if pos.x >= self.size || pos.y >= self.size {
|
if pos.x >= self.size || pos.y >= self.size {
|
||||||
@@ -44,6 +51,9 @@ impl Board {
|
|||||||
|
|
||||||
let mut new_board = self.clone();
|
let mut new_board = self.clone();
|
||||||
new_board.cells[pos.x][pos.y] = CellState::Occupied(color);
|
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 {
|
new_board.history.push(Move {
|
||||||
position: pos,
|
position: pos,
|
||||||
color,
|
color,
|
||||||
@@ -104,6 +114,14 @@ impl Board {
|
|||||||
let mut new_board = self.clone();
|
let mut new_board = self.clone();
|
||||||
let last_move = new_board.history.pop().unwrap();
|
let last_move = new_board.history.pop().unwrap();
|
||||||
new_board.cells[last_move.position.x][last_move.position.y] = CellState::Empty;
|
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);
|
new_board.current_turn = self.current_turn.saturating_sub(1);
|
||||||
Ok(new_board)
|
Ok(new_board)
|
||||||
}
|
}
|
||||||
@@ -269,4 +287,31 @@ mod tests {
|
|||||||
let _new = board.place(Position::new(7, 7), Color::Black).unwrap();
|
let _new = board.place(Position::new(7, 7), Color::Black).unwrap();
|
||||||
assert_eq!(board.get(Position::new(7, 7)), CellState::Empty);
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user