From 1e75354f8cfaf8f2ec9d4ff75ab290345af2168e Mon Sep 17 00:00:00 2001 From: "Tristan F." <26509014+LeoDog896@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:56:11 -0700 Subject: [PATCH] feat --- Cargo.lock | 33 ++++++++++ crates/game-solver/src/game.rs | 99 ++++++++++++++++++++++++------ crates/game-solver/src/lib.rs | 10 +-- crates/game-solver/src/player.rs | 21 ++++++- crates/games/Cargo.toml | 2 + crates/games/src/chomp/mod.rs | 18 ++---- crates/games/src/lib.rs | 1 + crates/games/src/sprouts/README.md | 6 ++ crates/games/src/sprouts/mod.rs | 6 ++ crates/games/src/util/cli/mod.rs | 13 ++-- rust-toolchain | 9 +++ 11 files changed, 170 insertions(+), 48 deletions(-) create mode 100644 crates/games/src/sprouts/README.md create mode 100644 crates/games/src/sprouts/mod.rs create mode 100644 rust-toolchain diff --git a/Cargo.lock b/Cargo.lock index dcf85d1..8bd9aab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -663,6 +663,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.98" @@ -1340,6 +1349,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.30" @@ -1522,6 +1537,7 @@ version = "0.1.0" dependencies = [ "anyhow", "array2d", + "castaway", "clap", "egui", "egui_commonmark", @@ -1530,6 +1546,7 @@ dependencies = [ "ndarray", "once_cell", "ordinal", + "petgraph", "serde", "serde-big-array", "thiserror", @@ -2386,6 +2403,16 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -2747,6 +2774,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.15" diff --git a/crates/game-solver/src/game.rs b/crates/game-solver/src/game.rs index 48127cd..5f8c920 100644 --- a/crates/game-solver/src/game.rs +++ b/crates/game-solver/src/game.rs @@ -14,7 +14,33 @@ pub enum GameState { Win(P), } +enum Partiality { + Impartial, + Partizan +} + +enum StateType { + Normal, + Misere +} + +impl StateType { + fn state(&self, game: &T) -> GameState + 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; @@ -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; /// Returns the amount of moves that have been played fn move_count(&self) -> usize; @@ -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, 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, 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; -} -pub trait PartizanGame: Game { /// Returns the player whose turn it is. /// The implementation of this should be /// similar to either @@ -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 { + 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 { + 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(game: &T) -> isize { game.max_moves().map_or(isize::MAX, |m| m as isize) diff --git a/crates/game-solver/src/lib.rs b/crates/game-solver/src/lib.rs index 35502fd..dca5c78 100644 --- a/crates/game-solver/src/lib.rs +++ b/crates/game-solver/src/lib.rs @@ -36,14 +36,8 @@ fn negamax + 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 diff --git a/crates/game-solver/src/player.rs b/crates/game-solver/src/player.rs index 5adc5cc..2c35dbd 100644 --- a/crates/game-solver/src/player.rs +++ b/crates/game-solver/src/player.rs @@ -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. @@ -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, @@ -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. @@ -102,4 +113,12 @@ impl Player for NPlayerConst { // This will always make index < N. Self::new_unchecked((self.0 + 1) % N) } -} \ No newline at end of file + + fn previous(self) -> Self { + if self.0 == 0 { + Self::new_unchecked(N - 1) + } else { + Self::new_unchecked(self.0 - 1) + } + } +} diff --git a/crates/games/Cargo.toml b/crates/games/Cargo.toml index c6b1690..fce22ea 100644 --- a/crates/games/Cargo.toml +++ b/crates/games/Cargo.toml @@ -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"] diff --git a/crates/games/src/chomp/mod.rs b/crates/games/src/chomp/mod.rs index dd72a3a..530de0a 100644 --- a/crates/games/src/chomp/mod.rs +++ b/crates/games/src/chomp/mod.rs @@ -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; @@ -124,11 +114,11 @@ impl Game for Chomp { moves.into_iter() } - fn state(&self) -> GameState { - 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 } } } diff --git a/crates/games/src/lib.rs b/crates/games/src/lib.rs index 757adbf..6f71eca 100644 --- a/crates/games/src/lib.rs +++ b/crates/games/src/lib.rs @@ -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, diff --git a/crates/games/src/sprouts/README.md b/crates/games/src/sprouts/README.md new file mode 100644 index 0000000..9200025 --- /dev/null +++ b/crates/games/src/sprouts/README.md @@ -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: diff --git a/crates/games/src/sprouts/mod.rs b/crates/games/src/sprouts/mod.rs new file mode 100644 index 0000000..91aedfb --- /dev/null +++ b/crates/games/src/sprouts/mod.rs @@ -0,0 +1,6 @@ +#![doc = include_str!("./README.md")] + +use petgraph::matrix_graph::MatrixGraph; + +#[derive(Clone)] +pub struct Sprouts(MatrixGraph<(), ()>); diff --git a/crates/games/src/util/cli/mod.rs b/crates/games/src/util/cli/mod.rs index 6370801..737a763 100644 --- a/crates/games/src/util/cli/mod.rs +++ b/crates/games/src/util/cli/mod.rs @@ -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(game: T) +pub fn play(game: T) where - T: Game + Eq + Hash + Sync + Send + Display + 'static, + P: Player, + T: Game + Eq + Hash + Sync + Send + Display + 'static, T::Move: Sync + Send + Display, T::MoveError: Sync + Send + Debug, { @@ -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); let mut move_scores = move_scores diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..fe60ec1 --- /dev/null +++ b/rust-toolchain @@ -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" ]