feat: impl the minimax algorithm (bot can't be beaten)
This commit is contained in:
parent
fbc1aafce6
commit
55849c407e
130
src/main.rs
130
src/main.rs
@ -86,14 +86,19 @@ impl Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_item(&mut self, item: Item, idx: usize) -> Result<(), &str> {
|
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");
|
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::X => Err("already set, choose another one!"),
|
||||||
Item::O => Err("already set, choose another one!"),
|
Item::O => Err("already set, choose another one!"),
|
||||||
Item::Empty => {
|
Item::Empty => {
|
||||||
self.items[idx - 1] = item;
|
self.items[idx] = item;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,6 +239,90 @@ impl From<String> 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<usize> {
|
||||||
|
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)]
|
#[allow(dead_code)]
|
||||||
struct TicTacToe {
|
struct TicTacToe {
|
||||||
board: Board,
|
board: Board,
|
||||||
@ -296,7 +385,7 @@ impl TicTacToe {
|
|||||||
Ok(self.players[0])
|
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) {
|
fn select_first_player(&mut self) {
|
||||||
if self.players.len() != 2 {
|
if self.players.len() != 2 {
|
||||||
fatal("unable to initialize the game, 2 players must be set".to_string());
|
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);
|
self.players = Player::from_game_type(self.game_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_player_item(&self) -> Result<usize, String> {
|
fn select_player_item(&mut self) -> Result<usize, String> {
|
||||||
if self.players.len() != 2 {
|
if self.players.len() != 2 {
|
||||||
return Err(
|
return Err(
|
||||||
"unable to get player item input, 2 players must be initialized".to_string(),
|
"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
|
// TODO: player can select the bot level
|
||||||
let mut rng = rand::thread_rng();
|
// bot uses minimax algorithm
|
||||||
let mut available_indexes = self.board.get_available_indexes();
|
let minimax = Minimax::new(player, self.players[1]);
|
||||||
available_indexes.shuffle(&mut rng);
|
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) {
|
fn run(&mut self) {
|
||||||
@ -416,14 +514,12 @@ impl TicTacToe {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
match self.select_player_item() {
|
match self.select_player_item() {
|
||||||
Ok(pos) => {
|
Ok(pos) => match self.board.set_item(player.item, pos - 1) {
|
||||||
match self.board.set_item(player.item, pos) {
|
Ok(_) => break,
|
||||||
Ok(_) => break,
|
Err(e) => {
|
||||||
Err(e) => {
|
error(e.to_string());
|
||||||
error(e.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error(e.to_string());
|
error(e.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user