summaryrefslogtreecommitdiff
path: root/tic_tac_toe/src/ai.rs
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2025-05-07 23:08:15 -0700
committerKai Stevenson <kai@kaistevenson.com>2025-05-07 23:08:15 -0700
commitaf2bc841119a6751c240dec95dd5511d4ee31d36 (patch)
tree693cb7d3d6d06bb667a89b1d4d8d2bdaee8ab983 /tic_tac_toe/src/ai.rs
init ; most of tic tac toe done
Diffstat (limited to 'tic_tac_toe/src/ai.rs')
-rw-r--r--tic_tac_toe/src/ai.rs111
1 files changed, 111 insertions, 0 deletions
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<Coord> = (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;
+}