├── .gitignore ├── src ├── drivers │ ├── mod.rs │ └── tic_tac_toe.rs ├── strategy │ ├── mod.rs │ ├── game_strategy.rs │ └── alpha_beta_minimax.rs ├── games │ ├── mod.rs │ ├── chess.rs │ └── tic_tac_toe.rs ├── main.rs └── lib.rs ├── LICENSE ├── Cargo.toml ├── .github └── workflows │ └── cargo.yml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /src/drivers/mod.rs: -------------------------------------------------------------------------------- 1 | mod tic_tac_toe; 2 | pub use tic_tac_toe::*; 3 | -------------------------------------------------------------------------------- /src/strategy/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod alpha_beta_minimax; 2 | pub mod game_strategy; 3 | -------------------------------------------------------------------------------- /src/games/mod.rs: -------------------------------------------------------------------------------- 1 | mod tic_tac_toe; 2 | pub use tic_tac_toe::TicTacToe; 3 | #[cfg(feature = "chess")] 4 | mod chess; 5 | #[cfg(feature = "chess")] 6 | pub use chess::Chess; 7 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use minimax_alpha_beta::*; 3 | 4 | #[derive(Parser, Debug, Clone)] 5 | #[clap( 6 | author = "Aalekh Patel ", 7 | version = "0.2.0", 8 | about = "Play a game of Tic Tac Toe with a computer opponent that uses the Alpha-Beta Minimax Engine." 9 | )] 10 | pub struct Cli { 11 | /// The size of the board. 12 | #[clap(long, default_value_t = 3)] 13 | pub size: usize, 14 | /// The depth of the search. 15 | #[clap(long, default_value_t = 9)] 16 | pub depth: i64, 17 | } 18 | 19 | fn main() { 20 | let cli = Cli::parse(); 21 | play_tic_tac_toe_against_computer_with_depth(cli.size, cli.depth); 22 | } 23 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Solve any Two-player Minimax game using the 2 | //! Minimax algorithm with Alpha-Beta pruning. 3 | //! Also, where possible, a parallel processing 4 | //! implementation is provided. 5 | 6 | mod drivers; 7 | /// Contains sruct and necessary implementations 8 | /// for `TicTacToe`: a popular two-player game 9 | /// where one player places a symbol - 'X' and another 10 | /// player places a symbol - 'O' on a square grid 11 | /// with the objective of creating a streak of same 12 | /// symbols of length the size of the grid in any direction. 13 | /// 14 | /// # Examples 15 | /// 16 | /// ``` 17 | /// use minimax_alpha_beta::tictactoe::TicTacToe; 18 | /// let mut tic_tac_toe = TicTacToe::create_game(3, None, None, None); 19 | /// tic_tac_toe.print_board(); 20 | /// assert_eq!(tic_tac_toe.size, 3); 21 | /// assert_eq!(tic_tac_toe.default_char, '-'); 22 | /// ``` 23 | // mod tests; 24 | pub mod games; 25 | pub mod strategy; 26 | 27 | pub use drivers::*; 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Aalekh Patel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minimax-alpha-beta" 3 | version = "0.2.0" 4 | authors = ["Aalekh Patel "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "An implementation of Alpha-Beta Pruning + Minimax Algorithm for arbitrary two player minimax style games like Chess, Go, TicTacToe, etc." 8 | homepage = "https://www.github.com/aalekhpatel07/minimax" 9 | documentation = "https://docs.rs/minimax-alpha-beta/" 10 | repository = "https://www.github.com/aalekhpatel07/minimax.git" 11 | readme = "README.md" 12 | keywords = ["game", "game-ai", "Minimax", "alpha-beta-pruning", "efficient-minimax"] 13 | categories = ["algorithms", "mathematics"] 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [features] 18 | default = ["tictactoe"] 19 | tictactoe = [] 20 | chess = ["dep:shakmaty"] 21 | 22 | [dependencies] 23 | shakmaty = { version = "0.21.3", optional = true } 24 | anyhow = { version = "1.0.59" } 25 | clap = { version = "3.2.16", features = ["derive"]} 26 | 27 | [profile.release] 28 | lto = "fat" 29 | debug = false 30 | 31 | [[bin]] 32 | name = "tic-tac-toe" 33 | path = "src/main.rs" -------------------------------------------------------------------------------- /.github/workflows/cargo.yml: -------------------------------------------------------------------------------- 1 | name: Cargo (full) 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | container: 16 | image: aalekhpatel07/rust:1.0 17 | options: --user root --security-opt seccomp=unconfined 18 | steps: 19 | - name: Checkout repository. 20 | uses: actions/checkout@v3 21 | - name: Run tests 22 | run: cargo nextest run --release --verbose 23 | - name: Generate test coverage report. 24 | run: cargo tarpaulin -o Html --output-dir ./target/tarpaulin 25 | - name: Run clippy 26 | run: cargo clippy --no-deps --fix 27 | - name: Build the project. 28 | run: | 29 | cargo build --release 30 | - name: Archive production build. 31 | uses: actions/upload-artifact@v3 32 | with: 33 | name: production-static-files 34 | path: | 35 | target/release/tic-tac-toe 36 | - name: Archive code coverage results. 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: test-coverage-report 40 | path: | 41 | target/tarpaulin 42 | -------------------------------------------------------------------------------- /src/strategy/game_strategy.rs: -------------------------------------------------------------------------------- 1 | /// Any two-player Minimax game must 2 | /// have this behavior. In other words, 3 | /// these functions should yield meaningful outputs 4 | /// for any two-player games. 5 | 6 | pub enum GameResult { 7 | Player1Win, 8 | Player2Win, 9 | Draw, 10 | } 11 | 12 | pub trait GameStrategy { 13 | type Player; 14 | type Move; 15 | type Board; 16 | 17 | /// Ability to statically evaluate the current game state. 18 | fn evaluate(&self) -> f64; 19 | /// Identify a winner, if exists. 20 | fn get_winner(&self) -> Option; 21 | /// Identify if the game is tied. 22 | fn is_game_tied(&self) -> bool; 23 | /// Identify if the game is in a completed state. 24 | fn is_game_complete(&self) -> bool; 25 | /// Ability to produce a collection of playable legal moves 26 | /// in the current position. 27 | fn get_available_moves(&self) -> Vec; 28 | /// Modify the game state by playing a given move. 29 | fn play(&mut self, mv: &Self::Move, maximizer: bool); 30 | /// Modify the game state by resetting a given move. 31 | fn clear(&mut self, mv: &Self::Move); 32 | /// Get the current state of the board. 33 | fn get_board(&self) -> &Self::Board; 34 | /// Determine if a given move is valid. 35 | fn is_a_valid_move(&self, mv: &Self::Move) -> bool; 36 | /// Ability to produce a sentinel (not-playable) move. 37 | fn get_a_sentinel_move(&self) -> Self::Move; 38 | } 39 | -------------------------------------------------------------------------------- /src/drivers/tic_tac_toe.rs: -------------------------------------------------------------------------------- 1 | use crate::games::TicTacToe; 2 | use crate::strategy::alpha_beta_minimax::AlphaBetaMiniMaxStrategy; 3 | use crate::strategy::game_strategy::GameStrategy; 4 | 5 | /// Read input. 6 | fn get_input() -> String { 7 | let mut buffer = String::new(); 8 | std::io::stdin().read_line(&mut buffer).expect("Failed"); 9 | buffer 10 | } 11 | 12 | /// Play a game of any size in a REPL against the engine. 13 | /// The default depth of 6 should make the 14 | /// engine reasonably fast. 15 | pub fn play_tic_tac_toe_against_computer(size: usize) { 16 | play_tic_tac_toe_against_computer_with_depth(size, 6) 17 | } 18 | 19 | /// Play a game of any size in a REPL against the engine. 20 | /// The higher the depth, the longer it takes and 21 | /// the more accurately the engine performs. 22 | pub fn play_tic_tac_toe_against_computer_with_depth(size: usize, depth: i64) { 23 | let mut ttt = TicTacToe::new(size); 24 | loop { 25 | println!("Board:\n{}", ttt); 26 | println!("\n"); 27 | 28 | if ttt.is_game_complete() { 29 | println!("Game is complete."); 30 | if ttt.is_game_tied() { 31 | println!("Game Tied!"); 32 | break; 33 | } else { 34 | println!("{} wins!", ttt.get_winner().unwrap()); 35 | break; 36 | } 37 | } 38 | 39 | let example_num: i64 = 7; 40 | println!( 41 | "Enter a move. (e.g. '{}' represents (row: {}, col: {}) : ", 42 | example_num, 43 | example_num as usize / size, 44 | example_num as usize % size 45 | ); 46 | let s = get_input().trim().parse::(); 47 | let n = s.unwrap_or(usize::MAX); 48 | 49 | if n == usize::MAX { 50 | break; 51 | } 52 | println!( 53 | "Move played by you: {} (i.e. {}, {})", 54 | n, 55 | n / size, 56 | n % size 57 | ); 58 | ttt.play(&n, true); 59 | let move_found = ttt.get_best_move(depth as i64, true); 60 | if move_found > (ttt.size * ttt.size) { 61 | println!("Game is complete."); 62 | if ttt.is_game_tied() { 63 | println!("Game Tied!"); 64 | break; 65 | } else { 66 | println!("{} wins!", ttt.get_winner().unwrap()); 67 | break; 68 | } 69 | } 70 | println!( 71 | "Move played by AI: {} (i.e. {}, {})", 72 | move_found, 73 | move_found / size, 74 | move_found % size 75 | ); 76 | ttt.play(&move_found, false); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/strategy/alpha_beta_minimax.rs: -------------------------------------------------------------------------------- 1 | use crate::strategy::game_strategy::GameStrategy; 2 | 3 | pub const INF: f64 = f64::INFINITY; 4 | pub const NEG_INF: f64 = f64::NEG_INFINITY; 5 | 6 | /// The behaviour required of any 7 | /// minimax game engine. 8 | pub trait AlphaBetaMiniMaxStrategy: GameStrategy { 9 | /// The ability to get the best move 10 | /// in the current state and for the 11 | /// current player. 12 | fn get_best_move( 13 | &mut self, 14 | max_depth: i64, 15 | is_maximizing: bool, 16 | ) -> ::Move; 17 | 18 | /// The ability to produce a best (good enough, sometimes) 19 | /// evaluation score possible over all 20 | /// possible moves at the current game state. 21 | fn minimax_score( 22 | &mut self, 23 | depth: i64, 24 | is_maximizing: bool, 25 | alpha: f64, 26 | beta: f64, 27 | max_depth: i64, 28 | ) -> f64; 29 | } 30 | 31 | /// Endow upon anything the ability to 32 | /// use the AlphaBetaMiniMaxStrategy implementation 33 | /// of the game engine as long as it understands 34 | /// how to behave as Strategy. 35 | impl AlphaBetaMiniMaxStrategy for T { 36 | fn get_best_move( 37 | &mut self, 38 | max_depth: i64, 39 | is_maximizing: bool, 40 | ) -> ::Move { 41 | let mut best_move: ::Move = self.get_a_sentinel_move(); 42 | 43 | if self.is_game_complete() { 44 | return best_move; 45 | } 46 | 47 | let alpha = NEG_INF; 48 | let beta = INF; 49 | 50 | if is_maximizing { 51 | let mut best_move_val: f64 = INF; 52 | 53 | for mv in self.get_available_moves() { 54 | self.play(&mv, !is_maximizing); 55 | let value = self.minimax_score(max_depth, is_maximizing, alpha, beta, max_depth); 56 | self.clear(&mv); 57 | if value <= best_move_val { 58 | best_move_val = value; 59 | best_move = mv; 60 | } 61 | } 62 | 63 | best_move 64 | } else { 65 | let mut best_move_val: f64 = NEG_INF; 66 | 67 | for mv in self.get_available_moves() { 68 | self.play(&mv, !is_maximizing); 69 | let value = self.minimax_score(max_depth, is_maximizing, alpha, beta, max_depth); 70 | self.clear(&mv); 71 | if value >= best_move_val { 72 | best_move_val = value; 73 | best_move = mv; 74 | } 75 | } 76 | best_move 77 | } 78 | } 79 | 80 | fn minimax_score( 81 | &mut self, 82 | depth: i64, 83 | is_maximizing: bool, 84 | mut alpha: f64, 85 | mut beta: f64, 86 | max_depth: i64, 87 | ) -> f64 { 88 | let avail: Vec<::Move> = self.get_available_moves(); 89 | if depth == 0 || self.is_game_complete() || avail.is_empty() { 90 | return self.evaluate(); 91 | } 92 | 93 | if is_maximizing { 94 | let mut value = NEG_INF; 95 | for idx in avail { 96 | self.play(&idx, is_maximizing); 97 | let score = self.minimax_score(depth - 1, !is_maximizing, alpha, beta, max_depth); 98 | 99 | value = value.max(score); 100 | alpha = alpha.max(score); 101 | 102 | self.clear(&idx); 103 | if beta <= alpha { 104 | break; 105 | } 106 | } 107 | if value != 0. { 108 | return value - (max_depth - depth) as f64; 109 | } 110 | value 111 | } else { 112 | let mut value = INF; 113 | for idx in avail { 114 | self.play(&idx, is_maximizing); 115 | let score = self.minimax_score(depth - 1, !is_maximizing, alpha, beta, max_depth); 116 | 117 | value = value.min(score); 118 | beta = beta.min(score); 119 | 120 | self.clear(&idx); 121 | if beta <= alpha { 122 | break; 123 | } 124 | } 125 | 126 | if value != 0. { 127 | return value + (max_depth - depth) as f64; 128 | } 129 | value 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/games/chess.rs: -------------------------------------------------------------------------------- 1 | use crate::strategy::game_strategy::GameStrategy; 2 | use anyhow::{bail, Result}; 3 | use std::ops::{Deref, DerefMut}; 4 | 5 | #[cfg(feature = "chess")] 6 | pub use shakmaty::Chess as ShakmatyChess; 7 | use shakmaty::Position; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Chess { 11 | pub inner: ShakmatyChess, 12 | pub moves_played: shakmaty::MoveList, 13 | } 14 | 15 | impl Default for Chess { 16 | fn default() -> Self { 17 | Self { 18 | inner: ShakmatyChess::default(), 19 | moves_played: shakmaty::MoveList::default(), 20 | } 21 | } 22 | } 23 | 24 | impl Deref for Chess { 25 | type Target = ShakmatyChess; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.inner 29 | } 30 | } 31 | 32 | impl DerefMut for Chess { 33 | fn deref_mut(&mut self) -> &mut Self::Target { 34 | &mut self.inner 35 | } 36 | } 37 | 38 | impl Chess { 39 | pub fn new() -> Self { 40 | Self::default() 41 | } 42 | 43 | fn _undo(&self, _move: shakmaty::Move) -> Result<()> { 44 | todo!("Implement undo for Chess moves."); 45 | } 46 | 47 | pub fn undo(&mut self) -> Result<()> { 48 | if let Some(prev_move) = self.moves_played.pop() { 49 | self._undo(prev_move) 50 | } else { 51 | bail!("No moves to undo."); 52 | } 53 | } 54 | 55 | fn _play(&mut self, _move: shakmaty::Move) { 56 | self.inner.play_unchecked(&_move); 57 | self.moves_played.push(_move); 58 | } 59 | } 60 | 61 | impl GameStrategy for Chess { 62 | type Player = shakmaty::Color; 63 | type Move = Option; 64 | type Board = shakmaty::Board; 65 | 66 | fn get_a_sentinel_move(&self) -> Self::Move { 67 | None 68 | } 69 | 70 | fn is_a_valid_move(&self, mv: &Self::Move) -> bool { 71 | mv.is_some() 72 | } 73 | 74 | fn play(&mut self, mv: &Self::Move, maximizer: bool) { 75 | if let Some(_mv) = mv { 76 | if maximizer { 77 | assert!(self.inner.turn() == shakmaty::Color::White); 78 | // self.inner.play(&_mv); 79 | self._play(_mv.clone()); 80 | self.moves_played.push(_mv.clone()); 81 | } else { 82 | assert!(self.inner.turn() == shakmaty::Color::Black); 83 | // self.inner.play(&mv); 84 | self._play(_mv.clone()); 85 | self.moves_played.push(_mv.clone()); 86 | } 87 | } else { 88 | panic!("Invalid move. Sentinel?"); 89 | } 90 | } 91 | 92 | fn evaluate(&self) -> f64 { 93 | todo!("Implement a static evaluation of a chess position.") 94 | } 95 | 96 | fn clear(&mut self, mv: &Self::Move) { 97 | if mv.is_none() { 98 | panic!("Invalid move. Sentinel?"); 99 | } 100 | let prev_move = self.moves_played.pop(); 101 | 102 | if prev_move.is_none() { 103 | panic!("Invalid move. Sentinel?"); 104 | } 105 | let _mv = prev_move.unwrap(); 106 | self._undo(_mv.clone()) 107 | .expect(&format!("Couldn't undo move: {:#?}", _mv)); 108 | } 109 | 110 | fn get_available_moves(&self) -> Vec { 111 | self.legal_moves() 112 | .iter() 113 | .map(|mv| Some(mv.clone())) 114 | .collect() 115 | } 116 | 117 | fn get_board(&self) -> &Self::Board { 118 | &self.inner.board() 119 | } 120 | fn get_winner(&self) -> Option { 121 | if let Some(outcome) = self.outcome() { 122 | match outcome { 123 | shakmaty::Outcome::Draw => None, 124 | shakmaty::Outcome::Decisive { winner } => Some(winner), 125 | } 126 | } else { 127 | None 128 | } 129 | } 130 | 131 | fn is_game_complete(&self) -> bool { 132 | self.outcome().is_some() 133 | } 134 | 135 | fn is_game_tied(&self) -> bool { 136 | if let Some(outcome) = self.outcome() { 137 | match outcome { 138 | shakmaty::Outcome::Draw => true, 139 | _ => false, 140 | } 141 | } else { 142 | false 143 | } 144 | } 145 | } 146 | 147 | #[cfg(test)] 148 | pub mod tests { 149 | pub use super::Chess; 150 | pub use crate::strategy::game_strategy::GameStrategy; 151 | use shakmaty::{ 152 | CastlingMode, Chess as ChessGame, Color, FromSetup, Piece, Position, Role, Setup, Square, 153 | }; 154 | 155 | #[test] 156 | fn test_chess_new() { 157 | let chess = Chess::new(); 158 | assert_eq!(chess.turn(), shakmaty::Color::White); 159 | } 160 | 161 | #[test] 162 | fn test_chess_evaluate() { 163 | let chess = Chess::new(); 164 | assert_eq!(chess.evaluate(), 0.); 165 | } 166 | 167 | #[test] 168 | fn test_chess_available_moves() { 169 | let chess = Chess::new(); 170 | let moves = chess.get_available_moves(); 171 | assert_eq!(moves.len(), chess.legal_moves().len()); 172 | 173 | println!("{:?}", moves); 174 | } 175 | 176 | #[test] 177 | fn test_chess_available_moves_capture() { 178 | let mut chess_setup = Setup::default(); 179 | 180 | let mut board = chess_setup.board; 181 | board.set_piece_at( 182 | Square::E4, 183 | Piece { 184 | color: Color::White, 185 | role: Role::Pawn, 186 | }, 187 | ); 188 | board.remove_piece_at(Square::E2).unwrap(); 189 | board.remove_piece_at(Square::D7).unwrap(); 190 | board.set_piece_at( 191 | Square::D5, 192 | Piece { 193 | color: Color::Black, 194 | role: Role::Pawn, 195 | }, 196 | ); 197 | 198 | chess_setup.board = board; 199 | 200 | let chess = ChessGame::from_setup(chess_setup, CastlingMode::Standard).unwrap(); 201 | 202 | let moves = chess.capture_moves(); 203 | assert_eq!(moves.len(), 1); 204 | // println!("{moves:#?}"); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Implementation for a minimax game engine. 2 | 3 | ## Background 4 | 5 | A Minimax game is any turn-based two player game where the objective of 6 | one player is to maximize the evaluation score while the opponent 7 | tries to minimize it. Examples of such games include Chess, Go, TicTacToe, Connect Four, etc. 8 | 9 | In this crate, there is a generic implementation of the minimax game-playing engine that can be used for any such games. The minimax algorithm is notorious for being slow so we speed it up using a pruning method called Alpha-Beta pruning. 10 | 11 | The minimax algorithm under-the-hood tries to simulate every possible "best" gameplays from either sides and based on the results, recommends a move that should be played to best improve the winning chances of the player to move. 12 | 13 | There is a caveat, in that, the minimax algorithm is too slow (since it explores the search space almost uncleverly). One optimization is that of pruning the search space by the means of a clever approach in which we "rule out gameplay sequences which the opponent won't definitely let us improve in." This is achieved by a technique called Alpha-Beta pruning. 14 | 15 | ## Usage 16 | 17 | ### As a binary 18 | 19 | Install with: 20 | ```sh 21 | cargo install minimax-alpha-beta 22 | ``` 23 | 24 | Get usage instructions with: 25 | ```sh 26 | tic-tac-toe --help 27 | ``` 28 | ``` 29 | minimax-alpha-beta 0.2.0 30 | Aalekh Patel 31 | Play a game of Tic Tac Toe with a computer opponent that uses the Alpha-Beta Minimax Engine. 32 | 33 | USAGE: 34 | tic-tac-toe [OPTIONS] 35 | 36 | OPTIONS: 37 | --depth The depth of the search [default: 9] 38 | -h, --help Print help information 39 | --size The size of the board [default: 3] 40 | -V, --version Print version information 41 | ``` 42 | 43 | For example, to play a regular 3x3 tic-tac-toe game just use `tic-tac-toe`. 44 | 45 | Otherwise to play a 6x6 tic-tac-toe game with search depth of 5, use 46 | ```sh 47 | tic-tac-toe --depth 5 --size 6 48 | ``` 49 | 50 | ### As a library 51 | 52 | The crate provides concrete implementations for TicTacToe, (note: other games are in works). 53 | 54 | Use the `TicTacToe::get_best_move(depth, player)` method to compute the best move in this position for this player. 55 | 56 | ### To use a pre-written driver: 57 | 58 | ```rust 59 | 60 | use minimax_alpha_beta::*; 61 | 62 | fn main() { 63 | let grid_size: usize = 3; 64 | let search_depth: i64 = 6; 65 | 66 | // Control the depth to trade running time and accuracy. 67 | // The higher the depth the slower the compute and higher the accuracy. 68 | // The lower the depth the faster the compute and lower the accuracy. 69 | drivers::play_tic_tac_toe_against_computer_with_depth(grid_size, search_depth); 70 | 71 | // Or simply use the default balance of `depth = 6`. 72 | drivers::play_tic_tac_toe_against_computer(grid_size); 73 | } 74 | ``` 75 | 76 | ### To write a driver yourself: 77 | 78 | ```rust 79 | use minimax_alpha_beta::games::tic_tac_toe::TicTacToe; 80 | use minimax_alpha_beta::strategy::{game_strategy::GameStrategy, alpha_beta_minimax::AlphaBetaMinimaxStrategy}; 81 | 82 | let mut ttt = TicTacToe::new(); 83 | ttt.print_board(); 84 | 85 | // The first argument takes a reference to the move position. 86 | // The structure of the board is like [[0, 1, 2], [3, 4, 5], [6, 7, 8]]. 87 | // The second argument governs who plays this move; 88 | // true means the first player, false means the second. 89 | 90 | ttt.play(&4, true); 91 | ttt.play(&0, false); 92 | 93 | ttt.print_board(); 94 | 95 | // The first argument is the depth to explore. 96 | // The higher the depth, the more the time it takes to compute 97 | // the best move and that the chances of it being the best move increase. 98 | // The lower the depth, the faster it takes to compute but the less 99 | // the likelihood of being the best move. 100 | 101 | // The second argument governs who plays this move; 102 | // true means the first player, false means the second. 103 | let best = ttt.get_best_move(6 as i64, true); 104 | 105 | ttt.play(&best, true); 106 | 107 | ttt.print_board(); 108 | 109 | ``` 110 | 111 | ### To use the engine for a completely new minimax game (e.g. chess): 112 | 113 | ```rust 114 | 115 | use minimax_alpha_beta::strategy::{game_strategy::GameStrategy, alpha_beta_minimax::AlphaBetaMiniMaxStrategy}; 116 | 117 | /// Define the Chess structure. 118 | pub struct Chess { 119 | /// The board could be represented by a vector of 64 characters. 120 | pub board: Vec, 121 | /// The default char could be a '.' 122 | pub default_char: char, 123 | /// The maximizer is White. 124 | pub maximizer: char, 125 | /// The minimizer is Black. 126 | pub minimizer: char, 127 | } 128 | 129 | // Implement any basic methods on Chess. 130 | // This should ideally allow us to work with 131 | // Chess at a very low level. 132 | 133 | impl Chess { 134 | // ... 135 | } 136 | 137 | // You'll likely need to have a new struct 138 | // that represents a move in Chess. 139 | pub struct ChessMove { 140 | // ... 141 | } 142 | 143 | // Implement all the higher level 144 | // methods on Chess. This should ideally 145 | // be compositions of the basic methods 146 | // in the default impl of Chess. 147 | 148 | impl Strategy for Chess { 149 | // ... 150 | type Move = ChessMove; 151 | // ... 152 | } 153 | 154 | // Once Strategy is implemented for Chess, the Minimax engine should be ready to use! 155 | 156 | // Make sure there is a create_game in the default impl for Chess. 157 | // Then create a game with parameters as necessary. 158 | let mut chessboard = Chess::create_game() 159 | 160 | let search_depth: i64 = 6; 161 | 162 | // Play arbitrary number of moves depending on how've you defined it in the default impl. 163 | chessboard.play(&your_move, true); 164 | 165 | let best_move: ChessMove = chessboard.get_best_move(search_depth, true); 166 | 167 | chessboard.play(&best_move, true); 168 | chessboard.print_board(); 169 | ``` 170 | 171 | ## Show appreciation 172 | 173 | I enjoyed creating this and is one of my first excursions in Rust. 174 | If you found this library useful or maybe just cool, consider [awarding a star](https://www.github.com/aalekhpatel07/minimax). 175 | 176 | ## Questions 177 | 178 | Please [create an issue](https://www.github.com/aalekhpatel07/minimax/issues). 179 | 180 | ## Contribution 181 | 182 | All [pull requests](https://www.github.com/aalekhpatel07/minimax/pull) are welcome! 183 | 184 | 185 | ## Contact 186 | 187 | Got any cool collaboration ideas? My github is [aalekhpatel07](https://www.github.com/aalekhpatel07) and you can reach me [here](mailto:itsme@aalekhpatel.com). 188 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.59" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "c91f1f46651137be86f3a2b9a8359f9ab421d04d941c62b5982e1ca21113adf9" 10 | 11 | [[package]] 12 | name = "arrayvec" 13 | version = "0.7.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" 16 | 17 | [[package]] 18 | name = "atty" 19 | version = "0.2.14" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 22 | dependencies = [ 23 | "hermit-abi", 24 | "libc", 25 | "winapi", 26 | ] 27 | 28 | [[package]] 29 | name = "autocfg" 30 | version = "1.1.0" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 33 | 34 | [[package]] 35 | name = "bitflags" 36 | version = "1.3.2" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 39 | 40 | [[package]] 41 | name = "btoi" 42 | version = "0.4.2" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "97c0869a9faa81f8bbf8102371105d6d0a7b79167a04c340b04ab16892246a11" 45 | dependencies = [ 46 | "num-traits", 47 | ] 48 | 49 | [[package]] 50 | name = "clap" 51 | version = "3.2.16" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" 54 | dependencies = [ 55 | "atty", 56 | "bitflags", 57 | "clap_derive", 58 | "clap_lex", 59 | "indexmap", 60 | "once_cell", 61 | "strsim", 62 | "termcolor", 63 | "textwrap", 64 | ] 65 | 66 | [[package]] 67 | name = "clap_derive" 68 | version = "3.2.15" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4" 71 | dependencies = [ 72 | "heck", 73 | "proc-macro-error", 74 | "proc-macro2", 75 | "quote", 76 | "syn", 77 | ] 78 | 79 | [[package]] 80 | name = "clap_lex" 81 | version = "0.2.4" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 84 | dependencies = [ 85 | "os_str_bytes", 86 | ] 87 | 88 | [[package]] 89 | name = "hashbrown" 90 | version = "0.12.3" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 93 | 94 | [[package]] 95 | name = "heck" 96 | version = "0.4.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 99 | 100 | [[package]] 101 | name = "hermit-abi" 102 | version = "0.1.19" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 105 | dependencies = [ 106 | "libc", 107 | ] 108 | 109 | [[package]] 110 | name = "indexmap" 111 | version = "1.9.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 114 | dependencies = [ 115 | "autocfg", 116 | "hashbrown", 117 | ] 118 | 119 | [[package]] 120 | name = "libc" 121 | version = "0.2.126" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 124 | 125 | [[package]] 126 | name = "minimax-alpha-beta" 127 | version = "0.2.0" 128 | dependencies = [ 129 | "anyhow", 130 | "clap", 131 | "shakmaty", 132 | ] 133 | 134 | [[package]] 135 | name = "num-traits" 136 | version = "0.2.15" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 139 | dependencies = [ 140 | "autocfg", 141 | ] 142 | 143 | [[package]] 144 | name = "once_cell" 145 | version = "1.13.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 148 | 149 | [[package]] 150 | name = "os_str_bytes" 151 | version = "6.2.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" 154 | 155 | [[package]] 156 | name = "proc-macro-error" 157 | version = "1.0.4" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 160 | dependencies = [ 161 | "proc-macro-error-attr", 162 | "proc-macro2", 163 | "quote", 164 | "syn", 165 | "version_check", 166 | ] 167 | 168 | [[package]] 169 | name = "proc-macro-error-attr" 170 | version = "1.0.4" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 173 | dependencies = [ 174 | "proc-macro2", 175 | "quote", 176 | "version_check", 177 | ] 178 | 179 | [[package]] 180 | name = "proc-macro2" 181 | version = "1.0.42" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" 184 | dependencies = [ 185 | "unicode-ident", 186 | ] 187 | 188 | [[package]] 189 | name = "quote" 190 | version = "1.0.20" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 193 | dependencies = [ 194 | "proc-macro2", 195 | ] 196 | 197 | [[package]] 198 | name = "shakmaty" 199 | version = "0.21.3" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "1f4e6c06e44c662e0f737af31b8860407a2028163bf5aa351cddc3136ca721e8" 202 | dependencies = [ 203 | "arrayvec", 204 | "bitflags", 205 | "btoi", 206 | ] 207 | 208 | [[package]] 209 | name = "strsim" 210 | version = "0.10.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 213 | 214 | [[package]] 215 | name = "syn" 216 | version = "1.0.98" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 219 | dependencies = [ 220 | "proc-macro2", 221 | "quote", 222 | "unicode-ident", 223 | ] 224 | 225 | [[package]] 226 | name = "termcolor" 227 | version = "1.1.3" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 230 | dependencies = [ 231 | "winapi-util", 232 | ] 233 | 234 | [[package]] 235 | name = "textwrap" 236 | version = "0.15.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 239 | 240 | [[package]] 241 | name = "unicode-ident" 242 | version = "1.0.2" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" 245 | 246 | [[package]] 247 | name = "version_check" 248 | version = "0.9.4" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 251 | 252 | [[package]] 253 | name = "winapi" 254 | version = "0.3.9" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 257 | dependencies = [ 258 | "winapi-i686-pc-windows-gnu", 259 | "winapi-x86_64-pc-windows-gnu", 260 | ] 261 | 262 | [[package]] 263 | name = "winapi-i686-pc-windows-gnu" 264 | version = "0.4.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 267 | 268 | [[package]] 269 | name = "winapi-util" 270 | version = "0.1.5" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 273 | dependencies = [ 274 | "winapi", 275 | ] 276 | 277 | [[package]] 278 | name = "winapi-x86_64-pc-windows-gnu" 279 | version = "0.4.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 282 | -------------------------------------------------------------------------------- /src/games/tic_tac_toe.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::strategy::game_strategy::GameStrategy; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct TicTacToe { 7 | pub board: Vec, 8 | pub size: usize, 9 | pub default_char: char, 10 | pub maximizer: char, 11 | pub minimizer: char, 12 | } 13 | 14 | impl Display for TicTacToe { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | for idx in 0..self.size { 17 | let start = self.size * idx; 18 | let end = self.size * (idx + 1); 19 | let sub: &[char] = &self.board[start as usize..end as usize]; 20 | 21 | for &x in sub.iter() { 22 | write!(f, "{}", x)?; 23 | } 24 | writeln!(f)?; 25 | } 26 | Ok(()) 27 | } 28 | } 29 | 30 | impl Default for TicTacToe { 31 | fn default() -> Self { 32 | TicTacToe::new(3) 33 | } 34 | } 35 | 36 | /// Implements all necessary 37 | /// methods to operate a TicTacToe 38 | /// game. 39 | impl TicTacToe { 40 | pub fn new(size: usize) -> Self { 41 | let board: Vec = vec!['-'; (size * size) as usize]; 42 | Self { 43 | board, 44 | size, 45 | default_char: '-', 46 | maximizer: 'o', 47 | minimizer: 'x', 48 | } 49 | } 50 | 51 | pub fn with_player_1(self, character: char) -> Self { 52 | Self { 53 | maximizer: character, 54 | ..self 55 | } 56 | } 57 | pub fn with_player_2(self, character: char) -> Self { 58 | Self { 59 | minimizer: character, 60 | ..self 61 | } 62 | } 63 | pub fn with_default_char(self, character: char) -> Self { 64 | Self { 65 | default_char: character, 66 | ..self 67 | } 68 | } 69 | 70 | /// Check the main and anti-diagonals 71 | /// for a winner. 72 | pub fn check_diagonals(&self) -> char { 73 | let mut winner = self.default_char; 74 | if self.check_diagonal(self.maximizer, true) || self.check_diagonal(self.maximizer, false) { 75 | winner = self.maximizer 76 | } else if self.check_diagonal(self.minimizer, true) 77 | || self.check_diagonal(self.minimizer, false) 78 | { 79 | winner = self.minimizer 80 | } 81 | winner 82 | } 83 | 84 | /// Check the rows of the grid for a winner. 85 | pub fn check_rows(&self) -> char { 86 | let mut winner = self.default_char; 87 | 88 | for row in 0..self.size as usize { 89 | if self.check_row(self.maximizer, row) { 90 | winner = self.maximizer; 91 | break; 92 | } else if self.check_row(self.minimizer, row) { 93 | winner = self.minimizer; 94 | break; 95 | } 96 | } 97 | winner 98 | } 99 | 100 | /// Check the columns of the grid for a winner. 101 | pub fn check_cols(&self) -> char { 102 | let mut winner = self.default_char; 103 | 104 | for col in 0..self.size as usize { 105 | if self.check_col(self.maximizer, col) { 106 | winner = self.maximizer; 107 | break; 108 | } else if self.check_col(self.minimizer, col) { 109 | winner = self.minimizer; 110 | break; 111 | } 112 | } 113 | winner 114 | } 115 | 116 | /// Check a given column if a given player has won. 117 | fn check_col(&self, ch: char, col_num: usize) -> bool { 118 | for row in 0..self.size as usize { 119 | if self.board[self.size as usize * row + col_num] != ch { 120 | return false; 121 | } 122 | } 123 | true 124 | } 125 | 126 | /// Check a given row if a given player has won. 127 | fn check_row(&self, ch: char, row_num: usize) -> bool { 128 | for col in 0..self.size as usize { 129 | if self.board[self.size as usize * row_num + col] != ch { 130 | return false; 131 | } 132 | } 133 | true 134 | } 135 | 136 | /// Check the main and anti diagonals if a 137 | /// given player has won. 138 | fn check_diagonal(&self, ch: char, diag: bool) -> bool { 139 | // main diagonal is represented by true. 140 | if diag { 141 | for idx in 0..self.size as usize { 142 | if self.board[(self.size as usize * idx as usize) + idx] != ch { 143 | return false; 144 | } 145 | } 146 | true 147 | } else { 148 | for idx in 0..self.size as usize { 149 | if self.board[(self.size as usize * (self.size as usize - 1 - idx as usize)) + idx] 150 | != ch 151 | { 152 | return false; 153 | } 154 | } 155 | true 156 | } 157 | } 158 | } 159 | 160 | /// Endow upon TicTacToe the ability to 161 | /// play games. 162 | impl GameStrategy for TicTacToe { 163 | /// The Player is a char. 164 | /// Usually one of 'o', 'O', 'x', 'X', '-'. 165 | type Player = char; 166 | 167 | /// The Move is a single number representing an 168 | /// index of the Board vector, i.e. in range 169 | /// `[0, (size * size) - 1]`. 170 | type Move = usize; 171 | 172 | /// The Board is a single vector of length `size * size`. 173 | type Board = Vec; 174 | 175 | fn evaluate(&self) -> f64 { 176 | if self.is_game_tied() { 177 | 0. 178 | } else { 179 | let _winner = self.get_winner().unwrap(); 180 | if _winner == self.maximizer { 181 | 1000. 182 | } else { 183 | -1000. 184 | } 185 | } 186 | } 187 | 188 | fn get_winner(&self) -> Option { 189 | let mut winner = self.check_diagonals(); 190 | 191 | if winner == self.default_char { 192 | winner = self.check_rows(); 193 | } 194 | if winner == self.default_char { 195 | winner = self.check_cols(); 196 | } 197 | Some(winner) 198 | } 199 | 200 | fn is_game_tied(&self) -> bool { 201 | let _winner = self.get_winner().unwrap(); 202 | 203 | _winner == self.default_char && self.get_available_moves().is_empty() 204 | } 205 | 206 | fn is_game_complete(&self) -> bool { 207 | let _winner = self.get_winner(); 208 | 209 | self.get_available_moves().is_empty() || _winner.unwrap() != '-' 210 | } 211 | 212 | fn get_available_moves(&self) -> Vec { 213 | let mut moves: Vec = vec![]; 214 | for idx in 0..(self.size * self.size) as usize { 215 | if self.board[idx] == '-' { 216 | moves.push(idx) 217 | } 218 | } 219 | moves 220 | } 221 | 222 | fn play(&mut self, &mv: &Self::Move, maximizer: bool) { 223 | // player: true means the maximizer's turn. 224 | 225 | if maximizer { 226 | self.board[mv] = self.maximizer; 227 | } else { 228 | self.board[mv] = self.minimizer; 229 | } 230 | } 231 | 232 | fn clear(&mut self, &mv: &Self::Move) { 233 | self.board[mv] = self.default_char 234 | } 235 | 236 | fn get_board(&self) -> &Self::Board { 237 | &self.board 238 | } 239 | 240 | fn is_a_valid_move(&self, &mv: &Self::Move) -> bool { 241 | self.board[mv] == self.default_char 242 | } 243 | 244 | fn get_a_sentinel_move(&self) -> Self::Move { 245 | self.size * self.size + 1 246 | } 247 | } 248 | 249 | #[cfg(test)] 250 | mod tests { 251 | use super::*; 252 | use crate::strategy::alpha_beta_minimax::AlphaBetaMiniMaxStrategy; 253 | 254 | #[test] 255 | fn best_move_in_given_3_by_3() { 256 | let mut ttt = TicTacToe::new(3) 257 | .with_player_1('o') 258 | .with_player_2('x') 259 | .with_default_char('-'); 260 | 261 | ttt.play(&8, true); 262 | ttt.play(&7, false); 263 | ttt.play(&5, true); 264 | 265 | assert_eq!(ttt.get_best_move(9, false), 2); 266 | } 267 | 268 | #[test] 269 | fn test_should_always_tie_a_3_by_3_after_9_moves_at_depth_9() { 270 | let mut ttt = TicTacToe::new(3); 271 | for move_number in 0..=8 { 272 | let is_maximising = move_number % 2 == 0; 273 | let i = ttt.get_best_move(9, is_maximising); 274 | ttt.play(&i, is_maximising); 275 | println!("{}", ttt); 276 | // ttt.print_board(); 277 | } 278 | assert!(ttt.is_game_complete()); 279 | assert!(ttt.is_game_tied()); 280 | } 281 | } 282 | --------------------------------------------------------------------------------