make the missing - a type error. because it's funny

This commit is contained in:
nora 2023-01-17 21:02:34 +01:00
parent 0a164540e5
commit 8a87691e10
5 changed files with 113 additions and 42 deletions

View file

@ -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),
)
}
}

View file

@ -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);

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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,