rusty2048_core/
ai.rs

1use crate::board::Tile;
2use crate::{Board, Direction, Game, GameConfig, GameResult};
3
4/// AI algorithm types
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum AIAlgorithm {
7    /// Simple greedy algorithm
8    Greedy,
9    /// Expectimax algorithm with limited depth
10    Expectimax,
11    /// Monte Carlo Tree Search
12    MCTS,
13}
14
15/// AI player for 2048 game
16pub struct AIPlayer {
17    algorithm: AIAlgorithm,
18    max_depth: usize,
19    simulation_count: usize,
20}
21
22impl AIPlayer {
23    /// Create a new AI player
24    pub fn new(algorithm: AIAlgorithm) -> Self {
25        let max_depth = match algorithm {
26            AIAlgorithm::Greedy => 1,
27            AIAlgorithm::Expectimax => 4,
28            AIAlgorithm::MCTS => 1000,
29        };
30
31        let simulation_count = match algorithm {
32            AIAlgorithm::Greedy => 1,
33            AIAlgorithm::Expectimax => 1,
34            AIAlgorithm::MCTS => 100,
35        };
36
37        Self {
38            algorithm,
39            max_depth,
40            simulation_count,
41        }
42    }
43
44    /// Set the maximum search depth
45    pub fn with_max_depth(mut self, depth: usize) -> Self {
46        self.max_depth = depth;
47        self
48    }
49
50    /// Set the number of simulations for MCTS
51    pub fn with_simulation_count(mut self, count: usize) -> Self {
52        self.simulation_count = count;
53        self
54    }
55
56    /// Get the best move for the current game state
57    pub fn get_best_move(&self, game: &Game) -> GameResult<Direction> {
58        match self.algorithm {
59            AIAlgorithm::Greedy => self.greedy_move(game),
60            AIAlgorithm::Expectimax => self.expectimax_move(game),
61            AIAlgorithm::MCTS => self.mcts_move(game),
62        }
63    }
64
65    /// Simple greedy algorithm - choose the move that gives the highest immediate score
66    fn greedy_move(&self, game: &Game) -> GameResult<Direction> {
67        let mut best_score = 0;
68        let mut best_direction = Direction::Up;
69
70        for &direction in &[
71            Direction::Up,
72            Direction::Down,
73            Direction::Left,
74            Direction::Right,
75        ] {
76            let mut game_copy = game.clone();
77            if let Ok(moved) = game_copy.make_move(direction) {
78                if moved {
79                    let score = game_copy.score().current();
80                    if score > best_score {
81                        best_score = score;
82                        best_direction = direction;
83                    }
84                }
85            }
86        }
87
88        Ok(best_direction)
89    }
90
91    /// Expectimax algorithm - considers both player moves and random tile placements
92    fn expectimax_move(&self, game: &Game) -> GameResult<Direction> {
93        let mut best_score = f64::NEG_INFINITY;
94        let mut best_direction = Direction::Up;
95
96        for &direction in &[
97            Direction::Up,
98            Direction::Down,
99            Direction::Left,
100            Direction::Right,
101        ] {
102            let mut game_copy = game.clone();
103            if let Ok(moved) = game_copy.make_move(direction) {
104                if moved {
105                    let score = self.expectimax_search(&game_copy, self.max_depth - 1, false);
106                    if score > best_score {
107                        best_score = score;
108                        best_direction = direction;
109                    }
110                }
111            }
112        }
113
114        Ok(best_direction)
115    }
116
117    /// Expectimax search implementation
118    fn expectimax_search(&self, game: &Game, depth: usize, is_maximizing: bool) -> f64 {
119        if depth == 0 || game.state() != crate::GameState::Playing {
120            return self.evaluate_board(game.board());
121        }
122
123        if is_maximizing {
124            // Player's turn - maximize score
125            let mut max_score = f64::NEG_INFINITY;
126            for &direction in &[
127                Direction::Up,
128                Direction::Down,
129                Direction::Left,
130                Direction::Right,
131            ] {
132                let mut game_copy = game.clone();
133                if let Ok(moved) = game_copy.make_move(direction) {
134                    if moved {
135                        let score = self.expectimax_search(&game_copy, depth - 1, false);
136                        max_score = max_score.max(score);
137                    }
138                }
139            }
140            max_score
141        } else {
142            // Random tile placement - expect average score
143            let empty_positions = game.board().empty_positions();
144            if empty_positions.is_empty() {
145                return self.evaluate_board(game.board());
146            }
147
148            let mut total_score = 0.0;
149            let mut count = 0;
150
151            // Sample a few random tile placements
152            for _ in 0..self.simulation_count.min(empty_positions.len()) {
153                let mut game_copy = game.clone();
154                if let Ok(()) = self.add_random_tile_simulation(&mut game_copy) {
155                    let score = self.expectimax_search(&game_copy, depth - 1, true);
156                    total_score += score;
157                    count += 1;
158                }
159            }
160
161            if count > 0 {
162                total_score / count as f64
163            } else {
164                self.evaluate_board(game.board())
165            }
166        }
167    }
168
169    /// Monte Carlo Tree Search algorithm
170    fn mcts_move(&self, game: &Game) -> GameResult<Direction> {
171        let mut root = MCTSNode::new(game.clone());
172
173        for _ in 0..self.simulation_count {
174            let mut current = &mut root;
175            let mut game_state = game.clone();
176
177            // Selection
178            while !current.children.is_empty() && current.visits > 0 {
179                current = current.select_child();
180                if let Some(direction) = current.last_move {
181                    let _ = game_state.make_move(direction);
182                }
183            }
184
185            // Expansion
186            if current.visits > 0 && game_state.state() == crate::GameState::Playing {
187                current.expand(&game_state);
188            }
189
190            // Simulation
191            let mut simulation_game = game_state.clone();
192            let simulation_result = self.simulate_random_game(&mut simulation_game);
193
194            // Backpropagation
195            current.backpropagate(simulation_result);
196        }
197
198        // Choose the best move
199        let best_child = root
200            .children
201            .iter()
202            .max_by(|a, b| a.visits.cmp(&b.visits))
203            .ok_or_else(|| crate::GameError::InvalidOperation("No valid moves".to_string()))?;
204
205        Ok(best_child.last_move.unwrap_or(Direction::Up))
206    }
207
208    /// Simulate a random game to completion
209    fn simulate_random_game(&self, game: &mut Game) -> f64 {
210        let mut moves = 0;
211        let max_moves = 1000; // Prevent infinite loops
212
213        while game.state() == crate::GameState::Playing && moves < max_moves {
214            let directions = [
215                Direction::Up,
216                Direction::Down,
217                Direction::Left,
218                Direction::Right,
219            ];
220            let mut moved = false;
221
222            for &direction in &directions {
223                if let Ok(did_move) = game.make_move(direction) {
224                    if did_move {
225                        moved = true;
226                        break;
227                    }
228                }
229            }
230
231            if !moved {
232                break; // No valid moves
233            }
234
235            moves += 1;
236        }
237
238        self.evaluate_board(game.board())
239    }
240
241    /// Add a random tile for simulation purposes
242    fn add_random_tile_simulation(&self, game: &mut Game) -> GameResult<()> {
243        let empty_positions = game.board().empty_positions();
244        if empty_positions.is_empty() {
245            return Ok(());
246        }
247
248        // Use a simple random selection for simulation
249        let random_index = (empty_positions.len() as f64 * 0.5) as usize; // Simplified
250        let (row, col) = empty_positions[random_index];
251        let value = if rand::random::<u64>() % 10 < 9 { 2 } else { 4 };
252
253        game.board_mut().set_tile(row, col, Tile::new(value))?;
254        Ok(())
255    }
256
257    /// Evaluate the current board state
258    fn evaluate_board(&self, board: &Board) -> f64 {
259        let mut score = 0.0;
260        let size = board.size();
261
262        // Weight matrix for position importance (corner and edge tiles are more valuable)
263        let weights = [
264            vec![4.0, 2.0, 2.0, 4.0],
265            vec![2.0, 1.0, 1.0, 2.0],
266            vec![2.0, 1.0, 1.0, 2.0],
267            vec![4.0, 2.0, 2.0, 4.0],
268        ];
269
270        // Evaluate each tile
271        for row in 0..size {
272            for col in 0..size {
273                if let Ok(tile) = board.get_tile(row, col) {
274                    if !tile.is_empty() {
275                        let weight = if row < weights.len() && col < weights[row].len() {
276                            weights[row][col]
277                        } else {
278                            1.0
279                        };
280                        score += (tile.value as f64) * weight;
281                    }
282                }
283            }
284        }
285
286        // Bonus for keeping high values in corners
287        score += self.corner_bonus(board);
288
289        // Penalty for having many small tiles scattered
290        score -= self.scattered_penalty(board);
291
292        // Bonus for smoothness (adjacent tiles with similar values)
293        score += self.smoothness_bonus(board);
294
295        score
296    }
297
298    /// Bonus for keeping high values in corners
299    fn corner_bonus(&self, board: &Board) -> f64 {
300        let size = board.size();
301        let mut bonus = 0.0;
302
303        let corners = [(0, 0), (0, size - 1), (size - 1, 0), (size - 1, size - 1)];
304
305        for (row, col) in corners {
306            if let Ok(tile) = board.get_tile(row, col) {
307                if !tile.is_empty() {
308                    bonus += tile.value as f64 * 2.0;
309                }
310            }
311        }
312
313        bonus
314    }
315
316    /// Penalty for having many small tiles scattered
317    fn scattered_penalty(&self, board: &Board) -> f64 {
318        let size = board.size();
319        let mut penalty = 0.0;
320        let mut small_tiles = 0;
321
322        for row in 0..size {
323            for col in 0..size {
324                if let Ok(tile) = board.get_tile(row, col) {
325                    if !tile.is_empty() && tile.value <= 8 {
326                        small_tiles += 1;
327                    }
328                }
329            }
330        }
331
332        penalty += small_tiles as f64 * 0.5;
333        penalty
334    }
335
336    /// Bonus for smoothness (adjacent tiles with similar values)
337    fn smoothness_bonus(&self, board: &Board) -> f64 {
338        let size = board.size();
339        let mut bonus = 0.0;
340
341        // Check horizontal smoothness
342        for row in 0..size {
343            for col in 0..size - 1 {
344                if let (Ok(tile1), Ok(tile2)) =
345                    (board.get_tile(row, col), board.get_tile(row, col + 1))
346                {
347                    if !tile1.is_empty() && !tile2.is_empty() {
348                        let diff = (tile1.value as f64 - tile2.value as f64).abs();
349                        bonus -= diff * 0.1;
350                    }
351                }
352            }
353        }
354
355        // Check vertical smoothness
356        for row in 0..size - 1 {
357            for col in 0..size {
358                if let (Ok(tile1), Ok(tile2)) =
359                    (board.get_tile(row, col), board.get_tile(row + 1, col))
360                {
361                    if !tile1.is_empty() && !tile2.is_empty() {
362                        let diff = (tile1.value as f64 - tile2.value as f64).abs();
363                        bonus -= diff * 0.1;
364                    }
365                }
366            }
367        }
368
369        bonus
370    }
371}
372
373/// MCTS Node for Monte Carlo Tree Search
374struct MCTSNode {
375    #[allow(dead_code)]
376    game: Game,
377    children: Vec<MCTSNode>,
378    visits: usize,
379    total_score: f64,
380    last_move: Option<Direction>,
381}
382
383impl MCTSNode {
384    fn new(game: Game) -> Self {
385        Self {
386            game,
387            children: Vec::new(),
388            visits: 0,
389            total_score: 0.0,
390            last_move: None,
391        }
392    }
393
394    fn select_child(&mut self) -> &mut MCTSNode {
395        // UCB1 formula for selection
396        let c = 1.414; // Exploration constant
397        let log_parent_visits = (self.visits as f64).ln();
398
399        if self.children.is_empty() {
400            panic!("Cannot select child from empty children list");
401        }
402
403        let mut best_index = 0;
404        let mut best_ucb = f64::NEG_INFINITY;
405
406        for (i, child) in self.children.iter().enumerate() {
407            let ucb = child.total_score / child.visits as f64
408                + c * (log_parent_visits / child.visits as f64).sqrt();
409            if ucb > best_ucb {
410                best_ucb = ucb;
411                best_index = i;
412            }
413        }
414
415        &mut self.children[best_index]
416    }
417
418    fn expand(&mut self, game: &Game) {
419        for &direction in &[
420            Direction::Up,
421            Direction::Down,
422            Direction::Left,
423            Direction::Right,
424        ] {
425            let mut game_copy = game.clone();
426            if let Ok(moved) = game_copy.make_move(direction) {
427                if moved {
428                    let mut child = MCTSNode::new(game_copy);
429                    child.last_move = Some(direction);
430                    self.children.push(child);
431                }
432            }
433        }
434    }
435
436    fn backpropagate(&mut self, score: f64) {
437        self.visits += 1;
438        self.total_score += score;
439    }
440}
441
442/// AI Game Controller - manages AI gameplay
443pub struct AIGameController {
444    ai_player: AIPlayer,
445    game: Game,
446    auto_play: bool,
447    move_delay_ms: u64,
448}
449
450impl AIGameController {
451    /// Create a new AI game controller
452    pub fn new(config: GameConfig, algorithm: AIAlgorithm) -> GameResult<Self> {
453        let ai_player = AIPlayer::new(algorithm);
454        let game = Game::new(config)?;
455
456        Ok(Self {
457            ai_player,
458            game,
459            auto_play: false,
460            move_delay_ms: 500,
461        })
462    }
463
464    /// Set auto-play mode
465    pub fn set_auto_play(&mut self, auto_play: bool) {
466        self.auto_play = auto_play;
467    }
468
469    /// Set move delay for auto-play
470    pub fn set_move_delay(&mut self, delay_ms: u64) {
471        self.move_delay_ms = delay_ms;
472    }
473
474    /// Get the current game
475    pub fn game(&self) -> &Game {
476        &self.game
477    }
478
479    /// Get a mutable reference to the game
480    pub fn game_mut(&mut self) -> &mut Game {
481        &mut self.game
482    }
483
484    /// Make an AI move
485    pub fn make_ai_move(&mut self) -> GameResult<bool> {
486        if self.game.state() != crate::GameState::Playing {
487            return Ok(false);
488        }
489
490        let best_move = self.ai_player.get_best_move(&self.game)?;
491        self.game.make_move(best_move)
492    }
493
494    /// Start a new AI game
495    pub fn new_game(&mut self) -> GameResult<()> {
496        self.game.new_game()
497    }
498
499    /// Get the AI algorithm being used
500    pub fn algorithm(&self) -> AIAlgorithm {
501        self.ai_player.algorithm
502    }
503}
504
505// Add rand dependency for simulation
506mod rand {
507    use std::collections::hash_map::DefaultHasher;
508    use std::hash::{Hash, Hasher};
509    use std::time::SystemTime;
510
511    pub fn random<T>() -> T
512    where
513        T: Copy + From<u64>,
514    {
515        let mut hasher = DefaultHasher::new();
516        SystemTime::now().hash(&mut hasher);
517        let hash = hasher.finish();
518        T::from(hash)
519    }
520}