summaryrefslogtreecommitdiff
path: root/tic_tac_toe/src
diff options
context:
space:
mode:
Diffstat (limited to 'tic_tac_toe/src')
-rw-r--r--tic_tac_toe/src/ai.rs111
-rw-r--r--tic_tac_toe/src/board.rs67
-rw-r--r--tic_tac_toe/src/console_helpers.rs11
-rw-r--r--tic_tac_toe/src/main.rs162
4 files changed, 351 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;
+}
diff --git a/tic_tac_toe/src/board.rs b/tic_tac_toe/src/board.rs
new file mode 100644
index 0000000..5ad23ad
--- /dev/null
+++ b/tic_tac_toe/src/board.rs
@@ -0,0 +1,67 @@
+use std::fmt;
+
+#[derive(Copy, Clone, PartialEq, Debug)]
+pub enum Tile {
+ PlayerOne,
+ PlayerTwo,
+ Unowned,
+}
+
+pub enum GameState {
+ PlayerOneWin,
+ PlayerTwoWin,
+ Draw,
+ InProgress,
+}
+
+impl fmt::Display for Tile {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let sym = match self {
+ Tile::PlayerOne => "X",
+ Tile::PlayerTwo => "O",
+ Tile::Unowned => "_",
+ };
+
+ write!(f, "{sym}")
+ }
+}
+
+pub type Coord = (usize, usize);
+
+pub fn parse_coord(val: &str) -> Result<Coord, &'static str> {
+ let split = val.split(",");
+
+ let res: Vec<usize> = split
+ .map(|v| v.trim().parse::<usize>().expect("Couldn't parse!"))
+ .collect();
+
+ if res.len() != 2 {
+ return Err("Must provide exactly two dimensional coordinates");
+ }
+
+ if res[0] < 1 || res[0] > 3 || res[1] < 1 || res[1] > 3 {
+ return Err("Coordinates must be between 1 and 3");
+ }
+
+ return Ok((res[0] - 1, res[1] - 1));
+}
+
+#[derive(Clone, Debug)]
+pub struct Board {
+ pub state: [[Tile; 3]; 3],
+}
+
+impl Board {
+ pub fn get_at_coord(&self, coord: Coord) -> Tile {
+ self.state[coord.0][coord.1]
+ }
+ pub fn set_at_coord(&mut self, coord: Coord, tile: Tile) -> () {
+ self.state[coord.0][coord.1] = tile;
+ }
+}
+
+pub fn init_board() -> Board {
+ return Board {
+ state: [[Tile::Unowned; 3]; 3],
+ };
+}
diff --git a/tic_tac_toe/src/console_helpers.rs b/tic_tac_toe/src/console_helpers.rs
new file mode 100644
index 0000000..ef293a5
--- /dev/null
+++ b/tic_tac_toe/src/console_helpers.rs
@@ -0,0 +1,11 @@
+pub fn write_header(header: &str) {
+ println!("--------{header}--------")
+}
+
+pub fn write_space() {
+ println!("\n\n")
+}
+
+pub fn clear_scrn() {
+ print!("\x1B[2J");
+}
diff --git a/tic_tac_toe/src/main.rs b/tic_tac_toe/src/main.rs
new file mode 100644
index 0000000..19285a2
--- /dev/null
+++ b/tic_tac_toe/src/main.rs
@@ -0,0 +1,162 @@
+mod ai;
+mod board;
+mod console_helpers;
+
+use ai::get_best_move;
+use board::{Board, Coord, GameState, Tile, init_board, parse_coord};
+use console_helpers::{clear_scrn, write_header};
+
+use std::io;
+
+fn render_board(board: &Board) -> () {
+ println!(" 1 2 3");
+ let mut y = 1;
+ for row_i in 0..3 {
+ let row: Vec<Tile> = (0..3).map(|col_i| board.state[col_i][row_i]).collect();
+ print!("{y} ");
+ for tile in row {
+ print!(" {tile} ")
+ }
+ println!("\n");
+ y += 1;
+ }
+}
+
+fn get_coord_input() -> Coord {
+ loop {
+ let mut entered = String::new();
+ match io::stdin().read_line(&mut entered) {
+ Ok(_) => {}
+ Err(_) => {
+ println!("Couldn't read input!");
+ continue;
+ }
+ };
+
+ let coord = match parse_coord(&entered) {
+ Ok(c) => c,
+ Err(e) => {
+ println!("Couldn't parse input '{entered}'! ({e})");
+ continue;
+ }
+ };
+
+ return coord;
+ }
+}
+
+fn check_state(board: &Board) -> GameState {
+ //check row
+ let mut has_space = false;
+ for row_i in 0..3 {
+ let row: Vec<Tile> = (0..3)
+ .map(|col_i| {
+ if !has_space && board.state[col_i][row_i] == Tile::Unowned {
+ has_space = true
+ }
+ return board.state[col_i][row_i];
+ })
+ .collect();
+ if row[0] != Tile::Unowned && row[0] == row[1] && row[1] == row[2] {
+ return match row[0] {
+ Tile::PlayerOne => GameState::PlayerOneWin,
+ Tile::PlayerTwo => GameState::PlayerTwoWin,
+ Tile::Unowned => panic!("Impossible state"),
+ };
+ }
+ }
+
+ //check draw
+ if !has_space {
+ return GameState::Draw;
+ }
+
+ //check col
+ for col in board.state {
+ if col[0] != Tile::Unowned && col[0] == col[1] && col[1] == col[2] {
+ return match col[0] {
+ Tile::PlayerOne => GameState::PlayerOneWin,
+ Tile::PlayerTwo => GameState::PlayerTwoWin,
+ Tile::Unowned => panic!("Impossible state"),
+ };
+ }
+ }
+
+ //check diagonal
+ if board.state[0][0] != Tile::Unowned
+ && board.state[0][0] == board.state[1][1]
+ && board.state[1][1] == board.state[2][2]
+ {
+ return match board.state[0][0] {
+ Tile::PlayerOne => GameState::PlayerOneWin,
+ Tile::PlayerTwo => GameState::PlayerTwoWin,
+ Tile::Unowned => panic!("Impossible state"),
+ };
+ }
+
+ if board.state[2][0] != Tile::Unowned
+ && board.state[2][0] == board.state[1][1]
+ && board.state[1][1] == board.state[0][2]
+ {
+ return match board.state[2][0] {
+ Tile::PlayerOne => GameState::PlayerOneWin,
+ Tile::PlayerTwo => GameState::PlayerTwoWin,
+ Tile::Unowned => panic!("Impossible state"),
+ };
+ }
+
+ //no wincon
+ return GameState::InProgress;
+}
+
+fn main() {
+ let mut board = init_board();
+ loop {
+ // clear_scrn();
+ write_header("Current state");
+ render_board(&board);
+
+ println!("Enter a move (e.g., 1,2) >");
+ let coord = get_coord_input();
+ board.set_at_coord(coord, Tile::PlayerOne);
+
+ match check_state(&board) {
+ GameState::InProgress => (),
+ GameState::Draw => {
+ println!("Draw!");
+ break;
+ }
+ GameState::PlayerOneWin => {
+ println!("Player one wins!");
+ break;
+ }
+ GameState::PlayerTwoWin => {
+ println!("Player two wins!");
+ break;
+ }
+ }
+
+ //get AI move
+ let ai_move = get_best_move(&board, Tile::PlayerTwo, true, 0);
+ board.set_at_coord(ai_move.coord, Tile::PlayerTwo);
+
+ match check_state(&board) {
+ GameState::InProgress => (),
+ GameState::Draw => {
+ println!("Draw!");
+ break;
+ }
+ GameState::PlayerOneWin => {
+ println!("Player one wins!");
+ break;
+ }
+ GameState::PlayerTwoWin => {
+ println!("Player two wins!");
+ break;
+ }
+ }
+ }
+
+ render_board(&board);
+ println!("Game over!")
+}