From af2bc841119a6751c240dec95dd5511d4ee31d36 Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Wed, 7 May 2025 23:08:15 -0700 Subject: init ; most of tic tac toe done --- tic_tac_toe/src/ai.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tic_tac_toe/src/ai.rs (limited to 'tic_tac_toe/src/ai.rs') diff --git a/tic_tac_toe/src/ai.rs b/tic_tac_toe/src/ai.rs new file mode 100644 index 0000000..e2d8918 --- /dev/null +++ b/tic_tac_toe/src/ai.rs @@ -0,0 +1,111 @@ +use crate::board::{Board, Coord, Tile}; +use crate::{GameState, check_state}; + +#[derive(Debug)] +pub struct BestMove { + pub coord: Coord, + eval: i8, + depth: u32, +} + +fn eval_for_player(board: &Board, player: Tile) -> i8 { + match player { + Tile::PlayerOne => { + return match check_state(board) { + GameState::PlayerTwoWin => -100, + GameState::Draw => -10, + GameState::InProgress => 0, + GameState::PlayerOneWin => 100, + }; + } + Tile::PlayerTwo => { + return match check_state(board) { + GameState::PlayerOneWin => -100, + GameState::Draw => -10, + GameState::InProgress => 0, + GameState::PlayerTwoWin => 100, + }; + } + Tile::Unowned => { + return match check_state(board) { + GameState::PlayerOneWin => -100, + GameState::PlayerTwoWin => -100, + GameState::InProgress => 0, + GameState::Draw => 100, + }; + } + } +} + +pub fn get_best_move(board: &Board, player: Tile, is_tl: bool, depth: u32) -> BestMove { + //base case + //game is over, return eval + let eval = eval_for_player(board, player); + + //eval 0 means the game is in progress + if eval != 0 { + return BestMove { + coord: (99, 99), + eval, + depth, + }; + } + + //get all possible moves + let possible_moves: Vec = (0..9) + .map(|i| { + return (i % 3, i / 3); + }) + .filter(|coord| { + return board.get_at_coord(*coord) == Tile::Unowned; + }) + .collect(); + + let mut cur_best = BestMove { + eval: i8::min_value(), + coord: possible_moves[0], + depth, + }; + + for p_move in possible_moves { + let mut new_board = board.clone(); + new_board.set_at_coord(p_move, player); + + let other_player = match player { + Tile::PlayerOne => Tile::PlayerTwo, + Tile::PlayerTwo => Tile::PlayerOne, + //this allows the AI to set as player 'unowned', which will cause it to try to draw + //todo: fix this + Tile::Unowned => Tile::PlayerOne, + }; + + let response_move = get_best_move(&new_board, other_player, false, depth + 1); + if response_move.coord.0 != 99 { + new_board.set_at_coord(response_move.coord, other_player); + } + + //this is the inverse of the response eval + let response_eval = -response_move.eval; + + if is_tl { + println!("AI > If I make move {p_move:?}, the best response is {response_move:?}"); + } + + //slightly bias for the centre to beat weak players + let centre_bias = if p_move == (1, 1) { 1 } else { 0 }; + + if response_eval > cur_best.eval { + cur_best = BestMove { + coord: p_move, + eval: response_eval + centre_bias, + depth: response_move.depth, + }; + } + } + + if is_tl { + println!("AI > I'll play {cur_best:?}") + } + + return cur_best; +} -- cgit v1.2.3-70-g09d2