feat: 迭代加深 + TT + Killer + 开局库 + VCF/VCT 集成 AI 引擎

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 15:50:44 +08:00
parent 852a8912e6
commit e216ae46dd
+173 -82
View File
@@ -1,60 +1,171 @@
use crate::ai::evaluate::evaluate_board; use crate::ai::evaluate::evaluate_board;
use crate::ai::killer::KillerTable;
use crate::ai::opening::OpeningBook;
use crate::ai::trans_table::{BoundType, TransTable};
use crate::ai::vcf;
use crate::ai::AiEngine; use crate::ai::AiEngine;
use crate::board::Board; use crate::board::Board;
use crate::rules; use crate::rules;
use crate::types::{Color, Position}; use crate::types::{Color, Position};
use std::time::{Duration, Instant};
const TIME_LIMITS: [u64; 5] = [1, 2, 3, 5, 8];
/// Alpha-Beta AI 引擎
#[derive(Clone)] #[derive(Clone)]
pub struct AlphaBetaAi { pub struct AlphaBetaAi {
depth: usize, difficulty: usize,
} }
impl AlphaBetaAi { impl AlphaBetaAi {
pub fn new(depth: usize) -> Self { pub fn new(difficulty: usize) -> Self {
Self { depth } Self { difficulty }
}
fn time_limit(&self) -> Duration {
let idx = self.difficulty.saturating_sub(1).min(4);
Duration::from_secs(TIME_LIMITS[idx])
} }
} }
impl AiEngine for AlphaBetaAi { impl AiEngine for AlphaBetaAi {
fn best_move(&self, board: &Board, color: Color) -> Option<Position> { fn best_move(&self, board: &Board, color: Color) -> Option<Position> {
// 1. 开局库(前 7 手)
if board.history().len() < 7 {
let book = OpeningBook::new();
if let Some(pos) = book.pick_random(board.hash()) {
return Some(pos);
}
}
// 2. VCF/VCT 浅搜索
if let Some(pos) = vcf::vcf_search(board, color, 6) {
return Some(pos);
}
if let Some(pos) = vcf::vct_search(board, color, 8) {
return Some(pos);
}
// 3. 迭代加深 Alpha-Beta
let candidates = board.get_candidate_moves(); let candidates = board.get_candidate_moves();
if candidates.is_empty() { if candidates.is_empty() {
return None; return None;
} }
let start = Instant::now();
let time_limit = self.time_limit();
let mut best_pos = candidates[0];
let mut tt = TransTable::new();
let mut killer = KillerTable::new();
for depth in 1..=20u32 {
if start.elapsed() >= time_limit {
break;
}
let (pos, completed) = self.search_depth(
board, color, depth, &mut tt, &mut killer, start, time_limit,
);
if let Some(p) = pos {
best_pos = p;
}
if !completed {
break;
}
}
Some(best_pos)
}
}
impl AlphaBetaAi {
fn search_depth(
&self, board: &Board, color: Color, depth: u32,
tt: &mut TransTable, killer: &mut KillerTable,
start: Instant, time_limit: Duration,
) -> (Option<Position>, bool) {
let candidates = board.get_candidate_moves();
if candidates.is_empty() {
return (None, true);
}
let mut best_pos = None; let mut best_pos = None;
let mut best_score = f64::NEG_INFINITY; let mut best_score = f64::NEG_INFINITY;
let mut alpha = f64::NEG_INFINITY;
let beta = f64::INFINITY;
let mut completed = true;
for &pos in &candidates { // 启发式排序: killer + 立即五连 + evaluate
// 禁手检查: 黑棋不能走禁手位置 let killer_moves = killer.get(depth as usize);
if rules::is_forbidden(board, pos, color) { let mut scored: Vec<(Position, f64)> = candidates
continue; .iter()
.filter(|&&p| !rules::is_forbidden(board, p, color))
.filter_map(|&p| {
board.place(p, color).ok().map(|b| {
if b.check_win(p) { (p, f64::INFINITY) }
else { (p, evaluate_board(&b, color)) }
})
})
.collect();
scored.sort_by(|a, b| {
let a_k = killer_moves.contains(&Some(a.0));
let b_k = killer_moves.contains(&Some(b.0));
if a_k && !b_k { std::cmp::Ordering::Less }
else if !a_k && b_k { std::cmp::Ordering::Greater }
else { b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal) }
});
for (pos, _) in scored {
if start.elapsed() >= time_limit {
completed = false;
break;
} }
if let Ok(new_board) = board.place(pos, color) { if let Ok(new_board) = board.place(pos, color) {
if new_board.check_win(pos) { if new_board.check_win(pos) {
return Some(pos); return (Some(pos), true);
} }
let score = -self.negamax( let score = -self.negamax(
&new_board, &new_board, depth - 1, -beta, -alpha, color.opponent(),
self.depth - 1, tt, killer, start, time_limit,
f64::NEG_INFINITY,
f64::INFINITY,
color.opponent(),
); );
if score > best_score { if score > best_score {
best_score = score; best_score = score;
best_pos = Some(pos); best_pos = Some(pos);
} }
if score > alpha {
alpha = score;
}
} }
} }
best_pos (best_pos, completed)
} }
}
impl AlphaBetaAi { fn negamax(
fn negamax(&self, board: &Board, depth: usize, mut alpha: f64, beta: f64, color: Color) -> f64 { &self, board: &Board, depth: u32, mut alpha: f64, beta: f64, color: Color,
tt: &mut TransTable, killer: &mut KillerTable,
start: Instant, time_limit: Duration,
) -> f64 {
if start.elapsed() >= time_limit {
return evaluate_board(board, color);
}
// 置换表
let hash = board.hash();
let alpha_orig = alpha;
if let Some(entry) = tt.probe(hash, depth as u8) {
match entry.bound {
BoundType::Exact => return entry.score as f64,
BoundType::LowerBound => alpha = alpha.max(entry.score as f64),
BoundType::UpperBound =>
if (entry.score as f64) <= alpha { return entry.score as f64; },
}
if alpha >= beta { return entry.score as f64; }
}
if depth == 0 { if depth == 0 {
return evaluate_board(board, color); return evaluate_board(board, color);
} }
@@ -64,41 +175,63 @@ impl AlphaBetaAi {
return evaluate_board(board, color); return evaluate_board(board, color);
} }
// 启发式排序:先评估每步棋,优先搜索高分走法 (跳过禁手) // 启发式排序
let killer_moves = killer.get(depth as usize);
let mut scored: Vec<(Position, f64)> = candidates let mut scored: Vec<(Position, f64)> = candidates
.into_iter() .into_iter()
.filter(|&pos| !rules::is_forbidden(board, pos, color)) .filter(|&p| !rules::is_forbidden(board, p, color))
.filter_map(|pos| { .filter_map(|p| {
board.place(pos, color).ok().map(|b| { board.place(p, color).ok().map(|b| {
if b.check_win(pos) { if b.check_win(p) { (p, f64::INFINITY) }
(pos, f64::INFINITY) else { (p, evaluate_board(&b, color)) }
} else {
let s = evaluate_board(&b, color);
(pos, s)
}
}) })
}) })
.collect(); .collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scored.sort_by(|a, b| {
let a_k = killer_moves.contains(&Some(a.0));
let b_k = killer_moves.contains(&Some(b.0));
if a_k && !b_k { std::cmp::Ordering::Less }
else if !a_k && b_k { std::cmp::Ordering::Greater }
else { b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal) }
});
let mut max_val = f64::NEG_INFINITY; let mut max_val = f64::NEG_INFINITY;
let mut best_move = None;
for (pos, _) in scored { for (pos, _) in scored {
if start.elapsed() >= time_limit {
break;
}
if let Ok(new_board) = board.place(pos, color) { if let Ok(new_board) = board.place(pos, color) {
if new_board.check_win(pos) { if new_board.check_win(pos) {
tt.store(hash, depth as u8, f64::INFINITY as i32, BoundType::Exact, Some(pos));
return f64::INFINITY; return f64::INFINITY;
} }
let val = -self.negamax(&new_board, depth - 1, -beta, -alpha, color.opponent()); let val = -self.negamax(
&new_board, depth - 1, -beta, -alpha, color.opponent(),
tt, killer, start, time_limit,
);
if val > max_val { if val > max_val {
max_val = val; max_val = val;
best_move = Some(pos);
} }
if val > alpha { if val > alpha {
alpha = val; alpha = val;
} }
if alpha >= beta { if alpha >= beta {
killer.record(depth as usize, pos);
break; break;
} }
} }
} }
let bound = if max_val <= alpha_orig { BoundType::UpperBound }
else if max_val >= beta { BoundType::LowerBound }
else { BoundType::Exact };
tt.store(hash, depth as u8, max_val as i32, bound, best_move);
max_val max_val
} }
} }
@@ -106,65 +239,25 @@ impl AlphaBetaAi {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::ai::AiEngine;
use crate::board::Board; use crate::board::Board;
use crate::types::{Color, Position}; use crate::types::{Color, Position};
#[test] #[test]
fn test_ai_returns_center_on_empty_board() { fn test_time_limits() {
assert_eq!(AlphaBetaAi::new(1).time_limit(), Duration::from_secs(1));
assert_eq!(AlphaBetaAi::new(5).time_limit(), Duration::from_secs(8));
}
#[test]
fn test_ai_returns_move_on_empty_board() {
let board = Board::new(15); let board = Board::new(15);
let ai = AlphaBetaAi::new(1); let ai = AlphaBetaAi::new(3);
let mv = ai.best_move(&board, Color::Black); let mv = ai.best_move(&board, Color::Black);
assert!(mv.is_some()); assert!(mv.is_some());
let pos = mv.unwrap();
assert!(pos.x >= 6 && pos.x <= 8);
assert!(pos.y >= 6 && pos.y <= 8);
} }
#[test] #[test]
fn test_ai_blocks_rush_four() { fn test_ai_takes_winning_move() {
// 白棋活三 (一端被己方黑棋堵住, 只有一端开放)
let board = Board::new(15);
let mut board = board;
board = board.place(Position::new(7, 1), Color::Black).unwrap();
board = board.place(Position::new(7, 2), Color::White).unwrap();
board = board.place(Position::new(7, 3), Color::White).unwrap();
board = board.place(Position::new(7, 4), Color::White).unwrap();
board = board.place(Position::new(7, 5), Color::White).unwrap();
let ai = AlphaBetaAi::new(3);
let mv = ai.best_move(&board, Color::Black).unwrap();
assert_eq!(
mv,
Position::new(7, 6),
"AI should block rush four at (7,6), got ({},{})",
mv.x,
mv.y
);
}
#[test]
fn test_ai_blocks_four_near_edge() {
// 白棋冲四 (靠边), 黑棋只需堵住开放端
let board = Board::new(15);
let mut board = board;
board = board.place(Position::new(7, 0), Color::White).unwrap();
board = board.place(Position::new(7, 1), Color::White).unwrap();
board = board.place(Position::new(7, 2), Color::White).unwrap();
board = board.place(Position::new(7, 3), Color::White).unwrap();
let ai = AlphaBetaAi::new(3);
let mv = ai.best_move(&board, Color::Black).unwrap();
assert_eq!(
mv,
Position::new(7, 4),
"AI should block four at (7,4), got ({},{})",
mv.x,
mv.y
);
}
#[test]
fn test_ai_takes_win() {
// 黑棋连四, (7,2) 和 (7,7) 都是胜着
let board = Board::new(15); let board = Board::new(15);
let mut board = board; let mut board = board;
board = board.place(Position::new(7, 3), Color::Black).unwrap(); board = board.place(Position::new(7, 3), Color::Black).unwrap();
@@ -175,9 +268,7 @@ mod tests {
let mv = ai.best_move(&board, Color::Black).unwrap(); let mv = ai.best_move(&board, Color::Black).unwrap();
assert!( assert!(
(mv.x == 7 && mv.y == 2) || (mv.x == 7 && mv.y == 7), (mv.x == 7 && mv.y == 2) || (mv.x == 7 && mv.y == 7),
"AI should take winning move, got ({},{})", "AI should win, got ({},{})", mv.x, mv.y
mv.x,
mv.y
); );
} }
} }