diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml new file mode 100644 index 0000000..2dd40ec --- /dev/null +++ b/.github/workflows/cargo.yml @@ -0,0 +1,41 @@ +name: Cargo (full) + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + container: + image: aalekhpatel07/rust:1.0 + options: --user root --security-opt seccomp=unconfined + steps: + - name: Checkout repository. + uses: actions/checkout@v3 + - name: Run tests + run: cargo nextest run --release --verbose + - name: Generate test coverage report. + run: cargo tarpaulin -o Html --output-dir ./target/tarpaulin + - name: Run clippy + run: cargo clippy --no-deps --fix + - name: Build the project. + run: | + cargo build --release + - name: Archive production build. + uses: actions/upload-artifact@v3 + with: + name: production-static-files + path: | + target/release/tic-tac-toe + - name: Archive code coverage results. + uses: actions/upload-artifact@v3 + with: + name: test-coverage-report + path: | + target/tarpaulin diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index ab9aa4e..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Rust - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose - - name: Run Clippy - run: cargo clippy --verbose diff --git a/.gitignore b/.gitignore index 2f5a8f4..2439597 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ target +.vscode/ diff --git a/Cargo.lock b/Cargo.lock index 2d0ec52..c02e85f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,280 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91f1f46651137be86f3a2b9a8359f9ab421d04d941c62b5982e1ca21113adf9" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "btoi" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c0869a9faa81f8bbf8102371105d6d0a7b79167a04c340b04ab16892246a11" +dependencies = [ + "num-traits", +] + +[[package]] +name = "clap" +version = "3.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + [[package]] name = "minimax-alpha-beta" -version = "0.1.7" +version = "0.2.0" +dependencies = [ + "anyhow", + "clap", + "shakmaty", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" + +[[package]] +name = "os_str_bytes" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "shakmaty" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4e6c06e44c662e0f737af31b8860407a2028163bf5aa351cddc3136ca721e8" +dependencies = [ + "arrayvec", + "bitflags", + "btoi", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "unicode-ident" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 5612d02..4a3ba65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "minimax-alpha-beta" -version = "0.1.7" +version = "0.2.0" authors = ["Aalekh Patel "] edition = "2018" license = "MIT" @@ -14,8 +14,20 @@ categories = ["algorithms", "mathematics"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] +[features] +default = ["tictactoe"] +tictactoe = [] +chess = ["dep:shakmaty"] +[dependencies] +shakmaty = { version = "0.21.3", optional = true } +anyhow = { version = "1.0.59" } +clap = { version = "3.2.16", features = ["derive"]} [profile.release] lto = "fat" +debug = false + +[[bin]] +name = "tic-tac-toe" +path = "src/main.rs" \ No newline at end of file diff --git a/src/drivers/mod.rs b/src/drivers/mod.rs new file mode 100644 index 0000000..b95fd9f --- /dev/null +++ b/src/drivers/mod.rs @@ -0,0 +1,2 @@ +mod tic_tac_toe; +pub use tic_tac_toe::*; diff --git a/src/drivers/tic_tac_toe.rs b/src/drivers/tic_tac_toe.rs new file mode 100644 index 0000000..7c8c450 --- /dev/null +++ b/src/drivers/tic_tac_toe.rs @@ -0,0 +1,78 @@ +use crate::games::TicTacToe; +use crate::strategy::alpha_beta_minimax::AlphaBetaMiniMaxStrategy; +use crate::strategy::game_strategy::GameStrategy; + +/// Read input. +fn get_input() -> String { + let mut buffer = String::new(); + std::io::stdin().read_line(&mut buffer).expect("Failed"); + buffer +} + +/// Play a game of any size in a REPL against the engine. +/// The default depth of 6 should make the +/// engine reasonably fast. +pub fn play_tic_tac_toe_against_computer(size: usize) { + play_tic_tac_toe_against_computer_with_depth(size, 6) +} + +/// Play a game of any size in a REPL against the engine. +/// The higher the depth, the longer it takes and +/// the more accurately the engine performs. +pub fn play_tic_tac_toe_against_computer_with_depth(size: usize, depth: i64) { + let mut ttt = TicTacToe::new(size); + loop { + println!("Board:\n{}", ttt); + println!("\n"); + + if ttt.is_game_complete() { + println!("Game is complete."); + if ttt.is_game_tied() { + println!("Game Tied!"); + break; + } else { + println!("{} wins!", ttt.get_winner().unwrap()); + break; + } + } + + let example_num: i64 = 7; + println!( + "Enter a move. (e.g. '{}' represents (row: {}, col: {}) : ", + example_num, + example_num as usize / size, + example_num as usize % size + ); + let s = get_input().trim().parse::(); + let n = s.unwrap_or(usize::MAX); + + if n == usize::MAX { + break; + } + println!( + "Move played by you: {} (i.e. {}, {})", + n, + n / size, + n % size + ); + ttt.play(&n, true); + let move_found = ttt.get_best_move(depth as i64, true); + if move_found > (ttt.size * ttt.size) { + println!("Game is complete."); + if ttt.is_game_tied() { + println!("Game Tied!"); + break; + } else { + println!("{} wins!", ttt.get_winner().unwrap()); + break; + } + } + println!( + "Move played by AI: {} (i.e. {}, {})", + move_found, + move_found / size, + move_found % size + ); + ttt.play(&move_found, false); + } +} diff --git a/src/games/chess.rs b/src/games/chess.rs new file mode 100644 index 0000000..3af4456 --- /dev/null +++ b/src/games/chess.rs @@ -0,0 +1,206 @@ +use crate::strategy::game_strategy::GameStrategy; +use anyhow::{bail, Result}; +use std::ops::{Deref, DerefMut}; + +#[cfg(feature = "chess")] +pub use shakmaty::Chess as ShakmatyChess; +use shakmaty::Position; + +#[derive(Debug, Clone)] +pub struct Chess { + pub inner: ShakmatyChess, + pub moves_played: shakmaty::MoveList, +} + +impl Default for Chess { + fn default() -> Self { + Self { + inner: ShakmatyChess::default(), + moves_played: shakmaty::MoveList::default(), + } + } +} + +impl Deref for Chess { + type Target = ShakmatyChess; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Chess { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl Chess { + pub fn new() -> Self { + Self::default() + } + + fn _undo(&self, _move: shakmaty::Move) -> Result<()> { + todo!("Implement undo for Chess moves."); + } + + pub fn undo(&mut self) -> Result<()> { + if let Some(prev_move) = self.moves_played.pop() { + self._undo(prev_move) + } else { + bail!("No moves to undo."); + } + } + + fn _play(&mut self, _move: shakmaty::Move) { + self.inner.play_unchecked(&_move); + self.moves_played.push(_move); + } +} + +impl GameStrategy for Chess { + type Player = shakmaty::Color; + type Move = Option; + type Board = shakmaty::Board; + + fn get_a_sentinel_move(&self) -> Self::Move { + None + } + + fn is_a_valid_move(&self, mv: &Self::Move) -> bool { + mv.is_some() + } + + fn play(&mut self, mv: &Self::Move, maximizer: bool) { + if let Some(_mv) = mv { + if maximizer { + assert!(self.inner.turn() == shakmaty::Color::White); + // self.inner.play(&_mv); + self._play(_mv.clone()); + self.moves_played.push(_mv.clone()); + } else { + assert!(self.inner.turn() == shakmaty::Color::Black); + // self.inner.play(&mv); + self._play(_mv.clone()); + self.moves_played.push(_mv.clone()); + } + } else { + panic!("Invalid move. Sentinel?"); + } + } + + fn evaluate(&self) -> f64 { + todo!("Implement a static evaluation of a chess position.") + } + + fn clear(&mut self, mv: &Self::Move) { + if mv.is_none() { + panic!("Invalid move. Sentinel?"); + } + let prev_move = self.moves_played.pop(); + + if prev_move.is_none() { + panic!("Invalid move. Sentinel?"); + } + let _mv = prev_move.unwrap(); + self._undo(_mv.clone()) + .expect(&format!("Couldn't undo move: {:#?}", _mv)); + } + + fn get_available_moves(&self) -> Vec { + self.legal_moves() + .iter() + .map(|mv| Some(mv.clone())) + .collect() + } + + fn get_board(&self) -> &Self::Board { + &self.inner.board() + } + fn get_winner(&self) -> Option { + if let Some(outcome) = self.outcome() { + match outcome { + shakmaty::Outcome::Draw => None, + shakmaty::Outcome::Decisive { winner } => Some(winner), + } + } else { + None + } + } + + fn is_game_complete(&self) -> bool { + self.outcome().is_some() + } + + fn is_game_tied(&self) -> bool { + if let Some(outcome) = self.outcome() { + match outcome { + shakmaty::Outcome::Draw => true, + _ => false, + } + } else { + false + } + } +} + +#[cfg(test)] +pub mod tests { + pub use super::Chess; + pub use crate::strategy::game_strategy::GameStrategy; + use shakmaty::{ + CastlingMode, Chess as ChessGame, Color, FromSetup, Piece, Position, Role, Setup, Square, + }; + + #[test] + fn test_chess_new() { + let chess = Chess::new(); + assert_eq!(chess.turn(), shakmaty::Color::White); + } + + #[test] + fn test_chess_evaluate() { + let chess = Chess::new(); + assert_eq!(chess.evaluate(), 0.); + } + + #[test] + fn test_chess_available_moves() { + let chess = Chess::new(); + let moves = chess.get_available_moves(); + assert_eq!(moves.len(), chess.legal_moves().len()); + + println!("{:?}", moves); + } + + #[test] + fn test_chess_available_moves_capture() { + let mut chess_setup = Setup::default(); + + let mut board = chess_setup.board; + board.set_piece_at( + Square::E4, + Piece { + color: Color::White, + role: Role::Pawn, + }, + ); + board.remove_piece_at(Square::E2).unwrap(); + board.remove_piece_at(Square::D7).unwrap(); + board.set_piece_at( + Square::D5, + Piece { + color: Color::Black, + role: Role::Pawn, + }, + ); + + chess_setup.board = board; + + let chess = ChessGame::from_setup(chess_setup, CastlingMode::Standard).unwrap(); + + let moves = chess.capture_moves(); + assert_eq!(moves.len(), 1); + // println!("{moves:#?}"); + } +} diff --git a/src/games/mod.rs b/src/games/mod.rs new file mode 100644 index 0000000..312f91e --- /dev/null +++ b/src/games/mod.rs @@ -0,0 +1,6 @@ +mod tic_tac_toe; +pub use tic_tac_toe::TicTacToe; +#[cfg(feature = "chess")] +mod chess; +#[cfg(feature = "chess")] +pub use chess::Chess; diff --git a/src/games/tic_tac_toe.rs b/src/games/tic_tac_toe.rs new file mode 100644 index 0000000..1147bf3 --- /dev/null +++ b/src/games/tic_tac_toe.rs @@ -0,0 +1,281 @@ +use std::fmt::Display; + +use crate::strategy::game_strategy::GameStrategy; + +#[derive(Debug, Clone)] +pub struct TicTacToe { + pub board: Vec, + pub size: usize, + pub default_char: char, + pub maximizer: char, + pub minimizer: char, +} + +impl Display for TicTacToe { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for idx in 0..self.size { + let start = self.size * idx; + let end = self.size * (idx + 1); + let sub: &[char] = &self.board[start as usize..end as usize]; + + for &x in sub.iter() { + write!(f, "{}", x)?; + } + writeln!(f)?; + } + Ok(()) + } +} + +impl Default for TicTacToe { + fn default() -> Self { + TicTacToe::new(3) + } +} + +/// Implements all necessary +/// methods to operate a TicTacToe +/// game. +impl TicTacToe { + pub fn new(size: usize) -> Self { + let board: Vec = vec!['-'; (size * size) as usize]; + Self { + board, + size, + default_char: '-', + maximizer: 'o', + minimizer: 'x', + } + } + + pub fn with_player_1(self, character: char) -> Self { + Self { + maximizer: character, + ..self + } + } + pub fn with_player_2(self, character: char) -> Self { + Self { + minimizer: character, + ..self + } + } + pub fn with_default_char(self, character: char) -> Self { + Self { + default_char: character, + ..self + } + } + + /// Check the main and anti-diagonals + /// for a winner. + pub fn check_diagonals(&self) -> char { + let mut winner = self.default_char; + if self.check_diagonal(self.maximizer, true) || self.check_diagonal(self.maximizer, false) { + winner = self.maximizer + } else if self.check_diagonal(self.minimizer, true) + || self.check_diagonal(self.minimizer, false) + { + winner = self.minimizer + } + winner + } + + /// Check the rows of the grid for a winner. + pub fn check_rows(&self) -> char { + let mut winner = self.default_char; + + for row in 0..self.size as usize { + if self.check_row(self.maximizer, row) { + winner = self.maximizer; + break; + } else if self.check_row(self.minimizer, row) { + winner = self.minimizer; + break; + } + } + winner + } + + /// Check the columns of the grid for a winner. + pub fn check_cols(&self) -> char { + let mut winner = self.default_char; + + for col in 0..self.size as usize { + if self.check_col(self.maximizer, col) { + winner = self.maximizer; + break; + } else if self.check_col(self.minimizer, col) { + winner = self.minimizer; + break; + } + } + winner + } + + /// Check a given column if a given player has won. + fn check_col(&self, ch: char, col_num: usize) -> bool { + for row in 0..self.size as usize { + if self.board[self.size as usize * row + col_num] != ch { + return false; + } + } + true + } + + /// Check a given row if a given player has won. + fn check_row(&self, ch: char, row_num: usize) -> bool { + for col in 0..self.size as usize { + if self.board[self.size as usize * row_num + col] != ch { + return false; + } + } + true + } + + /// Check the main and anti diagonals if a + /// given player has won. + fn check_diagonal(&self, ch: char, diag: bool) -> bool { + // main diagonal is represented by true. + if diag { + for idx in 0..self.size as usize { + if self.board[(self.size as usize * idx as usize) + idx] != ch { + return false; + } + } + true + } else { + for idx in 0..self.size as usize { + if self.board[(self.size as usize * (self.size as usize - 1 - idx as usize)) + idx] + != ch + { + return false; + } + } + true + } + } +} + +/// Endow upon TicTacToe the ability to +/// play games. +impl GameStrategy for TicTacToe { + /// The Player is a char. + /// Usually one of 'o', 'O', 'x', 'X', '-'. + type Player = char; + + /// The Move is a single number representing an + /// index of the Board vector, i.e. in range + /// `[0, (size * size) - 1]`. + type Move = usize; + + /// The Board is a single vector of length `size * size`. + type Board = Vec; + + fn evaluate(&self) -> f64 { + if self.is_game_tied() { + 0. + } else { + let _winner = self.get_winner().unwrap(); + if _winner == self.maximizer { + 1000. + } else { + -1000. + } + } + } + + fn get_winner(&self) -> Option { + let mut winner = self.check_diagonals(); + + if winner == self.default_char { + winner = self.check_rows(); + } + if winner == self.default_char { + winner = self.check_cols(); + } + Some(winner) + } + + fn is_game_tied(&self) -> bool { + let _winner = self.get_winner().unwrap(); + + _winner == self.default_char && self.get_available_moves().is_empty() + } + + fn is_game_complete(&self) -> bool { + let _winner = self.get_winner(); + + self.get_available_moves().is_empty() || _winner.unwrap() != '-' + } + + fn get_available_moves(&self) -> Vec { + let mut moves: Vec = vec![]; + for idx in 0..(self.size * self.size) as usize { + if self.board[idx] == '-' { + moves.push(idx) + } + } + moves + } + + fn play(&mut self, &mv: &Self::Move, maximizer: bool) { + // player: true means the maximizer's turn. + + if maximizer { + self.board[mv] = self.maximizer; + } else { + self.board[mv] = self.minimizer; + } + } + + fn clear(&mut self, &mv: &Self::Move) { + self.board[mv] = self.default_char + } + + fn get_board(&self) -> &Self::Board { + &self.board + } + + fn is_a_valid_move(&self, &mv: &Self::Move) -> bool { + self.board[mv] == self.default_char + } + + fn get_a_sentinel_move(&self) -> Self::Move { + self.size * self.size + 1 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::strategy::alpha_beta_minimax::AlphaBetaMiniMaxStrategy; + + #[test] + fn best_move_in_given_3_by_3() { + let mut ttt = TicTacToe::new(3) + .with_player_1('o') + .with_player_2('x') + .with_default_char('-'); + + ttt.play(&8, true); + ttt.play(&7, false); + ttt.play(&5, true); + + assert_eq!(ttt.get_best_move(9, false), 2); + } + + #[test] + fn test_should_always_tie_a_3_by_3_after_9_moves_at_depth_9() { + let mut ttt = TicTacToe::new(3); + for move_number in 0..=8 { + let is_maximising = move_number % 2 == 0; + let i = ttt.get_best_move(9, is_maximising); + ttt.play(&i, is_maximising); + println!("{}", ttt); + // ttt.print_board(); + } + assert!(ttt.is_game_complete()); + assert!(ttt.is_game_tied()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8083ade..4cf18f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ //! Also, where possible, a parallel processing //! implementation is provided. +mod drivers; /// Contains sruct and necessary implementations /// for `TicTacToe`: a popular two-player game /// where one player places a symbol - 'X' and another @@ -19,505 +20,8 @@ /// assert_eq!(tic_tac_toe.size, 3); /// assert_eq!(tic_tac_toe.default_char, '-'); /// ``` -mod tests; +// mod tests; +pub mod games; +pub mod strategy; -/// Contains the concrete implementation of -/// a TicTacToe game. -pub mod tictactoe { - - /// Contains basic data about a - /// TicTacToe game. - pub struct TicTacToe { - pub board: Vec, - pub size: usize, - pub default_char: char, - pub maximizer: char, - pub minimizer: char, - } - - /// Implements all necessary - /// methods to operate a TicTacToe - /// game. - impl TicTacToe { - /// Create a new game of TicTacToe - /// with a fresh board. - pub fn create_game( - size: usize, - default_char: Option, - maximizer: Option, - minimizer: Option, - ) -> TicTacToe { - let board: Vec = vec![default_char.unwrap_or('-'); (size * size) as usize]; - - TicTacToe { - size, - board, - maximizer: maximizer.unwrap_or('o'), - minimizer: minimizer.unwrap_or('x'), - default_char: default_char.unwrap_or('-'), - } - } - - /// Pretty print a TicTacToe board - /// for visualizing a game state. - pub fn print_board(&self) { - for idx in 0..self.size { - let start = self.size * idx; - let end = self.size * (idx + 1); - let sub: &[char] = &self.board[start as usize..end as usize]; - - for &x in sub.iter() { - print!("{}", x); - } - println!() - } - } - - /// Check the main and anti-diagonals - /// for a winner. - pub fn check_diagonals(&self) -> char { - let mut winner = self.default_char; - if self.check_diagonal(self.maximizer, true) - || self.check_diagonal(self.maximizer, false) - { - winner = self.maximizer - } else if self.check_diagonal(self.minimizer, true) - || self.check_diagonal(self.minimizer, false) - { - winner = self.minimizer - } - winner - } - - /// Check the rows of the grid for a winner. - pub fn check_rows(&self) -> char { - let mut winner = self.default_char; - - for row in 0..self.size as usize { - if self.check_row(self.maximizer, row) { - winner = self.maximizer; - break; - } else if self.check_row(self.minimizer, row) { - winner = self.minimizer; - break; - } - } - winner - } - - /// Check the columns of the grid for a winner. - pub fn check_cols(&self) -> char { - let mut winner = self.default_char; - - for col in 0..self.size as usize { - if self.check_col(self.maximizer, col) { - winner = self.maximizer; - break; - } else if self.check_col(self.minimizer, col) { - winner = self.minimizer; - break; - } - } - winner - } - - /// Check a given column if a given player has won. - fn check_col(&self, ch: char, col_num: usize) -> bool { - for row in 0..self.size as usize { - if self.board[self.size as usize * row + col_num] != ch { - return false; - } - } - true - } - - /// Check a given row if a given player has won. - fn check_row(&self, ch: char, row_num: usize) -> bool { - for col in 0..self.size as usize { - if self.board[self.size as usize * row_num + col] != ch { - return false; - } - } - true - } - - /// Check the main and anti diagonals if a - /// given player has won. - fn check_diagonal(&self, ch: char, diag: bool) -> bool { - // main diagonal is represented by true. - if diag { - for idx in 0..self.size as usize { - if self.board[(self.size as usize * idx as usize) + idx] != ch { - return false; - } - } - true - } else { - for idx in 0..self.size as usize { - if self.board - [(self.size as usize * (self.size as usize - 1 - idx as usize)) + idx] - != ch - { - return false; - } - } - true - } - } - } -} - -/// Contains the necessary behaviours for -/// two-player Minimax games. -pub mod strategy { - - /// Any two-player Minimax game must - /// have this behavior. In other words, - /// these functions should yield meaningful outputs - /// for any two-player games. - pub trait Strategy { - type Player; - type Move; - type Board; - - /// Ability to statically evaluate the current game state. - fn evaluate(&self) -> f64; - /// Identify a winner, if exists. - fn get_winner(&self) -> Self::Player; - /// Identify if the game is tied. - fn is_game_tied(&self) -> bool; - /// Identify if the game is in a completed state. - fn is_game_complete(&self) -> bool; - /// Ability to produce a collection of playable legal moves - /// in the current position. - fn get_available_moves(&self) -> Vec; - /// Modify the game state by playing a given move. - fn play(&mut self, mv: &Self::Move, maximizer: bool); - /// Modify the game state by resetting a given move. - fn clear(&mut self, mv: &Self::Move); - /// Get the current state of the board. - fn get_board(&self) -> &Self::Board; - /// Determine if a given move is valid. - fn is_a_valid_move(&self, mv: &Self::Move) -> bool; - /// Ability to produce a sentinel (not-playable) move. - fn get_a_sentinel_move(&self) -> Self::Move; - } - - /// The behaviour required of any - /// minimax game engine. - pub trait AlphaBetaMiniMaxStrategy: Strategy { - /// The ability to get the best move - /// in the current state and for the - /// current player. - fn get_best_move( - &mut self, - max_depth: i64, - is_maximizing: bool, - ) -> ::Move; - - /// The ability to produce a best (good enough, sometimes) - /// evaluation score possible over all - /// possible moves at the current game state. - fn minimax_score( - &mut self, - depth: i64, - is_maximizing: bool, - alpha: f64, - beta: f64, - max_depth: i64, - ) -> f64; - } -} - -/// Contains the concrete implementations -/// for the game-playing strategic behaviour -/// of TicTacToe. -pub mod games { - - use crate::strategy::*; - use crate::tictactoe::*; - - /// Endow upon TicTacToe the ability to - /// play games. - impl Strategy for TicTacToe { - /// The Player is a char. - /// Usually one of 'o', 'O', 'x', 'X', '-'. - type Player = char; - - /// The Move is a single number representing an - /// index of the Board vector, i.e. in range - /// `[0, (size * size) - 1]`. - type Move = usize; - - /// The Board is a single vector of length `size * size`. - type Board = Vec; - - fn evaluate(&self) -> f64 { - if self.is_game_tied() { - 0. - } else { - let _winner = self.get_winner(); - if _winner == self.maximizer { - 1000. - } else { - -1000. - } - } - } - - fn get_winner(&self) -> Self::Player { - let mut winner = self.check_diagonals(); - - if winner == self.default_char { - winner = self.check_rows(); - } - if winner == self.default_char { - winner = self.check_cols(); - } - winner - } - - fn is_game_tied(&self) -> bool { - let _winner = self.get_winner(); - - _winner == self.default_char && self.get_available_moves().is_empty() - } - - fn is_game_complete(&self) -> bool { - let _winner = self.get_winner(); - - self.get_available_moves().is_empty() || _winner != '-' - } - - fn get_available_moves(&self) -> Vec { - let mut moves: Vec = vec![]; - for idx in 0..(self.size * self.size) as usize { - if self.board[idx] == '-' { - moves.push(idx) - } - } - moves - } - - fn play(&mut self, &mv: &Self::Move, maximizer: bool) { - // player: true means the maximizer's turn. - - if maximizer { - self.board[mv] = self.maximizer; - } else { - self.board[mv] = self.minimizer; - } - } - - fn clear(&mut self, &mv: &Self::Move) { - self.board[mv] = self.default_char - } - - fn get_board(&self) -> &Self::Board { - &self.board - } - - fn is_a_valid_move(&self, &mv: &Self::Move) -> bool { - self.board[mv] == self.default_char - } - - fn get_a_sentinel_move(&self) -> Self::Move { - self.size * self.size + 1 - } - } - - pub const INF: f64 = f64::INFINITY; - pub const NEG_INF: f64 = f64::NEG_INFINITY; - - /// Endow upon anything the ability to - /// use the AlphaBetaMiniMaxStrategy implementation - /// of the game engine as long as it understands - /// how to behave as Strategy. - impl AlphaBetaMiniMaxStrategy for T { - fn get_best_move( - &mut self, - max_depth: i64, - is_maximizing: bool, - ) -> ::Move { - let mut best_move: ::Move = self.get_a_sentinel_move(); - - if self.is_game_complete() { - return best_move; - } - - let alpha = NEG_INF; - let beta = INF; - - if is_maximizing { - let mut best_move_val: f64 = INF; - - for mv in self.get_available_moves() { - self.play(&mv, false); - let value = self.minimax_score(max_depth, true, alpha, beta, max_depth); - self.clear(&mv); - if value <= best_move_val { - best_move_val = value; - best_move = mv; - } - } - - best_move - } else { - let mut best_move_val: f64 = NEG_INF; - - for mv in self.get_available_moves() { - self.play(&mv, false); - let value = self.minimax_score(max_depth, false, alpha, beta, max_depth); - self.clear(&mv); - if value >= best_move_val { - best_move_val = value; - best_move = mv; - } - } - best_move - } - } - - fn minimax_score( - &mut self, - depth: i64, - is_maximizing: bool, - mut alpha: f64, - mut beta: f64, - max_depth: i64, - ) -> f64 { - let avail: Vec<::Move> = self.get_available_moves(); - if depth == 0 || self.is_game_complete() || avail.is_empty() { - return self.evaluate(); - } - - if is_maximizing { - let mut value = NEG_INF; - for idx in avail { - self.play(&idx, true); - let score = self.minimax_score(depth - 1, false, alpha, beta, max_depth); - if score >= value { - value = score; - } - if score >= alpha { - alpha = score; - } - self.clear(&idx); - if beta <= alpha { - break; - } - } - if value != 0. { - return value - (max_depth - depth) as f64; - } - value - } else { - let mut value = INF; - for idx in avail { - self.play(&idx, false); - let score = self.minimax_score(depth - 1, true, alpha, beta, max_depth); - if score <= value { - value = score; - } - if score <= beta { - beta = score; - } - self.clear(&idx); - if beta <= alpha { - break; - } - } - - if value != 0. { - return value + (max_depth - depth) as f64; - } - value - } - } - } -} - -/// Contains simple drivers for some -/// of the games supported by the Minimax -/// engine. -pub mod drivers { - - use crate::strategy::*; - use crate::tictactoe::*; - - /// Read input. - pub fn get_input() -> String { - let mut buffer = String::new(); - std::io::stdin().read_line(&mut buffer).expect("Failed"); - buffer - } - - /// Play a game of any size in a REPL against the engine. - /// The default depth of 6 should make the - /// engine reasonably fast. - pub fn play_tic_tac_toe_against_computer(size: usize) { - play_tic_tac_toe_against_computer_with_depth(size, 6) - } - - /// Play a game of any size in a REPL against the engine. - /// The higher the depth, the longer it takes and - /// the more accurately the engine performs. - pub fn play_tic_tac_toe_against_computer_with_depth(size: usize, depth: i64) { - let mut ttt = TicTacToe::create_game(size, None, None, None); - loop { - println!("Board:\n"); - ttt.print_board(); - println!("\n"); - - if ttt.is_game_complete() { - println!("Game is complete."); - if ttt.is_game_tied() { - println!("Game Tied!"); - break; - } else { - println!("{} wins!", ttt.get_winner()); - break; - } - } - - let example_num: i64 = 7; - println!( - "Enter a move. (e.g. '{}' represents (row: {}, col: {}) : ", - example_num, - example_num as usize / size, - example_num as usize % size - ); - let s = get_input().trim().parse::(); - let n = s.unwrap_or(usize::MAX); - - if n == usize::MAX { - break; - } - println!( - "Move played by you: {} (i.e. {}, {})", - n, - n / size, - n % size - ); - ttt.play(&n, true); - let move_found = ttt.get_best_move(depth as i64, true); - if move_found > (ttt.size * ttt.size) { - println!("Game is complete."); - if ttt.is_game_tied() { - println!("Game Tied!"); - break; - } else { - println!("{} wins!", ttt.get_winner()); - break; - } - } - println!( - "Move played by AI: {} (i.e. {}, {})", - move_found, - move_found / size, - move_found % size - ); - ttt.play(&move_found, false); - } - } -} +pub use drivers::*; diff --git a/src/main.rs b/src/main.rs index 249fb40..d6d1942 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,22 @@ -use minimax_alpha_beta::drivers::*; +use clap::Parser; +use minimax_alpha_beta::*; + +#[derive(Parser, Debug, Clone)] +#[clap( + author = "Aalekh Patel ", + version = "0.2.0", + about = "Play a game of Tic Tac Toe with a computer opponent that uses the Alpha-Beta Minimax Engine." +)] +pub struct Cli { + /// The size of the board. + #[clap(long, default_value_t = 3)] + pub size: usize, + /// The depth of the search. + #[clap(long, default_value_t = 9)] + pub depth: i64, +} fn main() { - play_tic_tac_toe_against_computer_with_depth(4, 7); + let cli = Cli::parse(); + play_tic_tac_toe_against_computer_with_depth(cli.size, cli.depth); } diff --git a/src/strategy/alpha_beta_minimax.rs b/src/strategy/alpha_beta_minimax.rs new file mode 100644 index 0000000..60571e1 --- /dev/null +++ b/src/strategy/alpha_beta_minimax.rs @@ -0,0 +1,132 @@ +use crate::strategy::game_strategy::GameStrategy; + +pub const INF: f64 = f64::INFINITY; +pub const NEG_INF: f64 = f64::NEG_INFINITY; + +/// The behaviour required of any +/// minimax game engine. +pub trait AlphaBetaMiniMaxStrategy: GameStrategy { + /// The ability to get the best move + /// in the current state and for the + /// current player. + fn get_best_move( + &mut self, + max_depth: i64, + is_maximizing: bool, + ) -> ::Move; + + /// The ability to produce a best (good enough, sometimes) + /// evaluation score possible over all + /// possible moves at the current game state. + fn minimax_score( + &mut self, + depth: i64, + is_maximizing: bool, + alpha: f64, + beta: f64, + max_depth: i64, + ) -> f64; +} + +/// Endow upon anything the ability to +/// use the AlphaBetaMiniMaxStrategy implementation +/// of the game engine as long as it understands +/// how to behave as Strategy. +impl AlphaBetaMiniMaxStrategy for T { + fn get_best_move( + &mut self, + max_depth: i64, + is_maximizing: bool, + ) -> ::Move { + let mut best_move: ::Move = self.get_a_sentinel_move(); + + if self.is_game_complete() { + return best_move; + } + + let alpha = NEG_INF; + let beta = INF; + + if is_maximizing { + let mut best_move_val: f64 = INF; + + for mv in self.get_available_moves() { + self.play(&mv, !is_maximizing); + let value = self.minimax_score(max_depth, is_maximizing, alpha, beta, max_depth); + self.clear(&mv); + if value <= best_move_val { + best_move_val = value; + best_move = mv; + } + } + + best_move + } else { + let mut best_move_val: f64 = NEG_INF; + + for mv in self.get_available_moves() { + self.play(&mv, !is_maximizing); + let value = self.minimax_score(max_depth, is_maximizing, alpha, beta, max_depth); + self.clear(&mv); + if value >= best_move_val { + best_move_val = value; + best_move = mv; + } + } + best_move + } + } + + fn minimax_score( + &mut self, + depth: i64, + is_maximizing: bool, + mut alpha: f64, + mut beta: f64, + max_depth: i64, + ) -> f64 { + let avail: Vec<::Move> = self.get_available_moves(); + if depth == 0 || self.is_game_complete() || avail.is_empty() { + return self.evaluate(); + } + + if is_maximizing { + let mut value = NEG_INF; + for idx in avail { + self.play(&idx, is_maximizing); + let score = self.minimax_score(depth - 1, !is_maximizing, alpha, beta, max_depth); + + value = value.max(score); + alpha = alpha.max(score); + + self.clear(&idx); + if beta <= alpha { + break; + } + } + if value != 0. { + return value - (max_depth - depth) as f64; + } + value + } else { + let mut value = INF; + for idx in avail { + self.play(&idx, is_maximizing); + let score = self.minimax_score(depth - 1, !is_maximizing, alpha, beta, max_depth); + + value = value.min(score); + beta = beta.min(score); + + self.clear(&idx); + if beta <= alpha { + break; + } + } + + if value != 0. { + return value + (max_depth - depth) as f64; + } + value + } + } +} diff --git a/src/strategy/game_strategy.rs b/src/strategy/game_strategy.rs new file mode 100644 index 0000000..c59ff2f --- /dev/null +++ b/src/strategy/game_strategy.rs @@ -0,0 +1,38 @@ +/// Any two-player Minimax game must +/// have this behavior. In other words, +/// these functions should yield meaningful outputs +/// for any two-player games. + +pub enum GameResult { + Player1Win, + Player2Win, + Draw, +} + +pub trait GameStrategy { + type Player; + type Move; + type Board; + + /// Ability to statically evaluate the current game state. + fn evaluate(&self) -> f64; + /// Identify a winner, if exists. + fn get_winner(&self) -> Option; + /// Identify if the game is tied. + fn is_game_tied(&self) -> bool; + /// Identify if the game is in a completed state. + fn is_game_complete(&self) -> bool; + /// Ability to produce a collection of playable legal moves + /// in the current position. + fn get_available_moves(&self) -> Vec; + /// Modify the game state by playing a given move. + fn play(&mut self, mv: &Self::Move, maximizer: bool); + /// Modify the game state by resetting a given move. + fn clear(&mut self, mv: &Self::Move); + /// Get the current state of the board. + fn get_board(&self) -> &Self::Board; + /// Determine if a given move is valid. + fn is_a_valid_move(&self, mv: &Self::Move) -> bool; + /// Ability to produce a sentinel (not-playable) move. + fn get_a_sentinel_move(&self) -> Self::Move; +} diff --git a/src/strategy/mod.rs b/src/strategy/mod.rs new file mode 100644 index 0000000..02729ac --- /dev/null +++ b/src/strategy/mod.rs @@ -0,0 +1,2 @@ +pub mod alpha_beta_minimax; +pub mod game_strategy; diff --git a/src/tests.rs b/src/tests.rs deleted file mode 100644 index fe80084..0000000 --- a/src/tests.rs +++ /dev/null @@ -1,68 +0,0 @@ -#[cfg(test)] - -mod tictactoe { - - use crate::strategy::{AlphaBetaMiniMaxStrategy, Strategy}; - use crate::tictactoe::TicTacToe; - - #[test] - fn setup_game() { - let ttt = TicTacToe::create_game(5, None, None, None); - assert_eq!(ttt.is_game_complete(), false); - assert_eq!(ttt.is_game_tied(), false); - assert_eq!(ttt.get_winner(), '-'); - assert_eq!(ttt.size, 5); - - let ttt_regular = TicTacToe::create_game(5, Some('_'), Some('O'), Some('X')); - assert_eq!(ttt_regular.default_char, '_'); - assert_eq!(ttt_regular.maximizer, 'O'); - assert_eq!(ttt_regular.minimizer, 'X'); - } - - #[test] - fn test_get_best_move_3_by_3_depth_12() { - let mut ttt = TicTacToe::create_game(3, None, None, None); - - ttt.play(&4, true); - ttt.play(&0, false); - - let best = ttt.get_best_move(12 as i64, true); - - // Since the game state is: - // - // x \_ \_ - // - // \_ o \_ - // - // \_ \_ \_ - // - // and it is 'o' to move, - // any corner square is the best. - // - // So let's test that it is indeed the case. - - assert_eq!(best % 2, 0); - } - - #[test] - fn test_get_best_move_4_by_4_depth_6() { - let mut ttt = TicTacToe::create_game(4, None, None, None); - ttt.get_best_move(3 as i64, true); - } - #[test] - fn test_get_best_move_4_by_4_depth_8() { - let mut ttt = TicTacToe::create_game(4, None, None, None); - ttt.get_best_move(2 as i64, true); - } - #[test] - fn test_get_best_move_5_by_5_depth_6_after_5_moves_played() { - let mut ttt = TicTacToe::create_game(5, None, None, None); - - ttt.play(&12, true); - ttt.play(&22, false); - ttt.play(&1, true); - ttt.play(&16, false); - ttt.play(&8, true); - ttt.get_best_move(6 as i64, false); - } -}