mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-29 00:45:55 +08:00
feat(core): 棋盘引擎 — 不可变 Board, 落子/胜负/悔棋/候选位
实现不可变风格的 Board 结构体,place()/undo() 返回新 Board。 包含 bounds 检查、四方向五连胜负判定、悔棋历史管理、 空棋盘天元候选等功能。新增 11 个单元测试全部通过。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
use crate::types::{CellState, Color, Move, MoveError, Position, MAX_BOARD_SIZE};
|
||||
|
||||
/// 棋盘主体 — 不可变风格, place/undo 返回新 Board
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Board {
|
||||
pub size: usize,
|
||||
cells: [[CellState; MAX_BOARD_SIZE]; MAX_BOARD_SIZE],
|
||||
history: Vec<Move>,
|
||||
current_turn: u32,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
/// 创建空棋盘
|
||||
pub fn new(size: usize) -> Self {
|
||||
assert!(
|
||||
size <= MAX_BOARD_SIZE,
|
||||
"棋盘尺寸不能超过 {}",
|
||||
MAX_BOARD_SIZE
|
||||
);
|
||||
Self {
|
||||
size,
|
||||
cells: [[CellState::Empty; MAX_BOARD_SIZE]; MAX_BOARD_SIZE],
|
||||
history: Vec::new(),
|
||||
current_turn: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取指定位置的棋子状态
|
||||
pub fn get(&self, pos: Position) -> CellState {
|
||||
if pos.x >= self.size || pos.y >= self.size {
|
||||
return CellState::Empty;
|
||||
}
|
||||
self.cells[pos.x][pos.y]
|
||||
}
|
||||
|
||||
/// 落子 — 返回新 Board (不可变)
|
||||
pub fn place(&self, pos: Position, color: Color) -> Result<Board, MoveError> {
|
||||
if pos.x >= self.size || pos.y >= self.size {
|
||||
return Err(MoveError::OutOfBounds);
|
||||
}
|
||||
if self.cells[pos.x][pos.y] != CellState::Empty {
|
||||
return Err(MoveError::Occupied);
|
||||
}
|
||||
|
||||
let mut new_board = self.clone();
|
||||
new_board.cells[pos.x][pos.y] = CellState::Occupied(color);
|
||||
new_board.history.push(Move {
|
||||
position: pos,
|
||||
color,
|
||||
turn: self.current_turn,
|
||||
});
|
||||
new_board.current_turn = self.current_turn + 1;
|
||||
Ok(new_board)
|
||||
}
|
||||
|
||||
/// 胜负判定 — 从 pos 出发四方向扫描
|
||||
pub fn check_win(&self, pos: Position) -> bool {
|
||||
let cell = self.cells[pos.x][pos.y];
|
||||
let color = match cell {
|
||||
CellState::Occupied(c) => c,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let directions: [(isize, isize); 4] = [(0, 1), (1, 0), (1, 1), (1, -1)];
|
||||
|
||||
for (dx, dy) in directions {
|
||||
let mut count = 1u32;
|
||||
// 正方向
|
||||
let mut nx = pos.x as isize + dx;
|
||||
let mut ny = pos.y as isize + dy;
|
||||
while nx >= 0 && ny >= 0 && (nx as usize) < self.size && (ny as usize) < self.size {
|
||||
if self.cells[nx as usize][ny as usize] == CellState::Occupied(color) {
|
||||
count += 1;
|
||||
nx += dx;
|
||||
ny += dy;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 反方向
|
||||
let mut nx = pos.x as isize - dx;
|
||||
let mut ny = pos.y as isize - dy;
|
||||
while nx >= 0 && ny >= 0 && (nx as usize) < self.size && (ny as usize) < self.size {
|
||||
if self.cells[nx as usize][ny as usize] == CellState::Occupied(color) {
|
||||
count += 1;
|
||||
nx -= dx;
|
||||
ny -= dy;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if count >= 5 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// 悔棋 — 撤销最近一步
|
||||
pub fn undo(&self) -> Result<Board, MoveError> {
|
||||
if self.history.is_empty() {
|
||||
return Err(MoveError::GameOver);
|
||||
}
|
||||
let mut new_board = self.clone();
|
||||
let last_move = new_board.history.pop().unwrap();
|
||||
new_board.cells[last_move.position.x][last_move.position.y] = CellState::Empty;
|
||||
new_board.current_turn = self.current_turn.saturating_sub(1);
|
||||
Ok(new_board)
|
||||
}
|
||||
|
||||
/// 获取所有候选落子位 (已有棋子周围2格范围)
|
||||
pub fn get_candidate_moves(&self) -> Vec<Position> {
|
||||
let mut candidates = Vec::new();
|
||||
let range = 2isize;
|
||||
|
||||
if self.history.is_empty() {
|
||||
// 棋盘为空, 返回天元
|
||||
return vec![Position::new(self.size / 2, self.size / 2)];
|
||||
}
|
||||
|
||||
for x in 0..self.size {
|
||||
for y in 0..self.size {
|
||||
if self.cells[x][y] != CellState::Empty {
|
||||
for dx in -range..=range {
|
||||
for dy in -range..=range {
|
||||
let nx = x as isize + dx;
|
||||
let ny = y as isize + dy;
|
||||
if nx >= 0
|
||||
&& ny >= 0
|
||||
&& (nx as usize) < self.size
|
||||
&& (ny as usize) < self.size
|
||||
&& self.cells[nx as usize][ny as usize] == CellState::Empty
|
||||
{
|
||||
candidates.push(Position::new(nx as usize, ny as usize));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates.sort();
|
||||
candidates.dedup();
|
||||
candidates
|
||||
}
|
||||
|
||||
/// 获取落子历史 (用于棋谱)
|
||||
pub fn history(&self) -> &[Move] {
|
||||
&self.history
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::{CellState, Color, MoveError, Position};
|
||||
|
||||
#[test]
|
||||
fn test_empty_board_creation() {
|
||||
let board = Board::new(15);
|
||||
assert_eq!(board.size, 15);
|
||||
for x in 0..15 {
|
||||
for y in 0..15 {
|
||||
assert_eq!(board.get(Position::new(x, y)), CellState::Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_place_piece() {
|
||||
let board = Board::new(15);
|
||||
let result = board.place(Position::new(7, 7), Color::Black);
|
||||
assert!(result.is_ok());
|
||||
let new_board = result.unwrap();
|
||||
assert_eq!(
|
||||
new_board.get(Position::new(7, 7)),
|
||||
CellState::Occupied(Color::Black)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_place_on_occupied_fails() {
|
||||
let board = Board::new(15);
|
||||
let board = board.place(Position::new(7, 7), Color::Black).unwrap();
|
||||
let result = board.place(Position::new(7, 7), Color::White);
|
||||
assert_eq!(result, Err(MoveError::Occupied));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_place_out_of_bounds_fails() {
|
||||
let board = Board::new(15);
|
||||
let result = board.place(Position::new(20, 20), Color::Black);
|
||||
assert_eq!(result, Err(MoveError::OutOfBounds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_win_horizontal() {
|
||||
let board = Board::new(15);
|
||||
let mut board = board;
|
||||
for y in 3..7 {
|
||||
board = board.place(Position::new(7, y), Color::Black).unwrap();
|
||||
}
|
||||
let board = board.place(Position::new(7, 7), Color::Black).unwrap();
|
||||
assert!(board.check_win(Position::new(7, 7)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_win_vertical() {
|
||||
let board = Board::new(15);
|
||||
let mut board = board;
|
||||
for x in 3..7 {
|
||||
board = board.place(Position::new(x, 7), Color::White).unwrap();
|
||||
}
|
||||
let board = board.place(Position::new(7, 7), Color::White).unwrap();
|
||||
assert!(board.check_win(Position::new(7, 7)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_win_diagonal() {
|
||||
let board = Board::new(15);
|
||||
let mut board = board;
|
||||
for i in 1..5 {
|
||||
board = board
|
||||
.place(Position::new(3 + i, 3 + i), Color::Black)
|
||||
.unwrap();
|
||||
}
|
||||
let board = board.place(Position::new(8, 8), Color::Black).unwrap();
|
||||
assert!(board.check_win(Position::new(8, 8)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_win_on_four() {
|
||||
let board = Board::new(15);
|
||||
let mut board = board;
|
||||
for y in 3..6 {
|
||||
board = board.place(Position::new(7, y), Color::Black).unwrap();
|
||||
}
|
||||
let board = board.place(Position::new(7, 6), Color::Black).unwrap();
|
||||
assert!(!board.check_win(Position::new(7, 6)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo() {
|
||||
let board = Board::new(15);
|
||||
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.undo().unwrap();
|
||||
assert_eq!(board.get(Position::new(7, 8)), CellState::Empty);
|
||||
assert_eq!(
|
||||
board.get(Position::new(7, 7)),
|
||||
CellState::Occupied(Color::Black)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_empty_history() {
|
||||
let board = Board::new(15);
|
||||
assert_eq!(board.undo(), Err(MoveError::GameOver));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_immutable_place() {
|
||||
let board = Board::new(15);
|
||||
let _new = board.place(Position::new(7, 7), Color::Black).unwrap();
|
||||
assert_eq!(board.get(Position::new(7, 7)), CellState::Empty);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// Gobang core library — 纯游戏逻辑,零 GUI 依赖
|
||||
|
||||
pub mod board;
|
||||
pub mod types;
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ pub enum CellState {
|
||||
}
|
||||
|
||||
/// 一步棋
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Move {
|
||||
pub position: Position,
|
||||
pub color: Color,
|
||||
|
||||
Reference in New Issue
Block a user