1+ use crate :: board:: { Board , Player , Cell } ;
2+ use crate :: game:: Game ;
3+
4+ /// AI player that uses the Minimax algorithm to play optimally
5+ pub struct AI {
6+ player : Player ,
7+ }
8+
9+ impl AI {
10+ /// Creates a new AI player
11+ pub fn new ( player : Player ) -> Self {
12+ Self { player }
13+ }
14+
15+ /// Gets the best move for the AI using the Minimax algorithm
16+ pub fn get_best_move ( & self , game : & Game ) -> Option < usize > {
17+ let available_moves = game. get_available_moves ( ) ;
18+ if available_moves. is_empty ( ) {
19+ return None ;
20+ }
21+
22+ let mut best_score = i32:: MIN ;
23+ let mut best_move = None ;
24+
25+ for & position in & available_moves {
26+ let mut game_copy = self . clone_game ( game) ;
27+
28+ game_copy. make_move ( position) ;
29+
30+ let score = self . minimax ( & game_copy, 0 , false ) ;
31+
32+ if score > best_score {
33+ best_score = score;
34+ best_move = Some ( position) ;
35+ }
36+ }
37+
38+ best_move
39+ }
40+
41+ /// Minimax algorithm implementation with depth-first search
42+ /// Returns the score for the current game state from the AI's perspective
43+ /// is_maximizing: true when it's AI's turn (maximize), false when opponent's turn (minimize)
44+ fn minimax ( & self , game : & Game , depth : u32 , is_maximizing : bool ) -> i32 {
45+ // Check terminal states (game over)
46+ if let Some ( winner) = game. check_winner ( ) {
47+ return if winner == self . player {
48+ 10 - depth as i32 // AI wins: prefer winning sooner (higher score for fewer moves)
49+ } else {
50+ depth as i32 - 10 // AI loses: prefer losing later (less negative score)
51+ } ;
52+ }
53+
54+ if game. is_game_over ( ) {
55+ return 0 ;
56+ }
57+
58+ let available_moves = game. get_available_moves ( ) ;
59+
60+ if is_maximizing {
61+ // AI's turn - maximize the score
62+ let mut max_score = i32:: MIN ;
63+
64+ for & position in & available_moves {
65+ let mut game_copy = self . clone_game ( game) ;
66+ game_copy. make_move ( position) ;
67+
68+ let score = self . minimax ( & game_copy, depth + 1 , false ) ;
69+ max_score = max_score. max ( score) ;
70+ }
71+
72+ max_score
73+ } else {
74+ // Opponent's turn - minimize the score (from AI's perspective)
75+ let mut min_score = i32:: MAX ;
76+
77+ for & position in & available_moves {
78+ let mut game_copy = self . clone_game ( game) ;
79+ game_copy. make_move ( position) ;
80+
81+ let score = self . minimax ( & game_copy, depth + 1 , true ) ;
82+ min_score = min_score. min ( score) ;
83+ }
84+
85+ min_score
86+ }
87+ }
88+
89+ /// Creates a copy of the game state for simulation
90+ /// This is necessary since Game doesn't implement Clone
91+ fn clone_game ( & self , game : & Game ) -> Game {
92+ let mut new_game = Game :: new ( ) ;
93+
94+ let board = game. get_board ( ) ;
95+
96+ let mut moves = Vec :: new ( ) ;
97+ for position in 1 ..=9 {
98+ if let Some ( Cell :: Occupied ( player) ) = board. get_cell ( position) {
99+ moves. push ( ( position, player) ) ;
100+ }
101+ }
102+
103+
104+ let mut x_moves = Vec :: new ( ) ;
105+ let mut o_moves = Vec :: new ( ) ;
106+
107+ for ( pos, player) in moves {
108+ match player {
109+ Player :: X => x_moves. push ( pos) ,
110+ Player :: O => o_moves. push ( pos) ,
111+ }
112+ }
113+
114+ let mut move_sequence = Vec :: new ( ) ;
115+ let max_len = x_moves. len ( ) . max ( o_moves. len ( ) ) ;
116+
117+ for i in 0 ..max_len {
118+ if i < x_moves. len ( ) {
119+ move_sequence. push ( x_moves[ i] ) ;
120+ }
121+ if i < o_moves. len ( ) {
122+ move_sequence. push ( o_moves[ i] ) ;
123+ }
124+ }
125+
126+ for & position in & move_sequence {
127+ new_game. make_move ( position) ;
128+ }
129+
130+ new_game
131+ }
132+ }
133+
134+ #[ cfg( test) ]
135+ mod tests {
136+ use super :: * ;
137+ use crate :: game:: Game ;
138+
139+ #[ test]
140+ fn test_ai_creation ( ) {
141+ let ai = AI :: new ( Player :: O ) ;
142+ assert_eq ! ( ai. player, Player :: O ) ;
143+
144+ let ai_x = AI :: new ( Player :: X ) ;
145+ assert_eq ! ( ai_x. player, Player :: X ) ;
146+ }
147+
148+ #[ test]
149+ fn test_ai_blocks_winning_move ( ) {
150+ let mut game = Game :: new ( ) ;
151+ let ai = AI :: new ( Player :: O ) ;
152+
153+ // X threatens to win in top row
154+ game. make_move ( 1 ) ; // X
155+ game. make_move ( 4 ) ; // O (AI's previous move)
156+ game. make_move ( 2 ) ; // X
157+ // Now X threatens to win at position 3
158+
159+ let ai_move = ai. get_best_move ( & game) ;
160+ assert_eq ! ( ai_move, Some ( 3 ) ) ; // AI should block at position 3
161+ }
162+
163+ #[ test]
164+ fn test_ai_takes_winning_move ( ) {
165+ let mut game = Game :: new ( ) ;
166+ let ai = AI :: new ( Player :: O ) ;
167+
168+ // Set up a scenario where AI can win
169+ game. make_move ( 1 ) ; // X
170+ game. make_move ( 4 ) ; // O
171+ game. make_move ( 2 ) ; // X
172+ game. make_move ( 5 ) ; // O
173+ game. make_move ( 7 ) ; // X
174+ // Now O can win at position 6
175+
176+ let ai_move = ai. get_best_move ( & game) ;
177+ assert_eq ! ( ai_move, Some ( 6 ) ) ; // AI should win at position 6
178+ }
179+
180+ #[ test]
181+ fn test_ai_chooses_valid_move_on_empty_board ( ) {
182+ let game = Game :: new ( ) ;
183+ let ai = AI :: new ( Player :: X ) ;
184+
185+ let ai_move = ai. get_best_move ( & game) ;
186+ // On an empty board, any move should be valid (1-9)
187+ // The AI should choose some valid position
188+ assert ! ( ai_move. is_some( ) ) ;
189+ let position = ai_move. unwrap ( ) ;
190+ assert ! ( position >= 1 && position <= 9 ) ;
191+ }
192+
193+ #[ test]
194+ fn test_ai_no_moves_available ( ) {
195+ let mut game = Game :: new ( ) ;
196+ let ai = AI :: new ( Player :: O ) ;
197+
198+ // Fill the board completely
199+ for position in 1 ..=9 {
200+ let player = if position % 2 == 1 { Player :: X } else { Player :: O } ;
201+ game. make_move ( position) ;
202+ }
203+
204+ let ai_move = ai. get_best_move ( & game) ;
205+ assert_eq ! ( ai_move, None ) ; // No moves available
206+ }
207+
208+ #[ test]
209+ fn test_minimax_prefers_winning_sooner ( ) {
210+ let mut game = Game :: new ( ) ;
211+ let ai = AI :: new ( Player :: O ) ;
212+
213+ // Create a scenario where AI has multiple ways to win
214+ // AI should prefer the immediate win over a longer path to victory
215+ game. make_move ( 1 ) ; // X
216+ game. make_move ( 4 ) ; // O
217+ game. make_move ( 2 ) ; // X
218+ game. make_move ( 5 ) ; // O
219+ game. make_move ( 8 ) ; // X
220+ // AI can win immediately at position 6
221+
222+ let ai_move = ai. get_best_move ( & game) ;
223+ assert_eq ! ( ai_move, Some ( 6 ) ) ; // Should take the immediate win
224+ }
225+ }
0 commit comments