From 43d39fba30b065812bb23482f7cb095fd45a8600 Mon Sep 17 00:00:00 2001 From: nils <48135649+Nilstrieb@users.noreply.github.com> Date: Mon, 31 Oct 2022 15:45:56 +0100 Subject: [PATCH] slowly working --- Cargo.toml | 4 +++ build.rs | 66 +++++++++++++++++++++++++++++++++++----------- src/board.rs | 28 +++++++++++++++----- src/game.rs | 13 ++++++--- src/lib.rs | 33 ++++++++++++++++++++--- src/main.rs | 44 +++++++++++++++++++++++++++---- src/minmax.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 227 insertions(+), 34 deletions(-) create mode 100644 src/minmax.rs diff --git a/Cargo.toml b/Cargo.toml index 6054685..deb525b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + + +[profile.dev] +opt-level = 3 \ No newline at end of file diff --git a/build.rs b/build.rs index dcbf0fe..e4b583d 100644 --- a/build.rs +++ b/build.rs @@ -1,11 +1,18 @@ use std::{fs::File, io::Write, path::PathBuf}; -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Player { X, O, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum State { + Winner(Player), + InProgress, + Draw, +} + impl Player { fn from_u8(num: u8) -> Option { match num { @@ -15,14 +22,6 @@ impl Player { _ => panic!("Invalid value {num}"), } } - - fn as_u8(this: Option) -> u8 { - match this { - Some(Player::X) => 0, - Some(Player::O) => 1, - None => 2, - } - } } #[derive(Clone, Copy)] @@ -42,16 +41,42 @@ impl Board { Some(Self(num)) } - fn get(&self, index: usize) -> Option { + 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}"); + } + } + } + + pub fn get(&self, index: usize) -> Option { + self.validate(); debug_assert!(index < 9); - let shifted = self.0 >> (index * 2); + let board = self.0; + + let shifted = board >> (index * 2); let masked = shifted & 0b11; + Player::from_u8(masked as u8) } + + pub fn iter(&self) -> impl Iterator> { + let mut i = 0; + let this = self.clone(); + std::iter::from_fn(move || { + let result = (i < 8).then(|| this.get(i)); + i += 1; + result + }) + } } -fn winner(board: Board) -> Option { +fn result(board: Board) -> State { fn won_row(a: Option, b: Option, c: Option) -> Option { if a == Some(Player::X) && b == Some(Player::X) && c == Some(Player::X) { Some(Player::X) @@ -65,12 +90,16 @@ fn winner(board: Board) -> Option { macro_rules! test_row { ($a:literal, $b:literal, $c:literal) => { match won_row(board.get($a), board.get($b), board.get($c)) { - Some(player) => return Some(player), + Some(player) => return State::Winner(player), None => {} } }; } + if board.iter().all(|x| x.is_some()) { + return State::Draw; + } + test_row!(0, 1, 2); test_row!(3, 4, 5); test_row!(6, 7, 8); @@ -81,15 +110,20 @@ fn winner(board: Board) -> Option { test_row!(0, 4, 8); test_row!(2, 4, 6); - None + State::InProgress } fn calculate_win_table(file: &mut impl Write) { for board in 0..(2u32.pow(18)) { let byte = match Board::new(board) { Some(board) => { - let winner = winner(board); - Player::as_u8(winner) + let winner = result(board); + match winner { + State::Winner(Player::X) => 0, + State::Winner(Player::O) => 1, + State::InProgress => 2, + State::Draw => 3, + } } None => 0, }; diff --git a/src/board.rs b/src/board.rs index 198fc7d..92eea65 100644 --- a/src/board.rs +++ b/src/board.rs @@ -6,6 +6,13 @@ pub enum Player { O, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum State { + Winner(Player), + InProgress, + Draw, +} + impl Player { pub fn opponent(self) -> Self { match self { @@ -23,7 +30,7 @@ impl Player { }) } - fn as_u8(this: Option) -> u8 { + pub fn as_u8(this: Option) -> u8 { match this { Some(Player::X) => 0, Some(Player::O) => 1, @@ -95,28 +102,35 @@ impl Board { self.validate(); } - pub fn iter(&self) -> impl Iterator> + '_ { + pub fn iter(&self) -> impl Iterator> { let mut i = 0; + let this = self.clone(); std::iter::from_fn(move || { - let result = (i < 8).then(|| self.get(i)); + let result = (i < 8).then(|| this.get(i)); i += 1; result }) } - pub fn result(&self) -> Option { + pub fn result(&self) -> State { win_table::result(self) } } mod win_table { - use super::{Board, Player}; + 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")); - pub fn result(board: &Board) -> Option { - Player::from_u8(WIN_TABLE[board.0 as usize]).unwrap() + pub fn result(board: &Board) -> State { + match WIN_TABLE[board.0 as usize] { + 0 => State::Winner(Player::X), + 1 => State::Winner(Player::X), + 2 => State::InProgress, + 3 => State::Draw, + n => panic!("Invalid value {n} in table"), + } } } diff --git a/src/game.rs b/src/game.rs index 5e0ef68..affd7cd 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,4 +1,7 @@ -use crate::{board::Player, Board, GamePlayer}; +use crate::{ + board::{Player, State}, + Board, GamePlayer, +}; impl Board { pub fn play(&mut self, a: &mut A, b: &mut B) -> Option { @@ -11,8 +14,12 @@ impl Board { b.next_move(self, current_player); } - if let Some(winner) = self.result() { - return Some(winner); + match self.result() { + State::Winner(player) => return Some(player), + State::Draw => { + return None; + } + State::InProgress => {} } current_player = current_player.opponent(); diff --git a/src/lib.rs b/src/lib.rs index 995da17..5672a48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,17 @@ mod board; mod game; +mod minmax; use std::io::Write; -use board::Player; - -pub use board::Board; +pub use board::{Board, Player, State}; +pub use minmax::PerfectPlayer; pub trait GamePlayer { fn next_move(&mut self, board: &mut Board, this_player: Player); } +#[derive(Clone)] pub struct GreedyPlayer; impl GamePlayer for GreedyPlayer { @@ -20,6 +21,7 @@ impl GamePlayer for GreedyPlayer { } } +#[derive(Clone)] pub struct HumanPlayer; impl GamePlayer for HumanPlayer { @@ -48,3 +50,28 @@ impl GamePlayer for HumanPlayer { } } } + +#[derive(Clone)] +pub struct RandomPlayer; + +fn fun_random() -> u64 { + use std::hash::{BuildHasher, Hasher}; + std::collections::hash_map::RandomState::new() + .build_hasher() + .finish() +} + +impl GamePlayer for RandomPlayer { + fn next_move(&mut self, board: &mut Board, this_player: Player) { + loop { + let next = (fun_random() % 9) as usize; + match board.get(next) { + Some(_) => {} + None => { + board.set(next, Some(this_player)); + return; + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 60efe51..f272dae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,47 @@ -use minmax::{Board, GreedyPlayer}; +#![allow(unused_imports)] + +use std::time::SystemTime; + +use minmax::{Board, GamePlayer, GreedyPlayer, HumanPlayer, PerfectPlayer, Player, RandomPlayer}; fn main() { + let mut results = [0, 0, 0]; + + let start = SystemTime::now(); + + for _ in 0..10_000 { + let result = play_round(PerfectPlayer::new(), PerfectPlayer::new(), false); + let idx = Player::as_u8(result); + results[idx as usize] += 1; + } + + println!("Winner counts"); + println!(" X: {}", results[0]); + println!(" O: {}", results[1]); + println!(" Draw: {}", results[2]); + + let time = start.elapsed().unwrap(); + + println!("Completed in {}ms", time.as_millis()); +} + +fn play_round(mut a: A, mut b: B, print: bool) -> Option { let mut board = Board::empty(); - let result = board.play(&mut GreedyPlayer, &mut GreedyPlayer); - println!("{board}"); + let result = board.play(&mut a, &mut b); + if print { + println!("{board}"); + } match result { Some(winner) => { - println!("player {winner} won!"); + if print { + println!("player {winner} won!"); + } + } + None => { + if print { + println!("a draw...") + } } - None => println!("a draw..."), } + result } diff --git a/src/minmax.rs b/src/minmax.rs new file mode 100644 index 0000000..b30e119 --- /dev/null +++ b/src/minmax.rs @@ -0,0 +1,73 @@ +use crate::{ + board::{Player, State}, + Board, GamePlayer, +}; + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +struct Score(isize); + +#[derive(Clone)] +pub struct PerfectPlayer { + best_move: usize, +} + +impl PerfectPlayer { + pub fn new() -> Self { + Self { + best_move: usize::MAX, + } + } + + fn minmax(&mut self, board: &mut Board, player: Player, depth: usize) -> Score { + match board.result() { + State::Winner(winner) => { + if winner == player { + Score(1) + } else { + Score(-1) + } + } + State::Draw => Score(0), + State::InProgress => { + let mut max_value = Score(isize::MIN); + + debug_assert!( + !board.iter().all(|x| x.is_some()), + "the board is full but state is InProgress" + ); + + for (i, pos) in board.iter().enumerate() { + if pos.is_some() { + continue; + } + + board.set(i, Some(player)); + let value = self.minmax(board, player.opponent(), depth + 1); + board.set(i, None); + + if value > max_value { + max_value = value; + if depth == 0 { + self.best_move = i; + } + } + } + + max_value + } + } + } +} + +impl GamePlayer for PerfectPlayer { + fn next_move(&mut self, board: &mut Board, this_player: Player) { + self.best_move = usize::MAX; + self.minmax(board, this_player, 0); + + if self.best_move == usize::MAX { + panic!("could not make a move"); + } + + board.set(self.best_move, Some(this_player)); + } +}