From 5230adacded60aa43f41735317ed0acb41e71566 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:00:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20AI=20=E6=A3=8B=E5=BD=A2=E8=AF=84?= =?UTF-8?q?=E5=88=86=E6=A8=A1=E5=9D=97=20=E2=80=94=20=E8=BF=9E=E4=BA=94/?= =?UTF-8?q?=E6=B4=BB=E5=9B=9B/=E5=86=B2=E5=9B=9B/=E6=B4=BB=E4=B8=89?= =?UTF-8?q?=E7=AD=89=E6=A3=8B=E5=BD=A2=E6=89=93=E5=88=86?= 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/evaluate.rs | 122 ++++++++++++++++++++++++++++++++++++++++ core/src/ai/mod.rs | 11 ++++ core/src/ai/search.rs | 2 + core/src/lib.rs | 1 + 4 files changed, 136 insertions(+) create mode 100644 core/src/ai/evaluate.rs create mode 100644 core/src/ai/mod.rs create mode 100644 core/src/ai/search.rs diff --git a/core/src/ai/evaluate.rs b/core/src/ai/evaluate.rs new file mode 100644 index 0000000..c931951 --- /dev/null +++ b/core/src/ai/evaluate.rs @@ -0,0 +1,122 @@ +use crate::board::Board; +use crate::types::{CellState, Color, Position}; + +/// 棋形分数 +const FIVE: f64 = 100000.0; +const OPEN_FOUR: f64 = 10000.0; +const RUSH_FOUR: f64 = 5000.0; +const OPEN_THREE: f64 = 1000.0; +const SLEEP_THREE: f64 = 500.0; +const OPEN_TWO: f64 = 100.0; +const SLEEP_TWO: f64 = 50.0; +const OPEN_ONE: f64 = 10.0; + +/// 评估整个棋盘对 player 的得分 (player得分 - 对手得分) +pub fn evaluate_board(board: &Board, player: Color) -> f64 { + let player_score = evaluate_player(board, player); + let opponent_score = evaluate_player(board, player.opponent()); + player_score - opponent_score +} + +fn evaluate_player(board: &Board, color: Color) -> f64 { + let directions: [(isize, isize); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)]; + let mut total = 0.0f64; + let size = board.size; + + for x in 0..size { + for y in 0..size { + if board.get(Position::new(x, y)) != CellState::Occupied(color) { + continue; + } + for &(dx, dy) in &directions { + let (count, start_open, end_open) = + scan_pattern(board, Position::new(x, y), color, dx, dy); + total += score_pattern(count, start_open, end_open); + } + } + } + total +} + +/// 从 pos 向 (dx,dy) 方向扫描, 只计数起点 +fn scan_pattern( + board: &Board, + pos: Position, + color: Color, + dx: isize, + dy: isize, +) -> (u32, bool, bool) { + let mut count = 1u32; + + // 正方向 + let mut nx = pos.x as isize + dx; + let mut ny = pos.y as isize + dy; + while in_bounds(board, nx, ny) + && board.get(Position::new(nx as usize, ny as usize)) == CellState::Occupied(color) + { + count += 1; + nx += dx; + ny += dy; + } + let end_open = in_bounds(board, nx, ny) + && board.get(Position::new(nx as usize, ny as usize)) == CellState::Empty; + + // 反方向 (检查是否是起点) + let sx = pos.x as isize - dx; + let sy = pos.y as isize - dy; + let start_open = in_bounds(board, sx, sy) + && board.get(Position::new(sx as usize, sy as usize)) == CellState::Empty; + + // 如果不是连续段的起点, 不计分 (避免重复) + if in_bounds(board, sx, sy) + && board.get(Position::new(sx as usize, sy as usize)) == CellState::Occupied(color) + { + return (0, false, false); + } + + (count, start_open, end_open) +} + +fn score_pattern(count: u32, start_open: bool, end_open: bool) -> f64 { + let open_count = start_open as u32 + end_open as u32; + match (count, open_count) { + (5, _) => FIVE, + (4, 2) => OPEN_FOUR, + (4, 1) => RUSH_FOUR, + (3, 2) => OPEN_THREE, + (3, 1) => SLEEP_THREE, + (2, 2) => OPEN_TWO, + (2, 1) => SLEEP_TWO, + (1, 2) => OPEN_ONE, + _ => 0.0, + } +} + +fn in_bounds(board: &Board, x: isize, y: isize) -> bool { + x >= 0 && y >= 0 && (x as usize) < board.size && (y as usize) < board.size +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::board::Board; + use crate::types::{Color, Position}; + + #[test] + fn test_evaluate_empty_board() { + let board = Board::new(15); + let score = evaluate_board(&board, Color::Black); + assert_eq!(score, 0.0); + } + + #[test] + fn test_five_in_a_row_high_score() { + let board = Board::new(15); + let mut board = board; + for y in 5..10 { + board = board.place(Position::new(7, y), Color::Black).unwrap(); + } + let score = evaluate_board(&board, Color::Black); + assert!(score > 10000.0); + } +} diff --git a/core/src/ai/mod.rs b/core/src/ai/mod.rs new file mode 100644 index 0000000..211a535 --- /dev/null +++ b/core/src/ai/mod.rs @@ -0,0 +1,11 @@ +use crate::board::Board; +use crate::types::{Color, Position}; + +/// AI 引擎统一接口 +pub trait AiEngine: Send + Sync { + /// 返回 AI 的最佳落子位置, 无位置返回 None + fn best_move(&self, board: &Board, color: Color) -> Option; +} + +pub mod evaluate; +pub mod search; diff --git a/core/src/ai/search.rs b/core/src/ai/search.rs new file mode 100644 index 0000000..5f909b5 --- /dev/null +++ b/core/src/ai/search.rs @@ -0,0 +1,2 @@ +//! AI 搜索模块 — 占位符, 待实现 Alpha-Beta 搜索 +//! TODO: 在 Task 6 中实现完整搜索逻辑 diff --git a/core/src/lib.rs b/core/src/lib.rs index 3712640..03ffdd1 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,5 +1,6 @@ // Gobang core library — 纯游戏逻辑,零 GUI 依赖 +pub mod ai; pub mod board; pub mod rules; pub mod types;