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::{
|
||||
state::{position_as_int, Position},
|
||||
state::{position_as_int, IgnorePlayer, Position},
|
||||
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]
|
||||
const WIN_COUNT_TABLE: [i16; BOARD_POSITIONS] = [
|
||||
3, 4, 6, 7, 6, 4, 3,
|
||||
|
|
@ -117,7 +117,9 @@ impl Connect4 {
|
|||
.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 {
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -215,7 +217,7 @@ impl Display for Connect4 {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Player, Score, State};
|
||||
use crate::{Player, Score, State, state::IgnorePlayer};
|
||||
|
||||
use super::Connect4;
|
||||
|
||||
|
|
@ -244,7 +246,7 @@ mod tests {
|
|||
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);
|
||||
assert_eq!(board.rate(player), score);
|
||||
}
|
||||
|
|
@ -298,7 +300,7 @@ mod tests {
|
|||
______X
|
||||
",
|
||||
Player::X,
|
||||
Score(3),
|
||||
Score::new(3),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -312,7 +314,7 @@ mod tests {
|
|||
___X___
|
||||
",
|
||||
Player::X,
|
||||
Score(7),
|
||||
Score::new(7),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -326,7 +328,7 @@ mod tests {
|
|||
O__X___
|
||||
",
|
||||
Player::X,
|
||||
Score(4),
|
||||
Score::new(4),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +342,7 @@ mod tests {
|
|||
O_____X
|
||||
",
|
||||
Player::X,
|
||||
Score(0),
|
||||
Score::new(0),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
try_trait_v2,
|
||||
return_position_impl_trait_in_trait,
|
||||
let_chains,
|
||||
adt_const_params
|
||||
)]
|
||||
#![allow(incomplete_features)]
|
||||
|
||||
|
|
@ -15,6 +14,8 @@ pub mod tic_tac_toe;
|
|||
|
||||
use std::fmt::Display;
|
||||
|
||||
use state::IgnorePlayer;
|
||||
|
||||
pub use self::minmax::PerfectPlayer;
|
||||
pub use self::state::{Player, Score, State};
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ pub trait Game: Display {
|
|||
fn result(&self) -> State;
|
||||
|
||||
/// 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{Game, GamePlayer, Player, Score, State};
|
||||
use crate::{
|
||||
state::{GoodPlayer, MinmaxPlayer},
|
||||
Game, GamePlayer, Player, Score, State,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PerfectPlayer<G: Game> {
|
||||
|
|
@ -36,30 +39,30 @@ impl<G: Game> PerfectPlayer<G> {
|
|||
self.best_move.expect("no move made yet")
|
||||
}
|
||||
|
||||
fn minmax(
|
||||
fn minmax<P: MinmaxPlayer>(
|
||||
&mut self,
|
||||
board: &mut G,
|
||||
maximizing_player: Player,
|
||||
grandparents_favourite_child_alpha: Score,
|
||||
parents_favourite_child_beta: Score,
|
||||
grandparents_favourite_child_alpha: Score<P>,
|
||||
parents_favourite_child_beta: Score<P>,
|
||||
depth: usize,
|
||||
) -> Score {
|
||||
) -> Score<P> {
|
||||
// FIXME: Make depth decrease not increase.
|
||||
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() {
|
||||
State::Winner(winner) => {
|
||||
if winner == maximizing_player {
|
||||
// Our maximizing player wins the game, so this node is a win for it.
|
||||
Score::WON
|
||||
Score::WON.for_player::<P>()
|
||||
} else {
|
||||
// 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 => {
|
||||
// The board isn't done yet, go deeper!
|
||||
// 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
|
||||
// / \
|
||||
// X C(11) D( )
|
||||
let value = -self.minmax(
|
||||
let value = -self.minmax::<P::Enemy>(
|
||||
board,
|
||||
// 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.
|
||||
|
|
@ -130,7 +133,13 @@ impl<G: Game> GamePlayer<G> for PerfectPlayer<G> {
|
|||
self.best_move = None;
|
||||
|
||||
// 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(
|
||||
self.best_move
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::{
|
||||
fmt::{Display, Debug},
|
||||
ops::{ControlFlow, Try, Neg},
|
||||
fmt::{Debug, Display},
|
||||
marker::PhantomData,
|
||||
ops::{ControlFlow, Neg, Try},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -96,17 +97,44 @@ impl Try for State {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Score(pub i32);
|
||||
// This fun generic setup ensures that we never compare two scores from different layers.
|
||||
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.
|
||||
pub const LOST: Self = Self(i32::MIN + 1);
|
||||
pub const TIE: Self = Self(0);
|
||||
pub const WON: Self = Self(i32::MAX);
|
||||
pub const LOST: Self = Self(i32::MIN + 1, PhantomData);
|
||||
pub const TIE: Self = Self(0, PhantomData);
|
||||
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 {
|
||||
Self(int)
|
||||
Self(int, PhantomData)
|
||||
}
|
||||
|
||||
pub fn ignore_side(self) -> Score<IgnorePlayer> {
|
||||
Score(self.0, PhantomData)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
|
|
@ -117,20 +145,51 @@ impl Score {
|
|||
}
|
||||
}
|
||||
|
||||
impl Neg for Score {
|
||||
type Output = Self;
|
||||
impl<P: MinmaxPlayer> Neg for Score<P> {
|
||||
type Output = Score<P::Enemy>;
|
||||
|
||||
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 {
|
||||
match *self {
|
||||
Self::WON => f.write_str("WON"),
|
||||
Self::LOST => f.write_str("LOST"),
|
||||
Self(other) => Debug::fmt(&other, f),
|
||||
// this should be a match but StructuralEq does not like me
|
||||
if self.ignore_side() == Score::WON {
|
||||
f.write_str("WON")
|
||||
} 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 crate::{Game, Player, Score, State};
|
||||
use crate::{Game, Player, Score, State, state::IgnorePlayer};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TicTacToe(u32);
|
||||
|
|
@ -138,7 +138,7 @@ impl Game for TicTacToe {
|
|||
TicTacToe::result(self)
|
||||
}
|
||||
|
||||
fn rate(&self, player: Player) -> Score {
|
||||
fn rate(&self, player: Player) -> Score<IgnorePlayer> {
|
||||
match self.result() {
|
||||
State::Winner(winner) if player == winner => Score::WON,
|
||||
State::Winner(_) => Score::LOST,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue