mirror of
https://github.com/Noratrieb/minmax.git
synced 2026-01-15 07:45:04 +01:00
tic tac toe
This commit is contained in:
parent
43d39fba30
commit
c7a6bdf3c0
11 changed files with 149 additions and 40 deletions
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