diff --git a/Cargo.toml b/Cargo.toml index deb525b..473b494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,7 @@ edition = "2021" [profile.dev] -opt-level = 3 \ No newline at end of file +opt-level = 3 + +[profile.release] +debug = true \ No newline at end of file diff --git a/build.rs b/build.rs index e4b583d..eb40c49 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,25 @@ +//! Builds the board state table +//! +//! The board is encoded as an 18 bit integer, two bits for each position. +//! The position are in the bits row by row with the first position being the +//! least significant two bits. +//! ```text +//! 0 => X +//! 1 => O +//! 2 => Empty +//! 3 => INVALID +//! ``` +//! +//! Then, this integer is used as an index into the winner table. +//! Each byte of the winner table contains the information about the game state. +//! ```text +//! 0 => X +//! 1 => O +//! 2 => In Progress +//! 3 => Draw +//! _ => INVALID +//! ``` + use std::{fs::File, io::Write, path::PathBuf}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -69,7 +91,7 @@ impl Board { let mut i = 0; let this = self.clone(); std::iter::from_fn(move || { - let result = (i < 8).then(|| this.get(i)); + let result = (i < 9).then(|| this.get(i)); i += 1; result }) diff --git a/first b/first new file mode 100755 index 0000000..8b71a9f Binary files /dev/null and b/first differ diff --git a/perf.data b/perf.data new file mode 100644 index 0000000..16b2060 Binary files /dev/null and b/perf.data differ diff --git a/perf.data.old b/perf.data.old new file mode 100644 index 0000000..cbf50b5 Binary files /dev/null and b/perf.data.old differ diff --git a/second b/second new file mode 100755 index 0000000..0085b65 Binary files /dev/null and b/second differ diff --git a/src/board.rs b/src/board.rs index 92eea65..59cbc87 100644 --- a/src/board.rs +++ b/src/board.rs @@ -58,24 +58,21 @@ impl Board { Self(0x0002AAAA) } - #[cfg(not(debug_assertions))] - fn validate(&self) {} - - #[cfg(debug_assertions)] fn validate(&self) { - let board = self.0; - for i in 0..16 { - let next_step = board >> (i * 2); - let mask = 0b11; - let pos = next_step & mask; - if pos >= 3 { - panic!("Invalid bits, self: {board:0X}, bits: {pos:0X}"); + if cfg!(debug_assertions) { + let board = self.0; + for i in 0..16 { + let next_step = board >> (i * 2); + let mask = 0b11; + let pos = next_step & mask; + if pos >= 3 { + panic!("Invalid bits, self: {board:0X}, bits: {pos:0X}"); + } } } } pub fn get(&self, index: usize) -> Option { - self.validate(); debug_assert!(index < 9); let board = self.0; @@ -83,7 +80,13 @@ impl Board { let shifted = board >> (index * 2); let masked = shifted & 0b11; - Player::from_u8(masked as u8).unwrap() + // SAFETY: So uh, this is a bit unlucky. + // You see, there are two entire bits of information at our disposal for each position. + // This is really bad. We only have three valid states. So we need to do _something_ if it's invalid. + // We just hope that it will never be invalid which it really shouldn't be and also have a debug assertion + // here to make sure that it really is valid and then if it's not invalid we just mov it out and are happy. + self.validate(); + unsafe { Player::from_u8(masked as u8).unwrap_unchecked() } } pub fn set(&mut self, index: usize, value: Option) { @@ -106,7 +109,7 @@ impl Board { let mut i = 0; let this = self.clone(); std::iter::from_fn(move || { - let result = (i < 8).then(|| this.get(i)); + let result = (i < 9).then(|| this.get(i)); i += 1; result }) @@ -121,7 +124,7 @@ mod win_table { use super::{Board, Player, State}; const WIN_TABLE_SIZE: usize = 2usize.pow(2 * 9); - const WIN_TABLE: &[u8; WIN_TABLE_SIZE] = include_bytes!(concat!(env!("OUT_DIR"), "/win_table")); + static WIN_TABLE: &[u8; WIN_TABLE_SIZE] = include_bytes!(concat!(env!("OUT_DIR"), "/win_table")); pub fn result(board: &Board) -> State { match WIN_TABLE[board.0 as usize] { diff --git a/src/game.rs b/src/game.rs index affd7cd..da772fc 100644 --- a/src/game.rs +++ b/src/game.rs @@ -4,14 +4,18 @@ use crate::{ }; impl Board { - pub fn play(&mut self, a: &mut A, b: &mut B) -> Option { + pub fn default_play() -> Option { + Self::empty().play(&mut X::default(), &mut O::default()) + } + + pub fn play(&mut self, x: &mut A, o: &mut B) -> Option { let mut current_player = Player::X; for _ in 0..9 { if current_player == Player::X { - a.next_move(self, current_player); + x.next_move(self, current_player); } else { - b.next_move(self, current_player); + o.next_move(self, current_player); } match self.result() { diff --git a/src/lib.rs b/src/lib.rs index 5672a48..ec310c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,17 @@ mod board; mod game; -mod minmax; +mod perfect; use std::io::Write; pub use board::{Board, Player, State}; -pub use minmax::PerfectPlayer; +pub use perfect::PerfectPlayer; -pub trait GamePlayer { +pub trait GamePlayer: Default { fn next_move(&mut self, board: &mut Board, this_player: Player); } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct GreedyPlayer; impl GamePlayer for GreedyPlayer { @@ -21,7 +21,7 @@ impl GamePlayer for GreedyPlayer { } } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct HumanPlayer; impl GamePlayer for HumanPlayer { @@ -51,7 +51,7 @@ impl GamePlayer for HumanPlayer { } } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct RandomPlayer; fn fun_random() -> u64 { @@ -75,3 +75,34 @@ impl GamePlayer for RandomPlayer { } } } + +#[cfg(test)] +mod tests { + use crate::{Board, GamePlayer, GreedyPlayer, PerfectPlayer, Player, RandomPlayer}; + + fn assert_win_ratio(runs: u64, x_win_ratio: f64) { + let mut results = [0u64, 0, 0]; + + for _ in 0..runs { + let result = Board::default_play::(); + let idx = Player::as_u8(result); + results[idx as usize] += 1; + } + + let total = results.iter().copied().sum::(); + + let ratio = (total as f64) / (results[0] as f64); + println!("{ratio} >= {x_win_ratio}"); + assert!(ratio >= x_win_ratio); + } + + #[test] + fn perfect_always_beats_greedy() { + assert_win_ratio::(20, 1.0); + } + + #[test] + fn perfect_beats_random() { + assert_win_ratio::(10, 0.95); + } +} diff --git a/src/main.rs b/src/main.rs index f272dae..142795b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,8 +9,8 @@ fn main() { let start = SystemTime::now(); - for _ in 0..10_000 { - let result = play_round(PerfectPlayer::new(), PerfectPlayer::new(), false); + for _ in 0..1000 { + let result = play_round::(false); let idx = Player::as_u8(result); results[idx as usize] += 1; } @@ -25,21 +25,21 @@ fn main() { println!("Completed in {}ms", time.as_millis()); } -fn play_round(mut a: A, mut b: B, print: bool) -> Option { +fn play_round(print: bool) -> Option { let mut board = Board::empty(); - let result = board.play(&mut a, &mut b); + let result = board.play(&mut X::default(), &mut O::default()); if print { - println!("{board}"); + //println!("{board}"); } match result { Some(winner) => { if print { - println!("player {winner} won!"); + //println!("player {winner} won!"); } } None => { if print { - println!("a draw...") + //println!("a draw...") } } } diff --git a/src/minmax.rs b/src/perfect.rs similarity index 53% rename from src/minmax.rs rename to src/perfect.rs index b30e119..b4e29d1 100644 --- a/src/minmax.rs +++ b/src/perfect.rs @@ -1,16 +1,40 @@ +use std::ops::Neg; + use crate::{ board::{Player, State}, Board, GamePlayer, }; -#[derive(PartialEq, Eq, PartialOrd, Ord)] -struct Score(isize); +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +enum Score { + Lost = -1, + Tie = 0, + Won = 1, +} + +impl Neg for Score { + type Output = Self; + + fn neg(self) -> Self::Output { + match self { + Self::Lost => Self::Won, + Self::Tie => Self::Tie, + Self::Won => Self::Lost, + } + } +} #[derive(Clone)] pub struct PerfectPlayer { best_move: usize, } +impl Default for PerfectPlayer { + fn default() -> Self { + Self::new() + } +} + impl PerfectPlayer { pub fn new() -> Self { Self { @@ -19,17 +43,31 @@ impl PerfectPlayer { } fn minmax(&mut self, board: &mut Board, player: Player, depth: usize) -> Score { + if depth < 2 { + //print!("{board}{}| playing {player}: ", " ".repeat(depth)); + } match board.result() { State::Winner(winner) => { + if depth < 2 { + //println!(" a winner {winner}"); + } if winner == player { - Score(1) + Score::Won } else { - Score(-1) + Score::Lost } } - State::Draw => Score(0), + State::Draw => { + if depth < 2 { + //println!("this is gonna be a draw"); + } + Score::Tie + } State::InProgress => { - let mut max_value = Score(isize::MIN); + if depth < 2 { + //println!("not done yet"); + } + let mut max_value = Score::Lost; debug_assert!( !board.iter().all(|x| x.is_some()), @@ -42,7 +80,15 @@ impl PerfectPlayer { } board.set(i, Some(player)); - let value = self.minmax(board, player.opponent(), depth + 1); + let value = -self.minmax(board, player.opponent(), depth + 1); + + if depth < 2 { + if i == 8 { + //println!("AAA\n{board}AAAA"); + } + //println!("{}^| {i} {player} -> {:?}", " ".repeat(depth), value); + } + board.set(i, None); if value > max_value {