use crate::types::{Cell, GameState, MoveError, Player};
pub struct GameEngine {
board: [Cell; 9],
pub current_player: Player,
pub ai_enabled: bool,
}
impl GameEngine {
pub fn new() -> Self {
Self {
board: [Cell::Empty; 9],
current_player: Player::X,
ai_enabled: true,
}
}
pub fn with_ai(ai_enabled: bool) -> Self {
Self {
board: [Cell::Empty; 9],
current_player: Player::X,
ai_enabled,
}
}
pub fn get_board(&self) -> &[Cell; 9] {
&self.board
}
pub fn make_move(&mut self, index: usize) -> Result<(), MoveError> {
if index >= 9 {
return Err(MoveError::OutOfBounds);
}
if self.board[index] != Cell::Empty {
return Err(MoveError::CellOccupied);
}
match self.current_player {
Player::X => self.board[index] = Cell::X,
Player::O => self.board[index] = Cell::O,
}
self.current_player = self.current_player.opponent();
Ok(())
}
pub fn check_state(&self) -> GameState {
Self::check_board_state(&self, self.board)
}
pub fn is_over(&self) -> bool {
!matches!(self.check_state(), GameState::InProgress)
}
pub fn get_best_move(&self) -> Option<usize> {
if !self.ai_enabled || self.is_over() {
return None;
}
let mut best_score = -i32::MAX;
let mut best_move: Option<usize> = None;
let maximizing_player = self.current_player;
for i in 0..9 {
if self.board[i] == Cell::Empty {
let mut temp_board = self.board;
match maximizing_player {
Player::X => temp_board[i] = Cell::X,
Player::O => temp_board[i] = Cell::O,
}
let score = self.minimax_with_pruning(
temp_board,
maximizing_player.opponent(),
-i32::MAX,
i32::MAX,
);
if score > best_score {
best_score = score;
best_move = Some(i);
}
}
}
best_move
}
fn minimax_with_pruning(
&self,
board: [Cell; 9],
player: Player,
mut alpha: i32,
mut beta: i32,
) -> i32 {
let state = self.check_board_state(board);
match state {
GameState::Win(winner) => {
return if winner == self.current_player {
10
} else {
-10
};
}
GameState::Tie => return 0,
GameState::InProgress => {}
}
let available_moves: Vec<usize> = board
.iter()
.enumerate()
.filter_map(
|(i, &cell)| {
if cell == Cell::Empty { Some(i) } else { None }
},
)
.collect();
if available_moves.is_empty() {
return 0;
}
let current_player_is_maximizing = player == self.current_player;
if current_player_is_maximizing {
let mut max_eval = -i32::MAX;
for &move_index in &available_moves {
let mut temp_board = board;
match player {
Player::X => temp_board[move_index] = Cell::X,
Player::O => temp_board[move_index] = Cell::O,
}
let eval = self.minimax_with_pruning(temp_board, player.opponent(), alpha, beta);
max_eval = max_eval.max(eval);
alpha = alpha.max(eval);
if beta <= alpha {
break;
}
}
max_eval
} else {
let mut min_eval = i32::MAX;
for &move_index in &available_moves {
let mut temp_board = board;
match player {
Player::X => temp_board[move_index] = Cell::X,
Player::O => temp_board[move_index] = Cell::O,
}
let eval = self.minimax_with_pruning(temp_board, player.opponent(), alpha, beta);
min_eval = min_eval.min(eval);
beta = beta.min(eval);
if beta <= alpha {
break;
}
}
min_eval
}
}
fn check_board_state(&self, board: [Cell; 9]) -> GameState {
let winning_combinations = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for combination in &winning_combinations {
let cell_1 = board[combination[0]];
let cell_2 = board[combination[1]];
let cell_3 = board[combination[2]];
if cell_1 != Cell::Empty && cell_1 == cell_2 && cell_2 == cell_3 {
return match cell_1 {
Cell::X => GameState::Win(Player::X),
Cell::O => GameState::Win(Player::O),
_ => unreachable!(),
};
}
}
if !board.iter().any(|&cell| cell == Cell::Empty) {
return GameState::Tie;
}
GameState::InProgress
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn x_can_win() {
let mut game = GameEngine::new();
game.make_move(0).unwrap(); game.make_move(3).unwrap(); game.make_move(1).unwrap(); game.make_move(4).unwrap(); game.make_move(2).unwrap(); assert_eq!(game.check_state(), GameState::Win(Player::X));
}
#[test]
fn tie_game() {
let mut game = GameEngine::new();
let moves = [0, 1, 2, 4, 3, 5, 7, 6, 8];
for &i in &moves {
game.make_move(i).unwrap();
}
assert_eq!(game.check_state(), GameState::Tie);
}
#[test]
fn invalid_move_out_of_bounds() {
let mut game = GameEngine::new();
assert_eq!(game.make_move(9), Err(MoveError::OutOfBounds));
}
#[test]
fn invalid_move_occupied() {
let mut game = GameEngine::new();
game.make_move(0).unwrap();
assert_eq!(game.make_move(0), Err(MoveError::CellOccupied));
}
#[test]
fn minimax_ai_blocks_win() {
let mut game = GameEngine::new();
game.make_move(0).unwrap(); game.make_move(4).unwrap(); game.make_move(1).unwrap(); assert_eq!(game.get_best_move(), Some(2));
}
#[test]
fn human_vs_human_mode() {
let game = GameEngine::with_ai(false);
assert_eq!(game.get_best_move(), None); }
}