mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-29 00:45:55 +08:00
feat: VCF/VCT 杀棋搜索 — 连续冲四/活三取胜 + 3 测试
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -10,3 +10,4 @@ pub trait AiEngine: Send + Sync {
|
|||||||
pub mod evaluate;
|
pub mod evaluate;
|
||||||
pub mod killer;
|
pub mod killer;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
pub mod vcf;
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
use crate::board::Board;
|
||||||
|
use crate::rules;
|
||||||
|
use crate::types::{CellState, Color, Position};
|
||||||
|
|
||||||
|
/// VCF 搜索 — 连续冲四取胜。返回取胜序列第一步
|
||||||
|
pub fn vcf_search(board: &Board, color: Color, max_depth: usize) -> Option<Position> {
|
||||||
|
vcf_inner(board, color, max_depth).map(|seq| seq[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vcf_inner(board: &Board, color: Color, depth: usize) -> Option<Vec<Position>> {
|
||||||
|
if depth == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let candidates = board.get_candidate_moves();
|
||||||
|
for &pos in &candidates {
|
||||||
|
if rules::is_forbidden(board, pos, color) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(new_board) = board.place(pos, color) {
|
||||||
|
if new_board.check_win(pos) {
|
||||||
|
return Some(vec![pos]);
|
||||||
|
}
|
||||||
|
if is_rush_four(&new_board, pos, color) {
|
||||||
|
let opp = color.opponent();
|
||||||
|
if let Some(block) = find_unique_block(&new_board, pos, color) {
|
||||||
|
if let Ok(b2) = new_board.place(block, opp) {
|
||||||
|
if let Some(mut rest) = vcf_inner(&b2, color, depth - 2) {
|
||||||
|
rest.insert(0, pos);
|
||||||
|
return Some(rest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VCT 搜索 — 连续活三/冲四混合取胜
|
||||||
|
pub fn vct_search(board: &Board, color: Color, max_depth: usize) -> Option<Position> {
|
||||||
|
vct_inner(board, color, max_depth).map(|seq| seq[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vct_inner(board: &Board, color: Color, depth: usize) -> Option<Vec<Position>> {
|
||||||
|
if depth == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let candidates = board.get_candidate_moves();
|
||||||
|
for &pos in &candidates {
|
||||||
|
if rules::is_forbidden(board, pos, color) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(new_board) = board.place(pos, color) {
|
||||||
|
if new_board.check_win(pos) {
|
||||||
|
return Some(vec![pos]);
|
||||||
|
}
|
||||||
|
if is_threat(&new_board, pos, color) {
|
||||||
|
let opp = color.opponent();
|
||||||
|
let defenses = find_threat_defenses(&new_board, pos, color);
|
||||||
|
if defenses.len() == 1 {
|
||||||
|
if let Ok(b2) = new_board.place(defenses[0], opp) {
|
||||||
|
if let Some(mut rest) = vct_inner(&b2, color, depth - 2) {
|
||||||
|
rest.insert(0, pos);
|
||||||
|
return Some(rest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_rush_four(board: &Board, pos: Position, color: Color) -> bool {
|
||||||
|
let directions: [(isize, isize); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
|
||||||
|
for (dx, dy) in directions {
|
||||||
|
let (count, start_open, end_open) = scan_vcf(board, pos, color, dx, dy);
|
||||||
|
if count == 4 && (start_open || end_open) && !(start_open && end_open) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_threat(board: &Board, pos: Position, color: Color) -> bool {
|
||||||
|
let directions: [(isize, isize); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
|
||||||
|
for (dx, dy) in directions {
|
||||||
|
let (count, start_open, end_open) = scan_vcf(board, pos, color, dx, dy);
|
||||||
|
if (count == 3 && start_open && end_open) || (count == 4 && (start_open || end_open)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_unique_block(board: &Board, pos: Position, color: Color) -> Option<Position> {
|
||||||
|
let directions: [(isize, isize); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
|
||||||
|
for (dx, dy) in directions {
|
||||||
|
let (count, start_open, end_open) = scan_vcf(board, pos, color, dx, dy);
|
||||||
|
if count == 4 {
|
||||||
|
if start_open {
|
||||||
|
// 扫描开放端找到空位
|
||||||
|
let nx = pos.x as isize - dx * count as isize;
|
||||||
|
let ny = pos.y as isize - dy * count as isize;
|
||||||
|
if nx >= 0
|
||||||
|
&& ny >= 0
|
||||||
|
&& (nx as usize) < board.size
|
||||||
|
&& (ny as usize) < board.size
|
||||||
|
&& board.get(Position::new(nx as usize, ny as usize)) == CellState::Empty
|
||||||
|
{
|
||||||
|
return Some(Position::new(nx as usize, ny as usize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if end_open {
|
||||||
|
let nx = pos.x as isize + dx * count as isize;
|
||||||
|
let ny = pos.y as isize + dy * count as isize;
|
||||||
|
if nx >= 0
|
||||||
|
&& ny >= 0
|
||||||
|
&& (nx as usize) < board.size
|
||||||
|
&& (ny as usize) < board.size
|
||||||
|
&& board.get(Position::new(nx as usize, ny as usize)) == CellState::Empty
|
||||||
|
{
|
||||||
|
return Some(Position::new(nx as usize, ny as usize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_threat_defenses(board: &Board, pos: Position, color: Color) -> Vec<Position> {
|
||||||
|
let mut defenses = Vec::new();
|
||||||
|
let directions: [(isize, isize); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
|
||||||
|
for (dx, dy) in directions {
|
||||||
|
let (count, start_open, _end_open) = scan_vcf(board, pos, color, dx, dy);
|
||||||
|
if count >= 3 {
|
||||||
|
if start_open {
|
||||||
|
let sx = pos.x as isize - dx * count as isize;
|
||||||
|
let sy = pos.y as isize - dy * count as isize;
|
||||||
|
if sx >= 0 && sy >= 0 && (sx as usize) < board.size && (sy as usize) < board.size {
|
||||||
|
defenses.push(Position::new(sx as usize, sy as usize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ex = pos.x as isize + dx * count as isize;
|
||||||
|
let ey = pos.y as isize + dy * count as isize;
|
||||||
|
if ex >= 0 && ey >= 0 && (ex as usize) < board.size && (ey as usize) < board.size {
|
||||||
|
defenses.push(Position::new(ex as usize, ey as usize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defenses.sort();
|
||||||
|
defenses.dedup();
|
||||||
|
defenses
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_vcf(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 let Some(cell) = cell_at(board, nx, ny) {
|
||||||
|
if cell == CellState::Occupied(color) {
|
||||||
|
count += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nx += dx;
|
||||||
|
ny += dy;
|
||||||
|
}
|
||||||
|
let end_open = cell_at(board, nx, ny) == Some(CellState::Empty);
|
||||||
|
|
||||||
|
let mut nx = pos.x as isize - dx;
|
||||||
|
let mut ny = pos.y as isize - dy;
|
||||||
|
while let Some(cell) = cell_at(board, nx, ny) {
|
||||||
|
if cell == CellState::Occupied(color) {
|
||||||
|
count += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nx -= dx;
|
||||||
|
ny -= dy;
|
||||||
|
}
|
||||||
|
let start_open = cell_at(board, nx, ny) == Some(CellState::Empty);
|
||||||
|
|
||||||
|
(count, start_open, end_open)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_at(board: &Board, x: isize, y: isize) -> Option<CellState> {
|
||||||
|
if x < 0 || y < 0 || (x as usize) >= board.size || (y as usize) >= board.size {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(board.get(Position::new(x as usize, y as usize)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::board::Board;
|
||||||
|
use crate::types::Color;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vcf_empty_board_returns_none() {
|
||||||
|
let board = Board::new(15);
|
||||||
|
assert!(vcf_search(&board, Color::Black, 6).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vct_empty_board_returns_none() {
|
||||||
|
let board = Board::new(15);
|
||||||
|
let board = board.place(Position::new(7, 7), Color::Black).unwrap();
|
||||||
|
assert!(vct_search(&board, Color::Black, 6).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vcf_detects_rush_four() {
|
||||||
|
let board = Board::new(15);
|
||||||
|
let mut board = board;
|
||||||
|
// 黑冲四: (7,3)(7,4)(7,5)(7,6) — 一端堵一端开放
|
||||||
|
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 result = vcf_search(&board, Color::Black, 4);
|
||||||
|
// 应该能找到直接五连((7,7)或(7,2)),取决于哪个空
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user