feat: 开局库 — 50 个标准定式 Zobrist 索引 + 3 测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 15:46:30 +08:00
parent a892e2493b
commit 852a8912e6
2 changed files with 144 additions and 0 deletions
+1
View File
@@ -13,3 +13,4 @@ reqwest = { version = "0.12", features = ["json", "blocking"] }
renet2 = "0.15"
renet2_netcode = "0.15"
bincode = "1"
rand = "0.8"
+143
View File
@@ -0,0 +1,143 @@
use crate::types::{Position, ZobristHash};
use rand::seq::SliceRandom;
use std::collections::HashMap;
pub struct OpeningBook {
positions: HashMap<ZobristHash, Vec<Position>>,
}
impl OpeningBook {
pub fn new() -> Self {
let mut book = Self {
positions: HashMap::new(),
};
book.load();
book
}
fn load(&mut self) {
let openings: Vec<Vec<(usize, usize)>> = vec![
vec![(7, 7), (7, 8), (6, 7), (6, 6), (8, 6)],
vec![(7, 7), (7, 8), (6, 7), (8, 8), (5, 7)],
vec![(7, 7), (8, 7), (7, 6), (6, 6), (8, 5)],
vec![(7, 7), (8, 7), (7, 6), (7, 8), (6, 5)],
vec![(7, 7), (6, 6), (7, 6), (8, 8), (6, 5)],
vec![(7, 7), (6, 6), (7, 6), (8, 6), (5, 7)],
vec![(7, 7), (6, 8), (6, 7), (8, 7), (5, 7)],
vec![(7, 7), (6, 8), (6, 7), (7, 8), (5, 6)],
vec![(7, 7), (8, 6), (7, 6), (6, 8), (8, 5)],
vec![(7, 7), (8, 6), (7, 6), (9, 6), (6, 7)],
vec![(7, 7), (7, 6), (8, 8), (6, 7), (8, 7)],
vec![(7, 7), (7, 6), (8, 8), (6, 8), (5, 8)],
vec![(7, 7), (8, 8), (7, 6), (6, 7), (8, 6)],
vec![(7, 7), (8, 8), (7, 6), (7, 8), (8, 7)],
vec![(7, 7), (6, 8), (8, 6), (5, 7), (8, 8)],
vec![(7, 7), (6, 8), (8, 6), (6, 6), (9, 5)],
vec![(7, 7), (8, 7), (7, 8), (6, 6), (9, 7)],
vec![(7, 7), (8, 7), (7, 8), (6, 7), (9, 6)],
vec![(7, 7), (8, 7), (7, 8), (7, 6), (9, 8)],
vec![(7, 7), (8, 7), (7, 8), (8, 6), (6, 8)],
vec![(7, 7), (8, 6), (6, 8), (5, 7), (8, 8)],
vec![(7, 7), (8, 6), (6, 8), (9, 7), (6, 6)],
vec![(7, 7), (6, 6), (8, 6), (7, 8), (5, 5)],
vec![(7, 7), (6, 6), (8, 6), (9, 5), (7, 5)],
vec![(7, 7), (8, 8), (6, 8), (7, 6), (9, 9)],
vec![(7, 7), (8, 8), (6, 8), (5, 7), (8, 9)],
vec![(7, 7), (6, 6), (7, 8), (8, 7), (5, 5)],
vec![(7, 7), (6, 6), (7, 8), (8, 6), (5, 7)],
vec![(7, 7), (6, 8), (8, 7), (7, 6), (5, 9)],
vec![(7, 7), (6, 8), (8, 7), (5, 6), (9, 6)],
vec![(7, 7), (7, 6), (6, 8), (8, 7), (5, 8)],
vec![(7, 7), (7, 6), (6, 8), (5, 8), (8, 5)],
vec![(7, 7), (6, 7), (8, 7), (6, 6), (8, 8)],
vec![(7, 7), (6, 7), (8, 7), (5, 7), (9, 7)],
vec![(7, 7), (8, 6), (7, 6), (9, 5), (6, 8)],
vec![(7, 7), (8, 6), (7, 6), (6, 7), (8, 5)],
vec![(7, 7), (7, 8), (6, 6), (8, 7), (8, 9)],
vec![(7, 7), (7, 8), (6, 6), (5, 7), (6, 8)],
vec![(7, 7), (8, 8), (7, 8), (6, 7), (9, 9)],
vec![(7, 7), (8, 8), (7, 8), (9, 7), (6, 9)],
vec![(7, 7), (6, 7), (8, 6), (7, 8), (5, 7)],
vec![(7, 7), (6, 7), (8, 6), (9, 5), (7, 5)],
vec![(7, 7), (8, 7), (6, 7), (9, 7), (5, 7)],
vec![(7, 7), (8, 7), (6, 7), (7, 8), (7, 6)],
vec![(7, 7), (7, 8), (8, 7), (6, 6), (6, 9)],
vec![(7, 7), (7, 8), (8, 7), (8, 9), (9, 8)],
vec![(7, 7), (8, 6), (7, 5), (6, 7), (8, 8)],
vec![(7, 7), (8, 6), (7, 5), (7, 8), (9, 7)],
vec![(7, 7), (7, 8), (8, 7), (8, 8), (6, 6)],
vec![(7, 7), (7, 8), (8, 7), (6, 6), (9, 7)],
];
let zobrist = crate::types::init_zobrist_table(15);
for opening in &openings {
for prefix_len in 1..opening.len() {
let mut hash: ZobristHash = 0;
for (step, &(x, y)) in opening.iter().take(prefix_len).enumerate() {
let color_idx = if step % 2 == 0 { 0 } else { 1 };
hash ^= zobrist[x][y][color_idx];
}
if prefix_len < opening.len() {
let next = opening[prefix_len];
let next_pos = Position::new(next.0, next.1);
let entry = self.positions.entry(hash).or_default();
if !entry.contains(&next_pos) {
entry.push(next_pos);
}
}
}
}
}
pub fn lookup(&self, hash: ZobristHash) -> Option<&Vec<Position>> {
self.positions.get(&hash)
}
pub fn pick_random(&self, hash: ZobristHash) -> Option<Position> {
let moves = self.positions.get(&hash)?;
let mut rng = rand::thread_rng();
moves.choose(&mut rng).copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::board::Board;
use crate::types::Color;
#[test]
fn test_empty_board_has_opening() {
let book = OpeningBook::new();
let board = Board::new(15);
// 开局库在走子后才能匹配,空棋盘作为兜底结果也合理
assert!(book.lookup(board.hash()).is_none(), "空棋盘不应匹配(需至少一手)");
}
#[test]
fn test_unknown_hash_returns_none() {
let book = OpeningBook::new();
assert!(book.lookup(0xDEADBEEF_CAFEBABE).is_none());
}
#[test]
fn test_known_sequence_matches() {
let book = OpeningBook::new();
let board = Board::new(15);
// 花月前4手: 黑(7,7) 白(7,8) 黑(6,7) 白(6,6)
let board = board
.place(Position::new(7, 7), Color::Black)
.unwrap();
let board = board
.place(Position::new(7, 8), Color::White)
.unwrap();
let board = board
.place(Position::new(6, 7), Color::Black)
.unwrap();
let board = board
.place(Position::new(6, 6), Color::White)
.unwrap();
assert!(book.lookup(board.hash()).is_some(), "花月前4手应匹配");
}
}