Skip to content

Commit

Permalink
feat
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoDog896 committed Sep 22, 2024
1 parent 4abb035 commit 1e75354
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 48 deletions.
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 79 additions & 20 deletions crates/game-solver/src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,33 @@ pub enum GameState<P: Player> {
Win(P),
}

enum Partiality {
Impartial,
Partizan
}

enum StateType {
Normal,
Misere
}

impl StateType {
fn state<T>(&self, game: &T) -> GameState<T::Player>
where T: Game
{
match self {

}
}
}

/// Represents a combinatorial game.
///
/// A game has three distinct variants per game:
///
/// - Game play type: Normal, Misere, Other
/// - Game partiality type: Impartial, Partizan
/// - Game player count: >0
pub trait Game: Clone {
/// The type of move this game uses.
type Move: Clone;
Expand All @@ -24,17 +50,12 @@ pub trait Game: Clone {
where
Self: 'a;

/// The type of player this game uses.
/// There are three types of players:
///
/// - [`ZeroSumPlayer`] for two-player zero-sum 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 MoveError;

type Player: Player;

type MoveError;
fn partiality() -> Partiality;
fn state_type() -> Option<StateType>;

/// Returns the amount of moves that have been played
fn move_count(&self) -> usize;
Expand All @@ -52,23 +73,26 @@ pub trait Game: Clone {
/// 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 a reachable game in one move.
///
/// Rather, this function asks if there exists some game in the possible games set
/// which has a resolvable, positive or negative, outcome.
fn find_immediately_resolvable_game(&self) -> Result<Option<Self>, Self::MoveError> {
for m in &mut self.possible_moves() {
let mut new_self = self.clone();
new_self.make_move(&m)?;
if let GameState::Win(_) = new_self.state() {
return Ok(Some(new_self));
}
}

Ok(None)
}

/// 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 @@ -102,6 +126,41 @@ pub trait PartizanGame: Game {
fn player(&self) -> Self::Player;
}

/// A partizan game is a game,
/// where the possible_moves is affected
/// by which player's turn it is.
pub trait PartizanGame: Game {}

pub trait ImpartialGame: Game {}

pub trait NormalGame: Game {
/// Returns the current state of the game.
/// Used for verifying initialization and isn't commonly called.
///
/// This is a default implementation of [Game::state]
fn state_impl(&self) -> GameState<Self::Player> {
if self.possible_moves().next().is_none() {
GameState::Win(self.player().previous())
} else {
GameState::Playable
}
}
}

pub trait MisereGame: Game {
/// Returns the current state of the game.
/// Used for verifying initialization and isn't commonly called.
///
/// This is a default implementation of [Game::state]
fn state_impl(&self) -> GameState<Self::Player> {
if self.possible_moves().next().is_none() {
GameState::Win(self.player())
} else {
GameState::Playable
}
}
}

/// Utility function to get the upper bound of a game.
pub fn upper_bound<T: Game>(game: &T) -> isize {
game.max_moves().map_or(isize::MAX, |m| m as isize)
Expand Down
10 changes: 2 additions & 8 deletions crates/game-solver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,8 @@ fn negamax<T: Game<Player = ZeroSumPlayer> + Eq + Hash>(
};

// check if this is a winning configuration
// TODO: allow overloading of this - some kind of game.can_win_next()
for m in &mut game.possible_moves() {
// TODO: ties?
if let GameState::Win(_) = game.next_state(&m)? {
let mut board = game.clone();
board.make_move(&m)?;
return Ok(upper_bound(&board) - game.move_count() as isize - 1);
}
if let Ok(Some(board)) = game.find_immediately_resolvable_game() {
return Ok(upper_bound(&board) - board.move_count() as isize - 1);
}

// fetch values from the transposition table
Expand Down
21 changes: 20 additions & 1 deletion crates/game-solver/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ pub trait Player {
/// The next player to play
#[must_use]
fn next(self) -> Self;
/// The previous player to play
#[must_use]
fn previous(self) -> Self;
}

/// Represents a player in a zero-sum (2-player) game.
Expand Down Expand Up @@ -40,6 +43,10 @@ impl Player for ZeroSumPlayer {
Self::Right => Self::Left,
}
}

fn previous(self) -> Self {
self.next()
}
}

/// Represents a player in a zero-sum (2-player) game,
Expand Down Expand Up @@ -72,6 +79,10 @@ impl Player for ImpartialPlayer {
Self::Previous => Self::Next,
}
}

fn previous(self) -> Self {
self.next()
}
}

/// Represents a player in an N-player game.
Expand Down Expand Up @@ -102,4 +113,12 @@ impl<const N: usize> Player for NPlayerConst<N> {
// This will always make index < N.
Self::new_unchecked((self.0 + 1) % N)
}
}

fn previous(self) -> Self {
if self.0 == 0 {
Self::new_unchecked(N - 1)
} else {
Self::new_unchecked(self.0 - 1)
}
}
}
2 changes: 2 additions & 0 deletions crates/games/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ once_cell = "1.19.0"
egui = { version = "0.28", optional = true }
egui_commonmark = { version = "0.17.0", optional = true, features = ["macros"] }
thiserror = "1.0.63"
petgraph = "0.6.5"
castaway = "0.2.3"

[features]
"egui" = ["dep:egui", "dep:egui_commonmark"]
18 changes: 4 additions & 14 deletions crates/games/src/chomp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,6 @@ 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 Down Expand Up @@ -124,11 +114,11 @@ impl Game for Chomp {
moves.into_iter()
}

fn state(&self) -> GameState<Self::Player> {
if self.possible_moves().len() == 0 {
GameState::Win(self.player())
fn player(&self) -> Self::Player {
if self.move_count % 2 == 0 {
ZeroSumPlayer::Left
} else {
GameState::Playable
ZeroSumPlayer::Right
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/games/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod nim;
pub mod order_and_chaos;
pub mod reversi;
pub mod tic_tac_toe;
pub mod sprouts;

use crate::{
chomp::ChompArgs, domineering::DomineeringArgs, nim::NimArgs,
Expand Down
6 changes: 6 additions & 0 deletions crates/games/src/sprouts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Sprouts is a two-player impartial game that is represented by an unlabeled, undirected graph.

We introduce a generalization of sprouts (n, k), where n is the number of starting sprouts,
and k is the max amount of connections per sprout. The default variant is (n, 3).

More information: <https://en.wikipedia.org/wiki/Sprouts_(game)>
6 changes: 6 additions & 0 deletions crates/games/src/sprouts/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#![doc = include_str!("./README.md")]

use petgraph::matrix_graph::MatrixGraph;

#[derive(Clone)]
pub struct Sprouts(MatrixGraph<(), ()>);
13 changes: 8 additions & 5 deletions crates/games/src/util/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ use anyhow::{anyhow, Result};
use core::hash::Hash;
use game_solver::{
game::{Game, GameState},
par_move_scores,
par_move_scores, player::{Player, ZeroSumPlayer},
};
use std::fmt::{Debug, Display};
use std::{any::Any, fmt::{Debug, Display}};

pub fn play<T>(game: T)
pub fn play<T, P>(game: T)
where
T: Game<Player = ZeroSumPlayer> + Eq + Hash + Sync + Send + Display + 'static,
P: Player,
T: Game<Player = P> + Eq + Hash + Sync + Send + Display + 'static,
T::Move: Sync + Send + Display,
T::MoveError: Sync + Send + Debug,
{
Expand All @@ -17,7 +18,9 @@ where

match game.state() {
GameState::Playable => {
println!("Player {:?} to move", game.player());
let game_any = &game as &dyn Any;

if let Some(partizan)

let move_scores = par_move_scores(&game);

Check failure on line 25 in crates/games/src/util/cli/mod.rs

View workflow job for this annotation

GitHub Actions / format

expected one of `=` or `|`, found keyword `let`
let mut move_scores = move_scores
Expand Down
9 changes: 9 additions & 0 deletions rust-toolchain
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# If you see this, run "rustup self update" to get rustup 1.23 or newer.

# NOTE: above comment is for older `rustup` (before TOML support was added),
# which will treat the first line as the toolchain name, and therefore show it
# to the user in the error, instead of "error: invalid channel name '[toolchain]'".

[toolchain]
channel = "1.76"
components = [ "rustfmt", "clippy" ]

0 comments on commit 1e75354

Please sign in to comment.