Skip to content

Commit 0e240ae

Browse files
feat: implement Minimax AI algorithm and game logic
1 parent e0a3988 commit 0e240ae

3 files changed

Lines changed: 407 additions & 0 deletions

File tree

topics/tic-tac-toe/src/ai.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use crate::board::Board;
2+
use crate::game::Game;
3+
use crate::types::Player;
4+
5+
/// AI player using the Minimax algorithm
6+
pub struct AI {
7+
player: Player,
8+
}
9+
10+
impl AI {
11+
/// Creates a new AI instance
12+
pub fn new() -> Self {
13+
AI { player: Player::AI }
14+
}
15+
16+
/// Finds the best move for the AI using the Minimax algorithm
17+
/// Returns the position (0-8) of the best move
18+
pub fn find_best_move(&self, game: &Game) -> Option<usize> {
19+
let available_moves = game.available_moves();
20+
21+
if available_moves.is_empty() {
22+
return None;
23+
}
24+
25+
let mut best_score = i32::MIN;
26+
let mut best_move = available_moves[0];
27+
28+
// Try each available move and evaluate it
29+
for &position in &available_moves {
30+
let mut game_clone = self.simulate_move(game, position, self.player);
31+
let score = self.minimax(&mut game_clone, 0, false);
32+
33+
if score > best_score {
34+
best_score = score;
35+
best_move = position;
36+
}
37+
}
38+
39+
Some(best_move)
40+
}
41+
42+
/// Minimax algorithm with depth tracking
43+
///
44+
/// # Arguments
45+
/// * `game` - The current game state
46+
/// * `depth` - Current depth in the game tree
47+
/// * `is_maximizing` - True if maximizing player (AI), false if minimizing (Human)
48+
///
49+
/// # Returns
50+
/// The score of the board state
51+
fn minimax(&self, game: &mut Game, depth: i32, is_maximizing: bool) -> i32 {
52+
// Terminal state: check if game is over
53+
let score = game.evaluate();
54+
55+
// If AI won, return score minus depth (prefer faster wins)
56+
if score == 10 {
57+
return score - depth;
58+
}
59+
60+
// If Human won, return score plus depth (prefer slower losses)
61+
if score == -10 {
62+
return score + depth;
63+
}
64+
65+
// Check for draw
66+
let available_moves = game.available_moves();
67+
if available_moves.is_empty() {
68+
return 0;
69+
}
70+
71+
if is_maximizing {
72+
// Maximizing player (AI)
73+
let mut best_score = i32::MIN;
74+
75+
for &position in &available_moves {
76+
let mut game_clone = self.simulate_move(game, position, Player::AI);
77+
let score = self.minimax(&mut game_clone, depth + 1, false);
78+
best_score = best_score.max(score);
79+
}
80+
81+
best_score
82+
} else {
83+
// Minimizing player (Human)
84+
let mut best_score = i32::MAX;
85+
86+
for &position in &available_moves {
87+
let mut game_clone = self.simulate_move(game, position, Player::Human);
88+
let score = self.minimax(&mut game_clone, depth + 1, true);
89+
best_score = best_score.min(score);
90+
}
91+
92+
best_score
93+
}
94+
}
95+
96+
/// Simulates a move and returns a new game state
97+
fn simulate_move(&self, game: &Game, position: usize, player: Player) -> Game {
98+
// Create a copy of the current game using the board state
99+
let mut new_board = Board::new();
100+
101+
// Copy the current board state
102+
for i in 0..9 {
103+
if let Some(crate::types::Cell::Occupied(p)) = game.board().get(i) {
104+
new_board.make_move(i, p);
105+
}
106+
}
107+
108+
// Make the new move on the copied board
109+
new_board.make_move(position, player);
110+
111+
// Create a new game with this board state
112+
// We need to use Game::from_board or similar
113+
// For now, let's create a helper in Game
114+
self.create_game_from_board(new_board, player.opponent())
115+
}
116+
117+
/// Creates a game state from a board
118+
fn create_game_from_board(&self, board: Board, next_player: Player) -> Game {
119+
Game::from_board(board, next_player)
120+
}
121+
}
122+
123+
impl Default for AI {
124+
fn default() -> Self {
125+
Self::new()
126+
}
127+
}
128+
129+
#[cfg(test)]
130+
mod tests {
131+
use super::*;
132+
133+
#[test]
134+
fn test_ai_blocks_winning_move() {
135+
let mut game = Game::new();
136+
let ai = AI::new();
137+
138+
// Human has two in a row
139+
game.make_move(0); // Human X at position 0
140+
game.make_move(3); // AI O at position 3
141+
game.make_move(1); // Human X at position 1
142+
143+
// AI should block position 2 to prevent human win
144+
let best_move = ai.find_best_move(&game);
145+
assert_eq!(best_move, Some(2));
146+
}
147+
148+
#[test]
149+
fn test_ai_takes_winning_move() {
150+
let mut game = Game::new();
151+
let ai = AI::new();
152+
153+
// Setup: AI has two in a row
154+
game.make_move(0); // Human X
155+
game.make_move(3); // AI O
156+
game.make_move(1); // Human X
157+
game.make_move(4); // AI O
158+
game.make_move(8); // Human X
159+
160+
// AI should take position 5 to win
161+
let best_move = ai.find_best_move(&game);
162+
assert_eq!(best_move, Some(5));
163+
}
164+
165+
#[test]
166+
fn test_ai_finds_move_on_empty_board() {
167+
let game = Game::new();
168+
let ai = AI::new();
169+
170+
// AI should find a valid move
171+
let best_move = ai.find_best_move(&game);
172+
assert!(best_move.is_some());
173+
assert!(best_move.unwrap() < 9);
174+
}
175+
}

0 commit comments

Comments
 (0)