diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | guessing_game/Cargo.lock | 133 | ||||
-rw-r--r-- | guessing_game/Cargo.toml | 7 | ||||
-rw-r--r-- | guessing_game/src/main.rs | 42 | ||||
-rw-r--r-- | hello_world/Cargo.lock | 7 | ||||
-rw-r--r-- | hello_world/Cargo.toml | 6 | ||||
-rw-r--r-- | hello_world/src/main.rs | 3 | ||||
-rw-r--r-- | tic_tac_toe/Cargo.lock | 7 | ||||
-rw-r--r-- | tic_tac_toe/Cargo.toml | 6 | ||||
-rw-r--r-- | tic_tac_toe/src/ai.rs | 111 | ||||
-rw-r--r-- | tic_tac_toe/src/board.rs | 67 | ||||
-rw-r--r-- | tic_tac_toe/src/console_helpers.rs | 11 | ||||
-rw-r--r-- | tic_tac_toe/src/main.rs | 162 |
13 files changed, 563 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2a4093 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/target
\ No newline at end of file diff --git a/guessing_game/Cargo.lock b/guessing_game/Cargo.lock new file mode 100644 index 0000000..67ee4bb --- /dev/null +++ b/guessing_game/Cargo.lock @@ -0,0 +1,133 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "guessing_game" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/guessing_game/Cargo.toml b/guessing_game/Cargo.toml new file mode 100644 index 0000000..9f9c4ac --- /dev/null +++ b/guessing_game/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "guessing_game" +version = "0.1.0" +edition = "2024" + +[dependencies] +rand = "0.8.5" diff --git a/guessing_game/src/main.rs b/guessing_game/src/main.rs new file mode 100644 index 0000000..561843d --- /dev/null +++ b/guessing_game/src/main.rs @@ -0,0 +1,42 @@ +use rand::{self, Rng}; +use std::{cmp::Ordering, io}; + +fn main() { + println!("Guessing game!"); + + let secret_number = rand::thread_rng().gen_range(1..=10); + println!("The secret number is {}", secret_number); + + loop { + println!("Please enter a guess!"); + let mut guess = String::new(); + + io::stdin() + .read_line(&mut guess) + .expect("A guess should be entered"); + + println!("You guessed: {}", guess); + + let guess: u32 = match guess.trim().parse::<u32>() { + Ok(num) => num, + Err(err) => { + println!("Couldn't parse '{}' ({})", guess.trim(), err); + continue; + } + }; + + match secret_number.cmp(&guess) { + Ordering::Less => { + println!("You guessed wrong! ({} < {})", secret_number, guess); + } + Ordering::Equal => { + println!("You guessed right! ({} = {})", secret_number, guess); + break; + } + Ordering::Greater => { + println!("You guessed wrong! ({} > {})", secret_number, guess); + } + } + println!("Try again!") + } +} diff --git a/hello_world/Cargo.lock b/hello_world/Cargo.lock new file mode 100644 index 0000000..ce5ee3d --- /dev/null +++ b/hello_world/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "hello_world" +version = "0.1.0" diff --git a/hello_world/Cargo.toml b/hello_world/Cargo.toml new file mode 100644 index 0000000..4f0a461 --- /dev/null +++ b/hello_world/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "hello_world" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/hello_world/src/main.rs b/hello_world/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/hello_world/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/tic_tac_toe/Cargo.lock b/tic_tac_toe/Cargo.lock new file mode 100644 index 0000000..30a9d90 --- /dev/null +++ b/tic_tac_toe/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "tic_tac_toe" +version = "0.1.0" diff --git a/tic_tac_toe/Cargo.toml b/tic_tac_toe/Cargo.toml new file mode 100644 index 0000000..a6c26ae --- /dev/null +++ b/tic_tac_toe/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "tic_tac_toe" +version = "0.1.0" +edition = "2024" + +[dependencies] 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!") +} |