From 556ee39a288b9fb007d10cc57882940f1d4907f1 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 00:03:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20AI=20Alpha-Beta=20=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=20=E2=80=94=20Negamax=20+=20=E5=89=AA=E6=9E=9D=20+=20?= =?UTF-8?q?=E5=90=AF=E5=8F=91=E5=BC=8F=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- core/src/ai/search.rs | 191 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 2 deletions(-) diff --git a/core/src/ai/search.rs b/core/src/ai/search.rs index 5f909b5..fdd68ff 100644 --- a/core/src/ai/search.rs +++ b/core/src/ai/search.rs @@ -1,2 +1,189 @@ -//! AI 搜索模块 — 占位符, 待实现 Alpha-Beta 搜索 -//! TODO: 在 Task 6 中实现完整搜索逻辑 +use crate::ai::evaluate::evaluate_board; +use crate::ai::AiEngine; +use crate::board::Board; +use crate::types::{Color, Position}; + +/// Alpha-Beta AI 引擎 +pub struct AlphaBetaAi { + depth: usize, +} + +impl AlphaBetaAi { + pub fn new(depth: usize) -> Self { + Self { depth } + } +} + +impl AiEngine for AlphaBetaAi { + fn best_move(&self, board: &Board, color: Color) -> Option { + let candidates = board.get_candidate_moves(); + if candidates.is_empty() { + return None; + } + + let mut best_pos = None; + let mut best_score = f64::NEG_INFINITY; + + for &pos in &candidates { + if let Ok(new_board) = board.place(pos, color) { + if new_board.check_win(pos) { + return Some(pos); + } + let score = -self.negamax( + &new_board, + self.depth - 1, + f64::NEG_INFINITY, + f64::INFINITY, + color.opponent(), + ); + if score > best_score { + best_score = score; + best_pos = Some(pos); + } + } + } + + best_pos + } +} + +impl AlphaBetaAi { + fn negamax( + &self, + board: &Board, + depth: usize, + mut alpha: f64, + beta: f64, + color: Color, + ) -> f64 { + if depth == 0 { + return evaluate_board(board, color); + } + + let candidates = board.get_candidate_moves(); + if candidates.is_empty() { + return evaluate_board(board, color); + } + + // 启发式排序:先评估每步棋,优先搜索高分走法 + let mut scored: Vec<(Position, f64)> = candidates + .into_iter() + .filter_map(|pos| { + board.place(pos, color).ok().map(|b| { + if b.check_win(pos) { + (pos, f64::INFINITY) + } else { + let s = evaluate_board(&b, color); + (pos, s) + } + }) + }) + .collect(); + scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + + let mut max_val = f64::NEG_INFINITY; + for (pos, _) in scored { + if let Ok(new_board) = board.place(pos, color) { + if new_board.check_win(pos) { + return f64::INFINITY; + } + let val = -self.negamax( + &new_board, + depth - 1, + -beta, + -alpha, + color.opponent(), + ); + if val > max_val { + max_val = val; + } + if val > alpha { + alpha = val; + } + if alpha >= beta { + break; + } + } + } + max_val + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ai::AiEngine; + use crate::board::Board; + use crate::types::{Color, Position}; + + #[test] + fn test_ai_returns_center_on_empty_board() { + let board = Board::new(15); + let ai = AlphaBetaAi::new(1); + let mv = ai.best_move(&board, Color::Black); + assert!(mv.is_some()); + let pos = mv.unwrap(); + assert!(pos.x >= 6 && pos.x <= 8); + assert!(pos.y >= 6 && pos.y <= 8); + } + + #[test] + fn test_ai_blocks_rush_four() { + // 白棋活三 (一端被己方黑棋堵住, 只有一端开放) + 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 mut board = board; + board = board.place(Position::new(7, 3), Color::Black).unwrap(); + board = board.place(Position::new(7, 4), Color::Black).unwrap(); + board = board.place(Position::new(7, 5), Color::Black).unwrap(); + board = board.place(Position::new(7, 6), Color::Black).unwrap(); + let ai = AlphaBetaAi::new(3); + let mv = ai.best_move(&board, Color::Black).unwrap(); + assert!( + (mv.x == 7 && mv.y == 2) || (mv.x == 7 && mv.y == 7), + "AI should take winning move, got ({},{})", + mv.x, + mv.y + ); + } +}