diff --git a/src/lib.rs b/src/lib.rs index bd91328..2ab2a4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,17 @@ -#![feature(never_type, try_trait_v2)] +#![feature(never_type, try_trait_v2, return_position_impl_trait_in_trait)] +#![allow(incomplete_features)] pub mod connect4; +mod minmax; pub mod tic_tac_toe; mod player; +use self::minmax::GameBoard; pub use player::{Player, State}; pub trait Game { - type Board; + type Board: GameBoard; } pub trait GamePlayer: Default { diff --git a/src/minmax.rs b/src/minmax.rs new file mode 100644 index 0000000..c342466 --- /dev/null +++ b/src/minmax.rs @@ -0,0 +1,93 @@ +use std::ops::Neg; + +use crate::{Game, GamePlayer, Player, State}; + +pub trait GameBoard { + type Move: Copy; + + fn possible_moves(&self) -> impl Iterator; + + fn result(&self) -> State; + + fn make_move(&mut self, position: Self::Move, player: Player); + + fn undo_move(&mut self, position: Self::Move); +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Score(i8); + +impl Score { + const LOST: Self = Self(i8::MIN); + const TIE: Self = Self(0); + const WON: Self = Self(i8::MAX); +} + +impl Neg for Score { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(-self.0) + } +} + +#[derive(Clone)] +pub struct PerfectPlayer { + best_move: Option, +} + +impl Default for PerfectPlayer { + fn default() -> Self { + Self::new() + } +} + +impl PerfectPlayer { + pub fn new() -> Self { + Self { best_move: None } + } + + fn minmax(&mut self, board: &mut B, player: Player, depth: usize) -> Score { + if depth < 2 { + //print!("{board}{}| playing {player}: ", " ".repeat(depth)); + } + match board.result() { + State::Winner(winner) => { + if winner == player { + Score::WON + } else { + Score::LOST + } + } + State::Draw => Score::TIE, + State::InProgress => { + let mut max_value = Score::LOST; + + for pos in board.possible_moves() { + board.make_move(pos, player); + let value = -self.minmax(board, player.opponent(), depth + 1); + + board.undo_move(pos); + + if value > max_value { + max_value = value; + if depth == 0 { + self.best_move = Some(pos); + } + } + } + + max_value + } + } + } +} + +impl GamePlayer for PerfectPlayer { + fn next_move(&mut self, board: &mut G::Board, this_player: Player) { + self.best_move = None; + self.minmax(board, this_player, 0); + + board.make_move(self.best_move.expect("could not make move"), this_player); + } +} diff --git a/src/tic_tac_toe/board.rs b/src/tic_tac_toe/board.rs index f4a07ad..166109e 100644 --- a/src/tic_tac_toe/board.rs +++ b/src/tic_tac_toe/board.rs @@ -1,6 +1,6 @@ use std::fmt::{Display, Write}; -use crate::{Player, State}; +use crate::{minmax::GameBoard, Player, State}; #[derive(Clone)] pub struct Board(u32); @@ -113,6 +113,34 @@ impl Display for Board { } } +impl GameBoard for Board { + type Move = usize; + + fn possible_moves(&self) -> impl Iterator { + debug_assert!( + !self.iter().all(|x| x.is_some()), + "the board is full but state is InProgress" + ); + + self.iter() + .enumerate() + .filter(|(_, position)| position.is_none()) + .map(|(pos, _)| pos) + } + + fn result(&self) -> State { + Board::result(self) + } + + fn make_move(&mut self, position: Self::Move, player: Player) { + self.set(position, Some(player)); + } + + fn undo_move(&mut self, position: Self::Move) { + self.set(position, None); + } +} + #[cfg(test)] mod tests { use super::{Board, Player};