mirror of
https://github.com/Noratrieb/minmax.git
synced 2026-01-14 15:25:08 +01:00
make the missing - a type error. because it's funny
This commit is contained in:
parent
0a164540e5
commit
8a87691e10
5 changed files with 113 additions and 42 deletions
|
|
@ -4,7 +4,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
state::{position_as_int, Position},
|
state::{position_as_int, IgnorePlayer, Position},
|
||||||
Game, Player, Score, State,
|
Game, Player, Score, State,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ impl Connect4 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rate(&self, player: Player) -> Score {
|
fn rate(&self, player: Player) -> Score<IgnorePlayer> {
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
const WIN_COUNT_TABLE: [i16; BOARD_POSITIONS] = [
|
const WIN_COUNT_TABLE: [i16; BOARD_POSITIONS] = [
|
||||||
3, 4, 6, 7, 6, 4, 3,
|
3, 4, 6, 7, 6, 4, 3,
|
||||||
|
|
@ -117,7 +117,9 @@ impl Connect4 {
|
||||||
.sum::<i16>()
|
.sum::<i16>()
|
||||||
};
|
};
|
||||||
|
|
||||||
Score::new(i32::from(score_player(player) - score_player(player.opponent())))
|
Score::new(i32::from(
|
||||||
|
score_player(player) - score_player(player.opponent()),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn drop_player(&self, position: usize) -> usize {
|
pub fn drop_player(&self, position: usize) -> usize {
|
||||||
|
|
@ -185,7 +187,7 @@ impl Game for Connect4 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rate(&self, player: Player) -> Score {
|
fn rate(&self, player: Player) -> Score<IgnorePlayer> {
|
||||||
Connect4::rate(&self, player)
|
Connect4::rate(&self, player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +217,7 @@ impl Display for Connect4 {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{Player, Score, State};
|
use crate::{Player, Score, State, state::IgnorePlayer};
|
||||||
|
|
||||||
use super::Connect4;
|
use super::Connect4;
|
||||||
|
|
||||||
|
|
@ -244,7 +246,7 @@ mod tests {
|
||||||
assert_eq!(board.result(), state);
|
assert_eq!(board.result(), state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_rate(board: &str, player: Player, score: Score) {
|
fn test_rate(board: &str, player: Player, score: Score<IgnorePlayer>) {
|
||||||
let board = parse_board(board);
|
let board = parse_board(board);
|
||||||
assert_eq!(board.rate(player), score);
|
assert_eq!(board.rate(player), score);
|
||||||
}
|
}
|
||||||
|
|
@ -298,7 +300,7 @@ mod tests {
|
||||||
______X
|
______X
|
||||||
",
|
",
|
||||||
Player::X,
|
Player::X,
|
||||||
Score(3),
|
Score::new(3),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,7 +314,7 @@ mod tests {
|
||||||
___X___
|
___X___
|
||||||
",
|
",
|
||||||
Player::X,
|
Player::X,
|
||||||
Score(7),
|
Score::new(7),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,7 +328,7 @@ mod tests {
|
||||||
O__X___
|
O__X___
|
||||||
",
|
",
|
||||||
Player::X,
|
Player::X,
|
||||||
Score(4),
|
Score::new(4),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,7 +342,7 @@ mod tests {
|
||||||
O_____X
|
O_____X
|
||||||
",
|
",
|
||||||
Player::X,
|
Player::X,
|
||||||
Score(0),
|
Score::new(0),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
try_trait_v2,
|
try_trait_v2,
|
||||||
return_position_impl_trait_in_trait,
|
return_position_impl_trait_in_trait,
|
||||||
let_chains,
|
let_chains,
|
||||||
adt_const_params
|
|
||||||
)]
|
)]
|
||||||
#![allow(incomplete_features)]
|
#![allow(incomplete_features)]
|
||||||
|
|
||||||
|
|
@ -15,6 +14,8 @@ pub mod tic_tac_toe;
|
||||||
|
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use state::IgnorePlayer;
|
||||||
|
|
||||||
pub use self::minmax::PerfectPlayer;
|
pub use self::minmax::PerfectPlayer;
|
||||||
pub use self::state::{Player, Score, State};
|
pub use self::state::{Player, Score, State};
|
||||||
|
|
||||||
|
|
@ -47,7 +48,7 @@ pub trait Game: Display {
|
||||||
fn result(&self) -> State;
|
fn result(&self) -> State;
|
||||||
|
|
||||||
/// Only called if [`GameBoard::REASONABLE_SEARCH_DEPTH`] is `Some`.
|
/// Only called if [`GameBoard::REASONABLE_SEARCH_DEPTH`] is `Some`.
|
||||||
fn rate(&self, player: Player) -> Score;
|
fn rate(&self, player: Player) -> Score<IgnorePlayer>;
|
||||||
|
|
||||||
fn make_move(&mut self, position: Self::Move, player: Player);
|
fn make_move(&mut self, position: Self::Move, player: Player);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::{Game, GamePlayer, Player, Score, State};
|
use crate::{
|
||||||
|
state::{GoodPlayer, MinmaxPlayer},
|
||||||
|
Game, GamePlayer, Player, Score, State,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PerfectPlayer<G: Game> {
|
pub struct PerfectPlayer<G: Game> {
|
||||||
|
|
@ -36,30 +39,30 @@ impl<G: Game> PerfectPlayer<G> {
|
||||||
self.best_move.expect("no move made yet")
|
self.best_move.expect("no move made yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn minmax(
|
fn minmax<P: MinmaxPlayer>(
|
||||||
&mut self,
|
&mut self,
|
||||||
board: &mut G,
|
board: &mut G,
|
||||||
maximizing_player: Player,
|
maximizing_player: Player,
|
||||||
grandparents_favourite_child_alpha: Score,
|
grandparents_favourite_child_alpha: Score<P>,
|
||||||
parents_favourite_child_beta: Score,
|
parents_favourite_child_beta: Score<P>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
) -> Score {
|
) -> Score<P> {
|
||||||
// FIXME: Make depth decrease not increase.
|
// FIXME: Make depth decrease not increase.
|
||||||
if let Some(max_depth) = self.max_depth && depth >= max_depth {
|
if let Some(max_depth) = self.max_depth && depth >= max_depth {
|
||||||
return board.rate(maximizing_player);
|
return board.rate(maximizing_player).for_player::<P>();
|
||||||
}
|
}
|
||||||
|
|
||||||
match board.result() {
|
match board.result() {
|
||||||
State::Winner(winner) => {
|
State::Winner(winner) => {
|
||||||
if winner == maximizing_player {
|
if winner == maximizing_player {
|
||||||
// Our maximizing player wins the game, so this node is a win for it.
|
// Our maximizing player wins the game, so this node is a win for it.
|
||||||
Score::WON
|
Score::WON.for_player::<P>()
|
||||||
} else {
|
} else {
|
||||||
// The maximizing player lost the board here, so the node is a loss.
|
// The maximizing player lost the board here, so the node is a loss.
|
||||||
Score::LOST
|
Score::LOST.for_player::<P>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Draw => Score::TIE,
|
State::Draw => Score::TIE.for_player::<P>(),
|
||||||
State::InProgress => {
|
State::InProgress => {
|
||||||
// The board isn't done yet, go deeper!
|
// The board isn't done yet, go deeper!
|
||||||
// The alpha is the favourite (highest reward) child of our grandparent (who's on our side!).
|
// The alpha is the favourite (highest reward) child of our grandparent (who's on our side!).
|
||||||
|
|
@ -77,7 +80,7 @@ impl<G: Game> PerfectPlayer<G> {
|
||||||
// O A(10) B( ) <- we are here in the loop and about to call D
|
// O A(10) B( ) <- we are here in the loop and about to call D
|
||||||
// / \
|
// / \
|
||||||
// X C(11) D( )
|
// X C(11) D( )
|
||||||
let value = -self.minmax(
|
let value = -self.minmax::<P::Enemy>(
|
||||||
board,
|
board,
|
||||||
// The player that will maximize this round is now the opponent. This layer it was our original
|
// The player that will maximize this round is now the opponent. This layer it was our original
|
||||||
// opponent, O, but in the nested round it's X's turn again so they will try to maximize their score.
|
// opponent, O, but in the nested round it's X's turn again so they will try to maximize their score.
|
||||||
|
|
@ -130,7 +133,13 @@ impl<G: Game> GamePlayer<G> for PerfectPlayer<G> {
|
||||||
self.best_move = None;
|
self.best_move = None;
|
||||||
|
|
||||||
// Get the rating for the one move we will make.
|
// Get the rating for the one move we will make.
|
||||||
self.minmax(board, this_player, Score::LOST, Score::WON, 0);
|
self.minmax::<GoodPlayer>(
|
||||||
|
board,
|
||||||
|
this_player,
|
||||||
|
Score::LOST.for_player::<GoodPlayer>(),
|
||||||
|
Score::WON.for_player::<GoodPlayer>(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
board.make_move(
|
board.make_move(
|
||||||
self.best_move
|
self.best_move
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{Display, Debug},
|
fmt::{Debug, Display},
|
||||||
ops::{ControlFlow, Try, Neg},
|
marker::PhantomData,
|
||||||
|
ops::{ControlFlow, Neg, Try},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
@ -96,17 +97,44 @@ impl Try for State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
// This fun generic setup ensures that we never compare two scores from different layers.
|
||||||
pub struct Score(pub i32);
|
pub struct Score<P>(pub i32, PhantomData<P>);
|
||||||
|
|
||||||
impl Score {
|
pub trait MinmaxPlayer {
|
||||||
|
type Enemy: MinmaxPlayer<Enemy = Self>;
|
||||||
|
}
|
||||||
|
pub struct GoodPlayer;
|
||||||
|
pub struct EvilPlayer;
|
||||||
|
pub struct IgnorePlayer;
|
||||||
|
|
||||||
|
impl MinmaxPlayer for GoodPlayer {
|
||||||
|
type Enemy = EvilPlayer;
|
||||||
|
}
|
||||||
|
impl MinmaxPlayer for EvilPlayer {
|
||||||
|
type Enemy = GoodPlayer;
|
||||||
|
}
|
||||||
|
impl MinmaxPlayer for IgnorePlayer {
|
||||||
|
type Enemy = Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Score<IgnorePlayer> {
|
||||||
// Due to the nature of two's completement, we can't actually negate this properly, so add 1.
|
// Due to the nature of two's completement, we can't actually negate this properly, so add 1.
|
||||||
pub const LOST: Self = Self(i32::MIN + 1);
|
pub const LOST: Self = Self(i32::MIN + 1, PhantomData);
|
||||||
pub const TIE: Self = Self(0);
|
pub const TIE: Self = Self(0, PhantomData);
|
||||||
pub const WON: Self = Self(i32::MAX);
|
pub const WON: Self = Self(i32::MAX, PhantomData);
|
||||||
|
|
||||||
|
pub fn for_player<P>(self) -> Score<P> {
|
||||||
|
Score(self.0, PhantomData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Score<P> {
|
||||||
pub fn new(int: i32) -> Self {
|
pub fn new(int: i32) -> Self {
|
||||||
Self(int)
|
Self(int, PhantomData)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ignore_side(self) -> Score<IgnorePlayer> {
|
||||||
|
Score(self.0, PhantomData)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
@ -117,20 +145,51 @@ impl Score {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Neg for Score {
|
impl<P: MinmaxPlayer> Neg for Score<P> {
|
||||||
type Output = Self;
|
type Output = Score<P::Enemy>;
|
||||||
|
|
||||||
fn neg(self) -> Self::Output {
|
fn neg(self) -> Self::Output {
|
||||||
Self(-self.0)
|
Score(-self.0, PhantomData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Score {
|
impl<P> PartialEq for Score<P> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.0 == other.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Eq for Score<P> {}
|
||||||
|
|
||||||
|
impl<P> PartialOrd for Score<P> {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
self.0.partial_cmp(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Ord for Score<P> {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.0.cmp(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Clone for Score<P> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Copy for Score<P> {}
|
||||||
|
|
||||||
|
impl<P> Debug for Score<P> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match *self {
|
// this should be a match but StructuralEq does not like me
|
||||||
Self::WON => f.write_str("WON"),
|
if self.ignore_side() == Score::WON {
|
||||||
Self::LOST => f.write_str("LOST"),
|
f.write_str("WON")
|
||||||
Self(other) => Debug::fmt(&other, f),
|
} else if Score::LOST == self.ignore_side() {
|
||||||
|
f.write_str("LOST")
|
||||||
|
} else {
|
||||||
|
Debug::fmt(&self.0, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::fmt::{Display, Write};
|
use std::fmt::{Display, Write};
|
||||||
|
|
||||||
use crate::{Game, Player, Score, State};
|
use crate::{Game, Player, Score, State, state::IgnorePlayer};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TicTacToe(u32);
|
pub struct TicTacToe(u32);
|
||||||
|
|
@ -138,7 +138,7 @@ impl Game for TicTacToe {
|
||||||
TicTacToe::result(self)
|
TicTacToe::result(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rate(&self, player: Player) -> Score {
|
fn rate(&self, player: Player) -> Score<IgnorePlayer> {
|
||||||
match self.result() {
|
match self.result() {
|
||||||
State::Winner(winner) if player == winner => Score::WON,
|
State::Winner(winner) if player == winner => Score::WON,
|
||||||
State::Winner(_) => Score::LOST,
|
State::Winner(_) => Score::LOST,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue