diff --git a/src/main.rs b/src/main.rs index f09051b..b50e1c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,14 +86,19 @@ impl Board { } fn set_item(&mut self, item: Item, idx: usize) -> Result<(), &str> { - if idx == 0 || idx > 9 { + if idx > 8 { return Err("invalid input, must be between 1 <= x <= 9"); } - match self.items[idx - 1] { + // player must be forbidden to do this action (only used by minimax: undo action) + if item == Item::Empty { + self.items[idx] = item; + return Ok(()); + } + match self.items[idx] { Item::X => Err("already set, choose another one!"), Item::O => Err("already set, choose another one!"), Item::Empty => { - self.items[idx - 1] = item; + self.items[idx] = item; return Ok(()); } } @@ -234,6 +239,90 @@ impl From for GameType { } } +/// Holds all the mandatory informations to compute the **minimax** algorithm +/// avoiding to collect on each turn both player +struct Minimax { + bot: Player, + human: Player, +} + +impl Minimax { + fn new(bot: Player, human: Player) -> Self { + Minimax { bot, human } + } + + /// checks if there's a winner and set an associated score for each winner + /// +1 for the bot: **maximizer** + /// -1 for the human: **minimizer** + fn evaluate(&self, board: &Board) -> isize { + if board.check_win(self.bot) { + return 1; + } + if board.check_win(self.human) { + return -1; + } + 0 + } + + /// computes the **minimax** algorithm + /// recursive function, checks all board states (tree) for each turn (human + bot) and returns the best score + fn compute(&self, board: &mut Board, player: Player, depth: usize) -> isize { + let score = self.evaluate(board); + if score != 0 { + return score; + } + + // no more moves availables it's a tie + let available_indexes = board.get_available_indexes(); + if available_indexes.len() == 0 { + return 0; + } + + // maximizer move's + if player.is_bot { + // negative high value, enough to simulate an -INF + let mut best_score = -1000; + for index in available_indexes { + board.set_item(player.item, index).unwrap(); + best_score = std::cmp::max(best_score, self.compute(board, self.human, depth + 1)); + board.set_item(Item::Empty, index).unwrap(); + } + + return best_score; + } + + // mimimizer move's + let mut best_score = 1000; + for index in available_indexes { + board.set_item(player.item, index).unwrap(); + best_score = std::cmp::min(best_score, self.compute(board, self.bot, depth + 1)); + board.set_item(Item::Empty, index).unwrap(); + } + + best_score + } + + /// returns the bot best move's (board index) in the current board state + fn get_best_move(&self, board: &mut Board) -> Option { + let mut best_score = -1000; + let mut best_move = None; + + let available_indexes = board.get_available_indexes(); + for index in available_indexes { + board.set_item(self.bot.item, index).unwrap(); + let score = self.compute(board, self.human, 0); + board.set_item(Item::Empty, index).unwrap(); + + if score > best_score { + best_score = score; + best_move = Some(index); + } + } + + best_move + } +} + #[allow(dead_code)] struct TicTacToe { board: Board, @@ -296,7 +385,7 @@ impl TicTacToe { Ok(self.players[0]) } - /// if `GameType` is againt bot, you can select who starts + /// if `GameType` is against bot, you can select who starts fn select_first_player(&mut self) { if self.players.len() != 2 { fatal("unable to initialize the game, 2 players must be set".to_string()); @@ -364,7 +453,7 @@ impl TicTacToe { self.players = Player::from_game_type(self.game_type); } - fn select_player_item(&self) -> Result { + fn select_player_item(&mut self) -> Result { if self.players.len() != 2 { return Err( "unable to get player item input, 2 players must be initialized".to_string(), @@ -383,12 +472,21 @@ impl TicTacToe { } } - // for now, use a dump bot - let mut rng = rand::thread_rng(); - let mut available_indexes = self.board.get_available_indexes(); - available_indexes.shuffle(&mut rng); + // TODO: player can select the bot level + // bot uses minimax algorithm + let minimax = Minimax::new(player, self.players[1]); + match minimax.get_best_move(&mut self.board) { + Some(v) => { + return Ok(v + 1); + } + None => { + let mut rng = rand::thread_rng(); + let mut available_indexes = self.board.get_available_indexes(); + available_indexes.shuffle(&mut rng); - Ok(available_indexes[0] + 1) + return Ok(available_indexes[0] + 1); + } + } } fn run(&mut self) { @@ -416,14 +514,12 @@ impl TicTacToe { loop { match self.select_player_item() { - Ok(pos) => { - match self.board.set_item(player.item, pos) { - Ok(_) => break, - Err(e) => { - error(e.to_string()); - } + Ok(pos) => match self.board.set_item(player.item, pos - 1) { + Ok(_) => break, + Err(e) => { + error(e.to_string()); } - } + }, Err(e) => { error(e.to_string()); }