diff --git a/register-machines/Cargo.toml b/register-machines/Cargo.toml index b537379..9c26d17 100644 --- a/register-machines/Cargo.toml +++ b/register-machines/Cargo.toml @@ -10,5 +10,4 @@ name = "reference" path = "src/reference_solution.rs" [dependencies] -num-bigint = "0.4.3" -num-traits = "0.2.14" \ No newline at end of file +rug = "1.14.0" \ No newline at end of file diff --git a/register-machines/src/main.rs b/register-machines/src/main.rs index ee82b77..9300dd9 100644 --- a/register-machines/src/main.rs +++ b/register-machines/src/main.rs @@ -1,8 +1,10 @@ -use std::collections::HashMap; +use std::{collections::HashMap, vec}; +use rug::Integer; +type Godel = Integer; type Label = usize; type Register = u128; -type State = (Label, HashMap); +type State = (Label, HashMap); #[derive(Clone, Copy, Debug, PartialEq)] enum Instruction { @@ -13,35 +15,97 @@ enum Instruction { use Instruction::*; fn eval_program(program: &[Instruction], state: &State) -> State { - unimplemented!(); + let (mut l, mut rs) = state.clone(); + while l < program.len() { + match program[l] { + Add(i, j) => { + *rs.entry(i).or_insert(Godel::new()) += 1; + l = j; + }, + Sub(i, j, k) => { + let v = rs.entry(i).or_insert(Godel::new()); + if *v != 0 { + *v -= 1; + l = j; + } else { + l = k; + } + }, + Halt => { break; } + } + } + (l, rs) +} +fn encode_instruction(ins: &Instruction) -> Godel { + match ins { + Add(i, j) => + encode_pair1(&Godel::from(2 * i), &Godel::from(*j)), + Sub(i, j, k) => + encode_pair1(&Godel::from(2 * i + 1), + &encode_pair2(&Godel::from(*j), &Godel::from(*k)) + ), + Halt => Godel::ZERO + } } // <> = (2^x)*(2y+1) -fn encode_pair1(x: u128, y: u128) -> u128 { - unimplemented!(); +fn encode_pair1(x: &Godel, y: &Godel) -> Godel { + (Godel::from(2) * y + 1) << x.to_u32_wrapping() } // = (2^x)*(2y+1)-1 -fn encode_pair2(x: u128, y: u128) -> u128 { - unimplemented!(); +fn encode_pair2(x: &Godel, y: &Godel) -> Godel { + encode_pair1(x, y) - 1 } -fn encode_list_to_godel(l: &[u128]) -> u128 { - unimplemented!(); +fn encode_list_to_godel(l: &[Godel]) -> Godel { + if l.is_empty() { return Godel::ZERO; } + encode_pair1(&l[0], &encode_list_to_godel(&l[1..])) } -fn encode_program_to_list(program: &[Instruction]) -> Vec { - unimplemented!(); +fn encode_program_to_list(program: &[Instruction]) -> Vec { + program.iter().map(|x| encode_instruction(x)).collect() +} +// Returns 0 if there are no trailing zeros, else return trailing zero count (in binary) +fn trailing_zeros_in_binary(x: &Godel) -> Godel { + let mut b = x.clone(); + let mut c = Godel::new(); + if b != 0 { + b = (b.clone() ^ (b - 1)) >> 1; + while b != 0 { + c += 1; + b >>= 1; + } + } + c +} +fn decode_instruction(ins: &Godel) -> Instruction { + if *ins == 0 { return Halt; } + let (x, y) = decode_pair1(ins); + let i: Godel = x.clone() / 2; + if x % 2 != 0 { + let (j, k) = decode_pair2(&y); + Sub(i.try_into().unwrap(), j.try_into().unwrap(), k.try_into().unwrap()) + } else { + Add(i.try_into().unwrap(), y.try_into().unwrap()) + } } // a = (2^x)*(2y+1) -fn decode_pair1(a: u128) -> (u128, u128) { - unimplemented!(); +fn decode_pair1(a: &Godel) -> (Godel, Godel) { + let x: Godel = trailing_zeros_in_binary(a); + let z = Godel::from(a >> x.to_u32_wrapping()); + let y: Godel = (z - 1) / 2; + (x, y) } // a = (2^x)*(2y+1)-1 -fn decode_pair2(a: u128) -> (u128, u128) { - unimplemented!(); +fn decode_pair2(a: &Godel) -> (Godel, Godel) { + decode_pair1(&Godel::from(a + 1)) } -fn decode_godel_to_list(g: u128) -> Vec { - unimplemented!(); +fn decode_godel_to_list(g: &Godel) -> Vec { + if *g == 0 { return Vec::new(); } + let (x, xs) = decode_pair1(g); + let mut gs = vec![x]; + gs.splice(gs.len().., decode_godel_to_list(&xs)); + gs } -fn decode_list_to_program(program: &[u128]) -> Vec { - unimplemented!(); +fn decode_list_to_program(program: &[Godel]) -> Vec { + program.iter().map(|x| decode_instruction(x)).collect() } fn main() { @@ -49,30 +113,67 @@ fn main() { mod test { use crate::*; + #[test] + fn halt_encodes_to_godel_zero_num() { + let g = encode_instruction(&Halt); + assert_eq!(g, Godel::ZERO); + } + + #[test] + fn godel_zero_num_decodes_to_halt() { + let ins = decode_instruction(&Godel::ZERO); + assert_eq!(ins, Halt); + } + #[test] fn godel_num_to_godel_list() { - let n = 2u128.pow(46) * 20483; - let godel_list = decode_godel_to_list(n); - let true_godel_list = vec![46, 0, 10, 1]; + let n = Godel::from(20483) << 46; + let godel_list: Vec = decode_godel_to_list(&n); + let true_godel_list: Vec = vec![46, 0, 10, 1].iter().map(|x| Godel::from(*x)).collect(); assert_eq!(godel_list, true_godel_list) } + #[test] + fn godel_num_to_godel_list_large_num() { + let n = Godel::from(833) << 216; + let godel_list: Vec = decode_godel_to_list(&n); + let true_godel_list: Vec = vec![216, 5, 1, 0].iter().map(|x| Godel::from(*x)).collect(); + assert_eq!(godel_list, true_godel_list) + } + #[test] fn godel_list_to_godel_num() { - let godel_num = encode_list_to_godel(&[46, 0, 10, 1]); + let true_godel_list: Vec = [46, 0, 10, 1].iter().map(|x| Godel::from(*x)).collect(); + let godel_num: Godel = encode_list_to_godel(&true_godel_list); assert_eq!(godel_num, 2u128.pow(46) * 20483) } #[test] fn godel_list_to_program() { - let program = decode_list_to_program(&vec![46, 0, 10, 1]); + let true_godel_list: Vec = [46, 0, 10, 1].iter().map(|x| Godel::from(*x)).collect(); + let program = decode_list_to_program(&true_godel_list); assert_eq!(program, vec![Sub(0, 2, 1), Halt, Sub(0, 0, 1), Add(0, 0)]) } + #[test] + fn godel_list_to_program_2() { + let true_godel_list: Vec = [408, 2272, 7, 192, 8064, 144, 0].iter().map(|x| Godel::from(*x)).collect(); + let program = decode_list_to_program(&true_godel_list); + assert_eq!(program, vec![Sub(1, 1, 6), Sub(2, 2, 4), Add(0, 3), Add(3, 1), Sub(3, 5, 0), Add(2, 4), Halt]) + } + #[test] fn program_to_godel_list() { - let program = encode_program_to_list(&[Sub(0, 2, 1), Halt, Sub(0, 0, 1), Add(0, 0)]); - assert_eq!(program, [46, 0, 10, 1]) + let program: Vec = encode_program_to_list(&[Sub(0, 2, 1), Halt, Sub(0, 0, 1), Add(0, 0)]); + let true_godel_list: Vec = [46, 0, 10, 1].iter().map(|x| Godel::from(*x)).collect(); + assert_eq!(program, true_godel_list) + } + + #[test] + fn program_to_godel_list_2() { + let program: Vec = encode_program_to_list(&[Sub(1, 1, 6), Sub(2, 2, 4), Add(0, 3), Add(3, 1), Sub(3, 5, 0), Add(2, 4), Halt]); + let true_godel_list: Vec = [408, 2272, 7, 192, 8064, 144, 0].iter().map(|x| Godel::from(*x)).collect(); + assert_eq!(program, true_godel_list) } #[test] @@ -90,14 +191,14 @@ mod test { &program, &( 0, - HashMap::<_, _>::from_iter(IntoIter::new([(0, 0), (1, 7)])) + HashMap::<_, _>::from_iter(IntoIter::new([(0, Godel::from(0)), (1, Godel::from(7))])) ), ); assert_eq!( final_state, ( 4, - HashMap::<_, _>::from_iter(IntoIter::new([(0, 2), (1, 0)])) + HashMap::<_, _>::from_iter(IntoIter::new([(0, Godel::from(2)), (1, Godel::from(0))])) ) ) } diff --git a/tictactoe/Cargo.toml b/tictactoe/Cargo.toml index 0b0eb68..b9b1487 100644 --- a/tictactoe/Cargo.toml +++ b/tictactoe/Cargo.toml @@ -5,4 +5,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] \ No newline at end of file +[dependencies] +strum = { version = "0.23", features = ["derive"] } +itertools = "0.10.2" +colored = "2" +rand = "0.8.0" \ No newline at end of file diff --git a/tictactoe/src/main.rs b/tictactoe/src/main.rs index 26b0786..fdbfb7b 100644 --- a/tictactoe/src/main.rs +++ b/tictactoe/src/main.rs @@ -1,6 +1,15 @@ -use std::fmt; - -#[derive(PartialEq, Copy, Clone, Debug)] +use colored::*; +use itertools::Itertools; +use rand::Rng; +use std::{ + cmp, + collections::{HashMap, HashSet}, + fmt, + io::Write, +}; +use strum::{EnumCount, EnumIter, IntoEnumIterator}; + +#[derive(PartialEq, Copy, Clone, Debug, EnumIter, EnumCount, Eq, Hash)] enum Player { Player1, Player2, @@ -30,64 +39,530 @@ enum GameResult { enum GameError { NotLegalPosition, PositionOccupied, + MoveNotFound, } trait Game { - type Move; + type Move: Clone; + type Hash; fn moves(&self) -> Vec; fn winner(&self) -> Option; fn execute_move(&mut self, player_move: Self::Move) -> Result<(), GameError>; fn undo_move(&mut self, player_move: Self::Move) -> Result<(), GameError>; } -#[derive(PartialEq, Copy, Clone, Debug)] +#[derive(Clone, Debug)] struct MNKBoard { - board: [[Option; N]; M], - current_player: Player + ply: usize, + board: [[Option; N]; M], + move_sequence: Vec< as Game>::Move>, + current_player: Player, + hash: u64, + transposition: MNKTranspositionTable, } impl MNKBoard { fn new() -> Self { MNKBoard { - board: [[None; N]; M], - current_player: Player::Player1 + ply: 0, + board: [[None; N]; M], + move_sequence: vec![], + current_player: Player::Player1, + hash: 0, + transposition: MNKTranspositionTable::new(), + } + } + + fn load_board(board: [[Option; N]; M], current_player: Player) -> Self { + let hash_gen = MNKZobrist::::new(); + let mut hash = 0; + let mut ply = 0; + + for (r, row) in board.iter().enumerate() { + for (c, cell) in row.iter().enumerate() { + if let Some(p) = cell { + hash ^= hash_gen.get_hash(*p, r, c); + ply += 1; + } + } + } + + MNKBoard { + ply, + board, + move_sequence: vec![], + current_player, + hash, + transposition: MNKTranspositionTable::new(), + } + } + + fn iter2d(&self) -> impl Iterator)> { + self.board.iter().enumerate().flat_map(|(r, row_cells)| { + row_cells + .iter() + .enumerate() + .map(move |(c, cell)| ((r, c), cell)) + }) + } + + // Check if moves supplied contain at least one K-in-a-row combination for the specified player + // Time complexity: O(L), where L = moves.len() + fn has_win_combination(mut moves: Vec<&Option>, player: Player) -> bool { + if moves.len() < K { + return false; + } + + let mut count: HashMap, usize> = HashMap::new(); + let mut to_remove: usize = 0; + let remainder = moves.split_off(K); + + for &&m in &moves { + *count.entry(m).or_insert(0) += 1; + } + + if *count.entry(Some(player)).or_insert(0) == K { + return true; + } + + for &&m in &remainder { + *count.entry(*moves[to_remove]).or_insert(0) -= 1; + *count.entry(m).or_insert(0) += 1; + to_remove += 1; + if *count.entry(Some(player)).or_insert(0) == K { + return true; + } + } + + false + } + + // Check if current move results in the win of the current player + // Time complexity: O(K) + fn check_current_move(&self) -> Option { + let current_move = self.move_sequence.last(); + current_move?; + + let &(r, c) = current_move.unwrap(); + let (r32, c32) = (r as i32, c as i32); + let (m32, n32, k32) = (M as i32, N as i32, K as i32); + let player = self.board[r][c].unwrap(); + + // Initialise horizontal and vertical ranges + let horizontal = ((c32 - k32 + 1)..(c32 + k32)) + .filter(|&col| col >= 0 && col < n32) + .map(|v| v as usize); + + let vertical = ((r32 - k32 + 1)..(r32 + k32)) + .filter(|&row| row >= 0 && row < m32) + .map(|v| v as usize); + + // Check horizontal moves + let horizontal_moves: Vec<_> = horizontal.clone().map(|col| &self.board[r][col]).collect(); + if Self::has_win_combination(horizontal_moves, player) { + return Some(GameResult::PlayerWon(player)); + } + + // Check vertical moves + let vertical_moves: Vec<_> = vertical.clone().map(|row| &self.board[row][c]).collect(); + if Self::has_win_combination(vertical_moves, player) { + return Some(GameResult::PlayerWon(player)); + } + + // Check main diagonal moves + let main_diag_moves: Vec<_> = horizontal + .clone() + .zip(vertical.clone()) + .map(|(row, col)| &self.board[row][col]) + .collect(); + if Self::has_win_combination(main_diag_moves, player) { + return Some(GameResult::PlayerWon(player)); + } + + // Check anti diagonal moves + let anti_diag_moves: Vec<_> = horizontal + .zip(vertical.rev()) + .map(|(row, col)| &self.board[row][col]) + .collect(); + if Self::has_win_combination(anti_diag_moves, player) { + return Some(GameResult::PlayerWon(player)); + } + + if self.ply == M * N { + Some(GameResult::Draw) + } else { + None + } + } + + // Inspect the entire board to find a winner + // Usage: when a non-trivial board state is preloaded with no move sequence provided + // Time complexity: O(M * N) + fn check_board(&self) -> Option { + // Check horizontal moves + for horizontal_moves in self.board { + for player in Player::iter() { + if Self::has_win_combination(horizontal_moves.iter().collect(), player) { + return Some(GameResult::PlayerWon(player)); + } + } + } + + // Check vertical moves + for col in 0..N { + let vertical_moves: Vec<_> = (0..M).map(|row| &self.board[row][col]).collect(); + for player in Player::iter() { + if Self::has_win_combination(vertical_moves.clone(), player) { + return Some(GameResult::PlayerWon(player)); + } + } + } + + // Initialise diagonal moves + let mut main_diag_moves: Vec>> = vec![vec![]; M + N - 1]; + let mut anti_diag_moves: Vec>> = vec![vec![]; main_diag_moves.len()]; + + for row in 0..M { + for col in 0..N { + main_diag_moves[row + col].push(&self.board[row][col]); + } + } + + // Main diagonals + for diagonal_moves in main_diag_moves { + for player in Player::iter() { + if Self::has_win_combination(diagonal_moves.clone(), player) { + return Some(GameResult::PlayerWon(player)); + } + } + } + + let min_anti_diag = -(M as i32) + 1; + + for row in 0..M { + for col in 0..N { + let index = col as i32 - row as i32 - min_anti_diag; + anti_diag_moves[index as usize].push(&self.board[row][col]); + } + } + + // Anti diagonals + for diagonal_moves in anti_diag_moves { + for player in Player::iter() { + if Self::has_win_combination(diagonal_moves.clone(), player) { + return Some(GameResult::PlayerWon(player)); + } + } + } + + if self.ply == M * N { + Some(GameResult::Draw) + } else { + None } } } impl fmt::Display for MNKBoard { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - unimplemented!(); + let sep_row = format!("{}\n", "-".repeat(4 * N - 1)); + let board = self + .board + .iter() + .map(|row| { + let pretty_row = row.iter().map(|c| match c { + Some(Player::Player1) => " O ".cyan().bold(), + Some(Player::Player2) => " X ".red().bold(), + None => " ".normal(), + }); + format!("{}\n", pretty_row.format("|")) + }) + .format(&sep_row); + write!(f, "{}", board) } } impl Game for MNKBoard { type Move = (usize, usize); + type Hash = u64; + + // Generate list of legal moves that can be made by a player + // Time complexity: O(M * N) fn moves(&self) -> Vec { - unimplemented!(); + self.iter2d() + .filter(|(_, c)| c.is_none()) + .map(|((r, c), _)| (r, c)) + .collect() } + // Evaluate the game state to determine if there is a winner + // An analysis for this function can be conducted to find amortized time complexity fn winner(&self) -> Option { - unimplemented!(); + match self.move_sequence.last() { + // Board preloaded with no move sequences given; inspect whole board to determine game result + // Time complexity: O(M * N) + None => self.check_board(), + // Use last move to determine game result + // Time complexity: O(K) + _ => self.check_current_move(), + } } - fn execute_move(&mut self, player_move: Self::Move) -> Result<(), GameError> { - unimplemented!(); + fn execute_move(&mut self, player_move @ (r, c): Self::Move) -> Result<(), GameError> { + if !(0..M).contains(&r) || !(0..N).contains(&c) { + return Err(GameError::NotLegalPosition); + } + if self.board[r][c].is_some() { + return Err(GameError::PositionOccupied); + } + + self.ply += 1; + self.board[r][c] = Some(self.current_player); + self.hash ^= self + .transposition + .hash_func + .get_hash(self.current_player, r, c); + self.current_player = self.current_player.get_next(); + self.move_sequence.push(player_move); + Ok(()) } + // Undo all moves starting from player_move if it is a valid move, otherwise throw a MoveNotFound error fn undo_move(&mut self, player_move: Self::Move) -> Result<(), GameError> { - unimplemented!(); + let index = self.move_sequence.iter().position(|&m| m == player_move); + + match index { + None => Err(GameError::MoveNotFound), + Some(pos) => { + // ply = pos + 1 + let removed_moves = self.move_sequence.drain(pos..); + let removed_ply_count = self.ply - pos; + if removed_ply_count % 2 != 0 { + self.current_player = self.current_player.get_last(); + } + self.ply = pos; + + let mut cur_player = self.current_player; + for (r, c) in removed_moves.into_iter() { + self.board[r][c] = None; + println!( + "{}", + self.transposition.hash_func.get_hash(cur_player, r, c) + ); + self.hash ^= self.transposition.hash_func.get_hash(cur_player, r, c); + cur_player = cur_player.get_next(); + } + + Ok(()) + } + } } } -struct Minimax; +#[derive(Clone, Debug)] +struct MNKZobrist { + hash_values: [[[ as Game>::Hash; N]; M]; Player::COUNT], +} + +impl MNKZobrist { + fn new() -> Self { + let mut rng = rand::thread_rng(); + let mut values: HashSet = HashSet::new(); + while values.len() != Player::COUNT * M * N { + values.insert(rng.gen()); + } + + let values = Vec::from_iter(values); + let mut i = 0; + + let mut table = [[[0; N]; M]; 2]; + for board in table.iter_mut() { + for row in board.iter_mut() { + for cell in row.iter_mut() { + *cell = values[i]; + i += 1; + } + } + } + + MNKZobrist { hash_values: table } + } +} + +impl MNKZobrist { + fn get_hash( + &self, + player: Player, + row: usize, + col: usize, + ) -> as Game>::Hash { + let pid = player as usize; + self.hash_values[pid][row][col] + } +} + +#[derive(Clone, Debug)] +struct MNKTranspositionTable { + table: HashMap< as Game>::Hash, EvalEntry>>, + hash_func: MNKZobrist, +} + +impl MNKTranspositionTable { + fn new() -> Self { + MNKTranspositionTable { + table: HashMap::new(), + hash_func: MNKZobrist::::new(), + } + } + + fn get_entry( + &self, + hash: & as Game>::Hash, + ) -> Option<&EvalEntry>> { + self.table.get(hash) + } + + fn update_eval( + &mut self, + hash: as Game>::Hash, + entry: EvalEntry>, + ) { + self.table.insert(hash, entry); + } +} + +#[derive(Clone, Debug)] +struct EvalEntry { + eval: i32, + best_move: T::Move, +} + +struct MNKMinimax; + +impl MNKMinimax { + const WIN_VAL: i32 = i32::MAX; + const LOSE_VAL: i32 = i32::MIN; + + fn evaluate( + &self, + game: &MNKBoard, + _depth: i32, + ) -> i32 { + match game.winner() { + Some(GameResult::PlayerWon(Player::Player1)) => MNKMinimax::WIN_VAL, + Some(GameResult::PlayerWon(Player::Player2)) => MNKMinimax::LOSE_VAL, + _ => 0, + } + } + + fn alphabeta( + &self, + game: &mut MNKBoard, + depth: i32, + mut alpha: i32, + mut beta: i32, + player: Player, + ) -> i32 { + if let Some(entry) = game.transposition.get_entry(&game.hash) { + return entry.eval; + } + + if game.winner().is_some() { + return self.evaluate(game, depth); + } + + match player { + // maximising player; Player + Player::Player1 => { + let mut best_pos_hash: as Game>::Hash = 0; + let mut best_move: Option< as Game>::Move> = None; + let mut eval = i32::MIN; + let mut cutoff = false; + for m in game.moves() { + game.execute_move(m).unwrap(); + eval = cmp::max( + eval, + self.alphabeta(game, depth + 1, alpha, beta, Player::Player2), + ); + if eval >= beta { + cutoff = true; + } + if eval > alpha { + alpha = eval; + best_move = Some(m); + best_pos_hash = game.hash; + } + game.undo_move(m).unwrap(); + if cutoff { + break; + } + } + + if !cutoff { + if let Some(m) = best_move { + let entry = EvalEntry { eval, best_move: m }; + game.transposition.update_eval(best_pos_hash, entry); + } + } + eval + } + // minimising player; AI + Player::Player2 => { + let mut best_pos_hash: as Game>::Hash = 0; + let mut best_move: Option< as Game>::Move> = None; + let mut eval = i32::MAX; + let mut cutoff = false; + for m in game.moves() { + game.execute_move(m).unwrap(); + eval = cmp::min( + eval, + self.alphabeta(game, depth + 1, alpha, beta, Player::Player1), + ); + if eval <= alpha { + cutoff = true; + } + if beta < eval { + beta = eval; + best_move = Some(m); + best_pos_hash = game.hash; + } + game.undo_move(m).unwrap(); + if cutoff { + break; + } + } + + if !cutoff { + if let Some(m) = best_move { + let entry = EvalEntry { eval, best_move: m }; + game.transposition.update_eval(best_pos_hash, entry); + } + } + eval + } + } + } -impl Minimax { - fn next_move( + fn next_move( &mut self, - game: &G - ) -> G::Move { - unimplemented!(); + game: &mut MNKBoard, + ) -> as Game>::Move { + let move_evals: Vec<_> = game + .moves() + .iter() + .map(|m| { + game.execute_move(*m).unwrap(); + let eval = self.alphabeta(game, 0, i32::MIN, i32::MAX, Player::Player1); + game.undo_move(*m).unwrap(); + (*m, eval) + }) + .collect(); + + let (best_move, _) = move_evals + .iter() + .min_by(|(_, v1), (_, v2)| v1.cmp(v2)) + .unwrap(); + *best_move } } @@ -100,14 +575,20 @@ fn parse_input(s: &str) -> Result<(usize, usize), Box> { } fn main() { - let mut b: MNKBoard<4, 4, 4> = MNKBoard::new(); - let mut ai = Minimax; + let mut b: MNKBoard<4, 4, 3> = MNKBoard::new(); + let mut ai = MNKMinimax; let mut player = Player::Player1; + println!("{}", b); while b.winner().is_none() { match player { Player::Player1 => { + assert_eq!(b.current_player, Player::Player1); let mut line = String::new(); + + print!("Make your move (row, column): "); + std::io::stdout().flush().unwrap(); + std::io::stdin().read_line(&mut line).unwrap(); if let Ok(pos) = parse_input(line.as_str()) { if let Err(msg) = b.execute_move(pos) { @@ -121,25 +602,41 @@ fn main() { } } Player::Player2 => { - b.execute_move(ai.next_move(&b)) - .unwrap(); + assert_eq!(b.current_player, Player::Player2); + let ai_move = ai.next_move(&mut b); + b.execute_move(ai_move).unwrap(); + println!("The AI has made the move {:?}!", ai_move); println!("{}", b); } } player = player.get_next(); } + + match b.winner() { + Some(GameResult::PlayerWon(Player::Player1)) => { + println!("The winner is: {}", "O".blue().bold()); + } + Some(GameResult::PlayerWon(Player::Player2)) => { + println!("The winner is: {}", "X".red().bold()); + } + Some(GameResult::Draw) => { + println!("The game is drawn!"); + } + None => {} + } } mod test { use crate::{GameResult::*, Player::*, *}; #[test] fn three_by_three_tests() { - let board = MNKBoard::<3, 3, 3>{ - board: [[Some(Player1), None, None], + let board = [ + [Some(Player1), None, None], [Some(Player1), None, None], - [Some(Player1), None, None]], - current_player: Player1 - }; + [Some(Player1), None, None], + ]; + + let board = MNKBoard::<3, 3, 3>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player1))); assert_eq!( @@ -147,114 +644,281 @@ mod test { vec![(0, 1), (0, 2), (1, 1), (1, 2), (2, 1), (2, 2)] ); - let board = MNKBoard::<3, 3, 3> { - board: [[None, Some(Player2), None], + let board = [ [None, Some(Player2), None], - [None, Some(Player2), None]], - current_player: Player1 - }; + [None, Some(Player2), None], + [None, Some(Player2), None], + ]; + + let board = MNKBoard::<3, 3, 3>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player2))); - let board = MNKBoard::<3, 3, 3> { - board: [[None, Some(Player2), None], + let board = [ [None, Some(Player2), None], - [None, Some(Player1), None]], - current_player: Player1 - }; + [None, Some(Player2), None], + [None, Some(Player1), None], + ]; + + let board = MNKBoard::<3, 3, 3>::load_board(board, Player1); assert_eq!(board.winner(), None); - let board = MNKBoard::<3, 3, 2> { - board: [[None, Some(Player2), None], + let board = [ + [None, Some(Player2), None], [None, Some(Player2), None], - [None, Some(Player1), None]], - current_player: Player1 - }; + [None, Some(Player1), None], + ]; - assert_eq!(board.winner(), Some(PlayerWon(Player2))); + let board = MNKBoard::<3, 3, 3>::load_board(board, Player1); + + assert_eq!(board.winner(), None); - let board = MNKBoard::<3, 3, 3> { - board: [[None, Some(Player2), None], + let board = [ + [None, Some(Player2), None], [None, Some(Player1), None], - [Some(Player2), Some(Player2), Some(Player2)]], - current_player: Player1 - }; + [Some(Player2), Some(Player2), Some(Player2)], + ]; + + let board = MNKBoard::<3, 3, 3>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player2))); - let board = MNKBoard::<3, 3, 2> { - board: [[None, Some(Player2), None], + let board = [ + [None, Some(Player2), None], [None, Some(Player1), None], - [Some(Player2), None, Some(Player1)]], - current_player: Player1 - }; + [Some(Player2), None, Some(Player1)], + ]; + + let board = MNKBoard::<3, 3, 2>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player1))); - let board = MNKBoard::<3, 3, 2> { - board: [[None, Some(Player2), None], + let board = [ + [None, Some(Player2), None], [Some(Player1), None, None], - [Some(Player2), Some(Player1), None]], - current_player: Player1 - }; + [Some(Player2), Some(Player1), None], + ]; + + let board = MNKBoard::<3, 3, 2>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player1))); - assert_eq!( - board.moves(), - vec![(0, 0), (0, 2), (1, 1), (1, 2), (2, 2)] - ); + assert_eq!(board.moves(), vec![(0, 0), (0, 2), (1, 1), (1, 2), (2, 2)]); - let board = MNKBoard::<3, 3, 2> { - board: [[None, Some(Player2), None], + let board = [ + [None, Some(Player2), None], [Some(Player1), None, Some(Player2)], - [Some(Player2), None, None]], - current_player: Player1 - }; + [Some(Player2), None, None], + ]; + + let board = MNKBoard::<3, 3, 2>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player2))); - let board = MNKBoard::<2, 3, 2> { - board: [[None, Some(Player2), None], - [Some(Player1), None, Some(Player2)]], - current_player: Player1 - }; + let board = [ + [None, Some(Player2), None], + [Some(Player1), None, Some(Player2)], + ]; + + let board = MNKBoard::<2, 3, 2>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player2))); - let board = MNKBoard::<3, 2, 2> { - board: [[None, Some(Player2)], + let board = [ + [None, Some(Player2)], [Some(Player1), None], - [Some(Player2), Some(Player1)]], - current_player: Player1 - }; + [Some(Player2), Some(Player1)], + ]; + + let board = MNKBoard::<3, 2, 2>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player1))); - let board = MNKBoard::<3, 2, 2> { - board: [[None, Some(Player1)], + let board = [ + [None, Some(Player1)], [None, Some(Player2)], - [Some(Player2), Some(Player1)]], - current_player: Player1 - }; + [Some(Player2), Some(Player1)], + ]; + + let board = MNKBoard::<3, 2, 2>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player2))); - let board = MNKBoard::<2, 3, 2> { - board: [[None, Some(Player2), None], - [Some(Player2), None, Some(Player1)]], - current_player: Player1 - }; + let board = [ + [None, Some(Player2), None], + [Some(Player2), None, Some(Player1)], + ]; + + let board = MNKBoard::<2, 3, 2>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player2))); - let board = MNKBoard::<3, 3, 2> { - board: [[None, None, None], + let board = [ + [None, None, None], [None, None, Some(Player1)], - [None, Some(Player1), None]], - current_player: Player1 - }; + [None, Some(Player1), None], + ]; + + let board = MNKBoard::<3, 3, 2>::load_board(board, Player1); assert_eq!(board.winner(), Some(PlayerWon(Player1))); + + let board = [ + [Some(Player1), Some(Player2), Some(Player1)], + [Some(Player2), Some(Player2), Some(Player1)], + [Some(Player2), Some(Player1), Some(Player1)], + ]; + + let board = MNKBoard::<3, 3, 3>::load_board(board, Player2); + assert_eq!(board.winner(), Some(PlayerWon(Player1))); + } + + #[test] + fn check_result_by_last_move() { + let board = [ + [Some(Player1), Some(Player1), Some(Player1)], + [None, Some(Player2), None], + [None, Some(Player2), None], + ]; + + let mut board = MNKBoard::<3, 3, 3>::load_board(board, Player2); + board.move_sequence = vec![(0, 0)]; + + assert_eq!(board.winner(), Some(PlayerWon(Player1))); + + let board = [ + [Some(Player1), Some(Player1), Some(Player1)], + [None, Some(Player2), None], + [None, Some(Player2), None], + ]; + + let mut board = MNKBoard::<3, 3, 3>::load_board(board, Player2); + board.move_sequence = vec![(0, 1)]; + + assert_eq!(board.winner(), Some(PlayerWon(Player1))); + + let board = [ + [Some(Player1), Some(Player1), Some(Player1)], + [None, Some(Player2), None], + [None, Some(Player2), None], + ]; + + let mut board = MNKBoard::<3, 3, 3>::load_board(board, Player2); + board.move_sequence = vec![(0, 2)]; + + assert_eq!(board.winner(), Some(PlayerWon(Player1))); + + let board = [ + [Some(Player1), Some(Player2), Some(Player1)], + [Some(Player2), Some(Player2), Some(Player1)], + [Some(Player2), Some(Player1), Some(Player1)], + ]; + + let mut board = MNKBoard::<3, 3, 3>::load_board(board, Player2); + board.move_sequence = vec![ + (0, 0), + (1, 1), + (2, 2), + (0, 1), + (2, 1), + (2, 0), + (0, 2), + (1, 0), + (1, 2), + ]; + assert_eq!(board.winner(), Some(PlayerWon(Player1))); + } + + #[test] + fn check_current_player_cycle() { + let mut board = MNKBoard::<3, 3, 3>::new(); + assert_eq!(board.current_player, Player1); + board.execute_move((0, 1)).unwrap(); + + assert_eq!(board.current_player, Player2); + board.execute_move((0, 0)).unwrap(); + + assert_eq!(board.current_player, Player1); + board.execute_move((1, 1)).unwrap(); + + assert_eq!(board.current_player, Player2); + board.execute_move((2, 1)).unwrap(); + + assert_eq!(board.current_player, Player1); + board.execute_move((2, 0)).unwrap(); + + assert_eq!(board.current_player, Player2); + board.execute_move((1, 0)).unwrap(); + + assert_eq!(board.current_player, Player1); + board.execute_move((1, 2)).unwrap(); + + assert_eq!(board.current_player, Player2); + board.execute_move((0, 2)).unwrap(); + + assert_eq!(board.current_player, Player1); + board.execute_move((2, 2)).unwrap(); + + assert_eq!(board.winner(), Some(Draw)); + } + + #[test] + fn undo_arbitrary_move() { + let mut board = MNKBoard::<3, 3, 3>::new(); + assert_eq!(board.current_player, Player1); + board.execute_move((0, 1)).unwrap(); + + assert_eq!(board.current_player, Player2); + board.execute_move((0, 0)).unwrap(); + + assert_eq!(board.current_player, Player1); + board.execute_move((1, 1)).unwrap(); + + assert_eq!(board.current_player, Player2); + board.execute_move((2, 1)).unwrap(); + + assert_eq!(board.current_player, Player1); + board.undo_move((2, 1)).unwrap(); + + assert_eq!(board.current_player, Player2); + board.undo_move((0, 0)).unwrap(); + + assert_eq!(board.current_player, Player2); + board.undo_move((0, 1)).unwrap(); + + assert_eq!(board.current_player, Player1); + } + + #[test] + fn zobrist_hash_is_valid() { + // Create Zobrist hash table with following predetermined values: + let mut board = MNKBoard::<3, 3, 3>::new(); + + let white = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]; + let black = [[9, 10, 11], [12, 13, 14], [15, 16, 17]]; + let zobrist_table = [white, black]; + board.transposition.hash_func.hash_values = zobrist_table; + + assert_eq!(board.current_player, Player1); + + board.execute_move((2, 2)).unwrap(); + assert_eq!(board.current_player, Player2); + assert_eq!(board.hash, 8); + + board.execute_move((2, 1)).unwrap(); + assert_eq!(board.current_player, Player1); + assert_eq!(board.hash, 24); + + board.execute_move((2, 0)).unwrap(); + assert_eq!(board.current_player, Player2); + assert_eq!(board.hash, 30); + + board.execute_move((1, 2)).unwrap(); + assert_eq!(board.current_player, Player1); + assert_eq!(board.hash, 16); + + board.undo_move((2, 2)).unwrap(); + assert_eq!(board.current_player, Player1); + assert_eq!(board.hash, 0); } }