mirror of
https://github.com/Noratrieb/minmax.git
synced 2026-01-14 15:25:08 +01:00
tests
This commit is contained in:
parent
36fb1a5d3b
commit
d3973cc96f
5 changed files with 125 additions and 64 deletions
|
|
@ -12,7 +12,10 @@ pub mod tic_tac_toe;
|
||||||
|
|
||||||
pub mod player;
|
pub mod player;
|
||||||
|
|
||||||
use std::{fmt::Display, ops::Neg};
|
use std::{
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
ops::Neg,
|
||||||
|
};
|
||||||
|
|
||||||
pub use self::minmax::PerfectPlayer;
|
pub use self::minmax::PerfectPlayer;
|
||||||
pub use player::{Player, State};
|
pub use player::{Player, State};
|
||||||
|
|
@ -78,7 +81,7 @@ pub trait Game: Display {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct Score(i32);
|
pub struct Score(i32);
|
||||||
|
|
||||||
impl Score {
|
impl Score {
|
||||||
|
|
@ -106,3 +109,35 @@ impl Neg for Score {
|
||||||
Self(-self.0)
|
Self(-self.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for Score {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Self::WON => f.write_str("WON"),
|
||||||
|
Self::LOST => f.write_str("LOST"),
|
||||||
|
Self(other) => Debug::fmt(&other, f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn assert_win_ratio<G: Game, X: GamePlayer<G>, O: GamePlayer<G>>(
|
||||||
|
runs: u64,
|
||||||
|
x_win_ratio: f64,
|
||||||
|
x: impl Fn() -> X,
|
||||||
|
o: impl Fn() -> O,
|
||||||
|
) {
|
||||||
|
let mut results = [0u64, 0, 0];
|
||||||
|
|
||||||
|
for _ in 0..runs {
|
||||||
|
let result = G::empty().play::<X, O>(&mut x(), &mut 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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,8 @@ fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let player_a = get_player(args.o);
|
let player_a = get_player(args.x);
|
||||||
let player_b = get_player(args.x);
|
let player_b = get_player(args.o);
|
||||||
|
|
||||||
play_with_players(player_a, player_b);
|
play_with_players(player_a, player_b);
|
||||||
}
|
}
|
||||||
|
|
@ -105,8 +105,8 @@ fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let player_a = get_player(args.o);
|
let player_a = get_player(args.x);
|
||||||
let player_b = get_player(args.x);
|
let player_b = get_player(args.o);
|
||||||
|
|
||||||
play_with_players(player_a, player_b);
|
play_with_players(player_a, player_b);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,31 +61,37 @@ impl<G: Game> PerfectPlayer<G> {
|
||||||
|
|
||||||
for pos in board.possible_moves() {
|
for pos in board.possible_moves() {
|
||||||
board.make_move(pos, maximizing_player);
|
board.make_move(pos, maximizing_player);
|
||||||
let value =
|
let value = -self.minmax(
|
||||||
-self.minmax(board, maximizing_player.opponent(), -beta, -max_value, depth + 1);
|
board,
|
||||||
|
maximizing_player.opponent(),
|
||||||
|
-beta,
|
||||||
|
-max_value,
|
||||||
|
depth + 1,
|
||||||
|
);
|
||||||
|
|
||||||
board.undo_move(pos);
|
board.undo_move(pos);
|
||||||
|
|
||||||
if value > max_value {
|
if value >= max_value {
|
||||||
max_value = value;
|
max_value = value;
|
||||||
if depth == 0 {
|
if depth == 0 {
|
||||||
self.best_move = Some(pos);
|
self.best_move = Some(pos);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Imagine a game tree like this
|
// Imagine a game tree like this
|
||||||
// P( )
|
// P( )
|
||||||
// / \
|
// / \
|
||||||
// A(10) B( ) <- we are here in the loop for the first child that returned 11.
|
// A(10) B( ) <- we are here in the loop for the first child that returned 11.
|
||||||
// / \
|
// / \
|
||||||
// C(11) D( )
|
// C(11) D( )
|
||||||
//
|
//
|
||||||
// Our beta parameter is 10, because that's the current max_value of our parent.
|
// Our beta parameter is 10, because that's the current max_value of our parent.
|
||||||
// If P plays B, we know that B will pick something _at least_ as good as C. This means
|
// If P plays B, we know that B will pick something _at least_ as good as C. This means
|
||||||
// that B will be -11 or worse. -11 is definitly worse than -10, so playing B is definitly
|
// that B will be -11 or worse. -11 is definitly worse than -10, so playing B is definitly
|
||||||
// a very bad idea, no matter the value of D. So don't even bother calculating the value of D
|
// a very bad idea, no matter the value of D. So don't even bother calculating the value of D
|
||||||
// and just break out.
|
// and just break out.
|
||||||
if max_value >= beta {
|
if max_value >= beta {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,3 +115,56 @@ impl<G: Game> GamePlayer<G> for PerfectPlayer<G> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::assert_win_ratio;
|
||||||
|
use crate::connect4::board::Connect4;
|
||||||
|
use crate::minmax::PerfectPlayer;
|
||||||
|
|
||||||
|
use crate::player::{GreedyPlayer, RandomPlayer};
|
||||||
|
use crate::tic_tac_toe::TicTacToe;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perfect_always_beats_greedy() {
|
||||||
|
assert_win_ratio::<TicTacToe, _, _>(1, 1.0, || PerfectPlayer::new(false), || GreedyPlayer);
|
||||||
|
assert_win_ratio::<Connect4, _, _>(
|
||||||
|
1,
|
||||||
|
1.0,
|
||||||
|
|| PerfectPlayer::new(false).with_max_depth(Some(8)),
|
||||||
|
|| GreedyPlayer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perfect_beats_random() {
|
||||||
|
assert_win_ratio::<TicTacToe, _, _>(
|
||||||
|
10,
|
||||||
|
0.95,
|
||||||
|
|| PerfectPlayer::new(false),
|
||||||
|
|| RandomPlayer,
|
||||||
|
);
|
||||||
|
assert_win_ratio::<Connect4, _, _>(
|
||||||
|
5,
|
||||||
|
0.95,
|
||||||
|
|| PerfectPlayer::new(false).with_max_depth(Some(7)),
|
||||||
|
|| RandomPlayer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn good_beat_bad() {
|
||||||
|
assert_win_ratio::<TicTacToe, _, _>(
|
||||||
|
1,
|
||||||
|
1.0,
|
||||||
|
|| PerfectPlayer::new(false).with_max_depth(Some(7)),
|
||||||
|
|| PerfectPlayer::new(false).with_max_depth(Some(5)),
|
||||||
|
);
|
||||||
|
assert_win_ratio::<Connect4, _, _>(
|
||||||
|
1,
|
||||||
|
1.0,
|
||||||
|
|| PerfectPlayer::new(false).with_max_depth(Some(7)),
|
||||||
|
|| PerfectPlayer::new(false).with_max_depth(Some(5)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,8 +138,13 @@ impl Game for TicTacToe {
|
||||||
TicTacToe::result(self)
|
TicTacToe::result(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rate(&self, _: Player) -> Score {
|
fn rate(&self, player: Player) -> Score {
|
||||||
unimplemented!("we always finish the board")
|
match self.result() {
|
||||||
|
State::Winner(winner) if player == winner => Score::WON,
|
||||||
|
State::Winner(_) => Score::LOST,
|
||||||
|
State::InProgress => Score::TIE,
|
||||||
|
State::Draw => Score::TIE,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_move(&mut self, position: Self::Move, player: Player) {
|
fn make_move(&mut self, position: Self::Move, player: Player) {
|
||||||
|
|
|
||||||
|
|
@ -3,41 +3,3 @@ mod game;
|
||||||
mod player;
|
mod player;
|
||||||
|
|
||||||
pub use {board::TicTacToe, player::*};
|
pub use {board::TicTacToe, player::*};
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{minmax::PerfectPlayer, tic_tac_toe::board::TicTacToe, GamePlayer, Player};
|
|
||||||
|
|
||||||
use crate::player::{GreedyPlayer, RandomPlayer};
|
|
||||||
|
|
||||||
fn assert_win_ratio<X: GamePlayer<TicTacToe>, O: GamePlayer<TicTacToe>>(
|
|
||||||
runs: u64,
|
|
||||||
x_win_ratio: f64,
|
|
||||||
x: impl Fn() -> X,
|
|
||||||
o: impl Fn() -> O,
|
|
||||||
) {
|
|
||||||
let mut results = [0u64, 0, 0];
|
|
||||||
|
|
||||||
for _ in 0..runs {
|
|
||||||
let result = TicTacToe::empty().play::<X, O>(&mut x(), &mut 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(1, 1.0, || PerfectPlayer::new(false), || GreedyPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn perfect_beats_random() {
|
|
||||||
assert_win_ratio(10, 0.95, || PerfectPlayer::new(false), || RandomPlayer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue