summaryrefslogtreecommitdiff
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
init ; most of tic tac toe done
-rw-r--r--.gitignore1
-rw-r--r--guessing_game/Cargo.lock133
-rw-r--r--guessing_game/Cargo.toml7
-rw-r--r--guessing_game/src/main.rs42
-rw-r--r--hello_world/Cargo.lock7
-rw-r--r--hello_world/Cargo.toml6
-rw-r--r--hello_world/src/main.rs3
-rw-r--r--tic_tac_toe/Cargo.lock7
-rw-r--r--tic_tac_toe/Cargo.toml6
-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
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!")
+}