Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoDog896 committed Sep 22, 2024
1 parent fa37770 commit 4abb035
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 190 deletions.
144 changes: 36 additions & 108 deletions crates/game-solver/src/game.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
//! Game trait and related types.
/// Represents a player.
pub trait Player {
/// The max player count.
#[must_use]
fn count() -> usize;
/// The current index of this player starting at 0.
#[must_use]
fn idx(&self) -> usize;
/// The next player to play
#[must_use]
fn next(self) -> Self;
}
use crate::player::Player;

/// Represents a move outcome
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
Expand All @@ -25,67 +14,6 @@ pub enum GameState<P: Player> {
Win(P),
}

/// Represents a player in a zero-sum (2-player) game.
///
/// Allows for usage of `negamax` instead of minimax.
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
pub enum ZeroSumPlayer {
/// The first player.
One,
/// The second player.
Two,
}

impl Player for ZeroSumPlayer {
fn count() -> usize {
2
}

fn idx(&self) -> usize {
match self {
Self::One => 0,
Self::Two => 1,
}
}

fn next(self) -> Self {
match self {
ZeroSumPlayer::One => ZeroSumPlayer::Two,
ZeroSumPlayer::Two => ZeroSumPlayer::One,
}
}
}

/// Represents a player in an N-player game.
pub struct NPlayerConst<const N: usize>(usize);

impl<const N: usize> NPlayerConst<N> {
pub fn new(index: usize) -> NPlayerConst<N> {
assert!(index < N, "Player index {index} >= max player count {N}");
Self(index)
}

pub fn new_unchecked(index: usize) -> NPlayerConst<N> {
debug_assert!(index < N, "Player index {index} >= max player count {N}");
Self(index)
}
}

impl<const N: usize> Player for NPlayerConst<N> {
fn count() -> usize {
N
}

fn idx(&self) -> usize {
self.0
}

fn next(self) -> Self {
// This will always make index < N.
Self::new_unchecked((self.0 + 1) % N)
}
}

/// Represents a combinatorial game.
pub trait Game: Clone {
/// The type of move this game uses.
Expand All @@ -97,17 +25,50 @@ pub trait Game: Clone {
Self: 'a;

/// The type of player this game uses.
/// There are two types of players:
/// There are three types of players:
///
/// - [`ZeroSumPlayer`] for two-player zero-sum games.
/// - [`NPlayer`] for N-player games.
/// - [`NPlayer`] for N-player games where N > 2.
///
/// If your game is a two-player zero-sum game, using [`ZeroSumPlayer`]
/// allows `negamax` to be used instead of minimax.
type Player: Player;

type MoveError;

/// Returns the amount of moves that have been played
fn move_count(&self) -> usize;

/// Get the max number of moves in a game, if any.
fn max_moves(&self) -> Option<usize>;

/// Makes a move.
fn make_move(&mut self, m: &Self::Move) -> Result<(), Self::MoveError>;

/// Returns an iterator of all possible moves.
///
/// If possible, this function should "guess" what the best moves are first.
/// For example, if this is for tic tac toe, it should give the middle move first.
/// Since "better" moves would be found first, this permits more alpha/beta cutoffs.
fn possible_moves(&self) -> Self::Iter<'_>;

// TODO: fn is_immediately_resolvable instead - better optimization for unwinnable games
/// Returns the next state given a move.
///
/// This has a default implementation and is mainly useful for optimization -
/// this is used at every tree check and should be fast.
fn next_state(&self, m: &Self::Move) -> Result<GameState<Self::Player>, Self::MoveError> {
let mut new_self = self.clone();
new_self.make_move(m)?;
Ok(new_self.state())
}

/// Returns the current state of the game.
/// Used for verifying initialization and isn't commonly called.
fn state(&self) -> GameState<Self::Player>;
}

pub trait PartizanGame: Game {
/// Returns the player whose turn it is.
/// The implementation of this should be
/// similar to either
Expand Down Expand Up @@ -139,39 +100,6 @@ pub trait Game: Clone {
/// However, no implementation is provided
/// because this does not keep track of the move count.
fn player(&self) -> Self::Player;

// TODO: (move_count/max_moves) allow custom evaluation

/// Returns the amount of moves that have been played
fn move_count(&self) -> usize;

/// Get the max number of moves in a game, if any.
fn max_moves(&self) -> Option<usize>;

/// Makes a move.
fn make_move(&mut self, m: &Self::Move) -> Result<(), Self::MoveError>;

/// Returns an iterator of all possible moves.
///
/// If possible, this function should "guess" what the best moves are first.
/// For example, if this is for tic tac toe, it should give the middle move first.
/// Since "better" moves would be found first, this permits more alpha/beta cutoffs.
fn possible_moves(&self) -> Self::Iter<'_>;

// TODO: fn is_immediately_resolvable instead - better optimization for unwinnable games
/// Returns the next state given a move.
///
/// This has a default implementation and is mainly useful for optimization -
/// this is used at every tree check and should be fast.
fn next_state(&self, m: &Self::Move) -> Result<GameState<Self::Player>, Self::MoveError> {
let mut new_self = self.clone();
new_self.make_move(m)?;
Ok(new_self.state())
}

/// Returns the current state of the game.
/// Used for verifying initialization and isn't commonly called.
fn state(&self) -> GameState<Self::Player>;
}

/// Utility function to get the upper bound of a game.
Expand Down
4 changes: 3 additions & 1 deletion crates/game-solver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! a great place to start.
pub mod game;
pub mod player;
// TODO: reinforcement
// #[cfg(feature = "reinforcement")]
// pub mod reinforcement;
Expand All @@ -15,7 +16,8 @@ use std::hash::BuildHasher;

use game::{upper_bound, GameState};

use crate::game::{Game, ZeroSumPlayer};
use crate::game::Game;
use crate::player::ZeroSumPlayer;
use crate::transposition::{Score, TranspositionTable};
use std::hash::Hash;

Expand Down
105 changes: 105 additions & 0 deletions crates/game-solver/src/player.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/// Represents a player.
pub trait Player {
/// The max player count.
#[must_use]
fn count() -> usize;
/// The current index of this player starting at 0.
#[must_use]
fn idx(&self) -> usize;
/// The next player to play
#[must_use]
fn next(self) -> Self;
}

/// Represents a player in a zero-sum (2-player) game.
///
/// Allows for usage of `negamax` instead of minimax.
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
pub enum ZeroSumPlayer {
/// The first player.
Left,
/// The second player.
Right,
}

impl Player for ZeroSumPlayer {
fn count() -> usize {
2
}

fn idx(&self) -> usize {
match self {
Self::Left => 0,
Self::Right => 1,
}
}

fn next(self) -> Self {
match self {
Self::Left => Self::Right,
Self::Right => Self::Left,
}
}
}

/// Represents a player in a zero-sum (2-player) game,
/// where the game is impartial. That is,
/// a player does not affect the `Game::possible_moves` function.
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
pub enum ImpartialPlayer {
/// The player that will play on the current game state,
Next,
/// The player that has played previous to this game state
/// (or will play after Next).
Previous
}

impl Player for ImpartialPlayer {
fn count() -> usize {
2
}

fn idx(&self) -> usize {
match self {
Self::Next => 0,
Self::Previous => 1,
}
}

fn next(self) -> Self {
match self {
Self::Next => Self::Previous,
Self::Previous => Self::Next,
}
}
}

/// Represents a player in an N-player game.
pub struct NPlayerConst<const N: usize>(usize);

impl<const N: usize> NPlayerConst<N> {
pub fn new(index: usize) -> NPlayerConst<N> {
assert!(index < N, "Player index {index} >= max player count {N}");
Self(index)
}

pub fn new_unchecked(index: usize) -> NPlayerConst<N> {
debug_assert!(index < N, "Player index {index} >= max player count {N}");
Self(index)
}
}

impl<const N: usize> Player for NPlayerConst<N> {
fn count() -> usize {
N
}

fn idx(&self) -> usize {
self.0
}

fn next(self) -> Self {
// This will always make index < N.
Self::new_unchecked((self.0 + 1) % N)
}
}
20 changes: 11 additions & 9 deletions crates/games/src/chomp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pub mod gui;
use anyhow::Error;
use array2d::Array2D;
use clap::Args;
use game_solver::game::{Game, GameState, ZeroSumPlayer};
use game_solver::{game::{Game, GameState, PartizanGame}, player::ZeroSumPlayer};
use serde::{Deserialize, Serialize};
use thiserror::Error;

Expand Down Expand Up @@ -74,6 +74,16 @@ pub enum ChompMoveError {

pub type ChompMove = NaturalMove<2>;

impl PartizanGame for Chomp {
fn player(&self) -> Self::Player {
if self.move_count % 2 == 0 {
ZeroSumPlayer::Left
} else {
ZeroSumPlayer::Right
}
}
}

impl Game for Chomp {
type Move = ChompMove;
type Iter<'a> = std::vec::IntoIter<Self::Move>;
Expand All @@ -84,14 +94,6 @@ impl Game for Chomp {
Some(self.width * self.height)
}

fn player(&self) -> Self::Player {
if self.move_count % 2 == 0 {
ZeroSumPlayer::One
} else {
ZeroSumPlayer::Two
}
}

fn move_count(&self) -> usize {
self.move_count
}
Expand Down
Loading

0 comments on commit 4abb035

Please sign in to comment.