mirror of
https://github.com/Noratrieb/minmax.git
synced 2026-01-14 15:25:08 +01:00
tic tac toe
This commit is contained in:
parent
43d39fba30
commit
c7a6bdf3c0
11 changed files with 149 additions and 40 deletions
|
|
@ -9,4 +9,7 @@ edition = "2021"
|
|||
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 3
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
24
build.rs
24
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
|
||||
})
|
||||
|
|
|
|||
BIN
first
Executable file
BIN
first
Executable file
Binary file not shown.
BIN
perf.data
Normal file
BIN
perf.data
Normal file
Binary file not shown.
BIN
perf.data.old
Normal file
BIN
perf.data.old
Normal file
Binary file not shown.
BIN
second
Executable file
BIN
second
Executable file
Binary file not shown.
33
src/board.rs
33
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<Player> {
|
||||
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<Player>) {
|
||||
|
|
@ -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] {
|
||||
|
|
|
|||
10
src/game.rs
10
src/game.rs
|
|
@ -4,14 +4,18 @@ use crate::{
|
|||
};
|
||||
|
||||
impl Board {
|
||||
pub fn play<A: GamePlayer, B: GamePlayer>(&mut self, a: &mut A, b: &mut B) -> Option<Player> {
|
||||
pub fn default_play<X: GamePlayer, O: GamePlayer>() -> Option<Player> {
|
||||
Self::empty().play(&mut X::default(), &mut O::default())
|
||||
}
|
||||
|
||||
pub fn play<A: GamePlayer, B: GamePlayer>(&mut self, x: &mut A, o: &mut B) -> Option<Player> {
|
||||
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() {
|
||||
|
|
|
|||
43
src/lib.rs
43
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<X: GamePlayer, O: GamePlayer>(runs: u64, x_win_ratio: f64) {
|
||||
let mut results = [0u64, 0, 0];
|
||||
|
||||
for _ in 0..runs {
|
||||
let result = Board::default_play::<X, O>();
|
||||
let idx = Player::as_u8(result);
|
||||
results[idx as usize] += 1;
|
||||
}
|
||||
|
||||
let total = results.iter().copied().sum::<u64>();
|
||||
|
||||
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::<PerfectPlayer, GreedyPlayer>(20, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perfect_beats_random() {
|
||||
assert_win_ratio::<PerfectPlayer, RandomPlayer>(10, 0.95);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
src/main.rs
14
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::<PerfectPlayer, GreedyPlayer>(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<A: GamePlayer, B: GamePlayer>(mut a: A, mut b: B, print: bool) -> Option<Player> {
|
||||
fn play_round<X: GamePlayer, O: GamePlayer>(print: bool) -> Option<Player> {
|
||||
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...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
Loading…
Add table
Add a link
Reference in a new issue