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::{ 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),
) )
} }
} }

View file

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

View file

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

View file

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

View file

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