From 8a87691e107a8c62a97665097e3e4f65e9383a3b Mon Sep 17 00:00:00 2001 From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com> Date: Tue, 17 Jan 2023 21:02:34 +0100 Subject: [PATCH] make the missing `-` a type error. because it's funny --- minmax-rs/src/connect4/board.rs | 22 +++---- minmax-rs/src/lib.rs | 5 +- minmax-rs/src/minmax.rs | 31 ++++++---- minmax-rs/src/state.rs | 93 ++++++++++++++++++++++++------ minmax-rs/src/tic_tac_toe/board.rs | 4 +- 5 files changed, 113 insertions(+), 42 deletions(-) diff --git a/minmax-rs/src/connect4/board.rs b/minmax-rs/src/connect4/board.rs index 571e7fc..c9c4b39 100644 --- a/minmax-rs/src/connect4/board.rs +++ b/minmax-rs/src/connect4/board.rs @@ -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 { #[rustfmt::skip] const WIN_COUNT_TABLE: [i16; BOARD_POSITIONS] = [ 3, 4, 6, 7, 6, 4, 3, @@ -117,7 +117,9 @@ impl Connect4 { .sum::() }; - 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 { 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) { 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), ) } } diff --git a/minmax-rs/src/lib.rs b/minmax-rs/src/lib.rs index 5d40163..0739ca6 100644 --- a/minmax-rs/src/lib.rs +++ b/minmax-rs/src/lib.rs @@ -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; fn make_move(&mut self, position: Self::Move, player: Player); diff --git a/minmax-rs/src/minmax.rs b/minmax-rs/src/minmax.rs index 2468857..0239ba7 100644 --- a/minmax-rs/src/minmax.rs +++ b/minmax-rs/src/minmax.rs @@ -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 { @@ -36,30 +39,30 @@ impl PerfectPlayer { self.best_move.expect("no move made yet") } - fn minmax( + fn minmax( &mut self, board: &mut G, maximizing_player: Player, - grandparents_favourite_child_alpha: Score, - parents_favourite_child_beta: Score, + grandparents_favourite_child_alpha: Score

, + parents_favourite_child_beta: Score

, depth: usize, - ) -> Score { + ) -> Score

{ // 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::

(); } 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::

() } else { // The maximizing player lost the board here, so the node is a loss. - Score::LOST + Score::LOST.for_player::

() } } - State::Draw => Score::TIE, + State::Draw => Score::TIE.for_player::

(), 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 PerfectPlayer { // 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::( 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 GamePlayer for PerfectPlayer { 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::( + board, + this_player, + Score::LOST.for_player::(), + Score::WON.for_player::(), + 0, + ); board.make_move( self.best_move diff --git a/minmax-rs/src/state.rs b/minmax-rs/src/state.rs index e9e4a01..0c6a1b7 100644 --- a/minmax-rs/src/state.rs +++ b/minmax-rs/src/state.rs @@ -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

(pub i32, PhantomData

); -impl Score { +pub trait MinmaxPlayer { + type Enemy: MinmaxPlayer; +} +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 { // 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

(self) -> Score

{ + Score(self.0, PhantomData) + } +} + +impl

Score

{ pub fn new(int: i32) -> Self { - Self(int) + Self(int, PhantomData) + } + + pub fn ignore_side(self) -> Score { + Score(self.0, PhantomData) } #[allow(unused)] @@ -117,20 +145,51 @@ impl Score { } } -impl Neg for Score { - type Output = Self; +impl Neg for Score

{ + type Output = Score; fn neg(self) -> Self::Output { - Self(-self.0) + Score(-self.0, PhantomData) } } -impl Debug for Score { +impl

PartialEq for Score

{ + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl

Eq for Score

{} + +impl

PartialOrd for Score

{ + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} + +impl

Ord for Score

{ + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } +} + +impl

Clone for Score

{ + fn clone(&self) -> Self { + *self + } +} + +impl

Copy for Score

{} + +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), + // 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) } } } diff --git a/minmax-rs/src/tic_tac_toe/board.rs b/minmax-rs/src/tic_tac_toe/board.rs index aaa98ba..dbb6cff 100644 --- a/minmax-rs/src/tic_tac_toe/board.rs +++ b/minmax-rs/src/tic_tac_toe/board.rs @@ -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 { match self.result() { State::Winner(winner) if player == winner => Score::WON, State::Winner(_) => Score::LOST,