Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit 89853d9
Author: Tristan F. <LeoDog896@hotmail.com>
Date:   Thu Sep 26 14:17:04 2024 -0700

    style: fmt

commit e7aecb1
Author: Tristan F. <LeoDog896@hotmail.com>
Date:   Thu Sep 26 14:16:56 2024 -0700

    feat: full sprouts game

commit 2080e63
Author: Tristan F. <LeoDog896@hotmail.com>
Date:   Thu Sep 26 12:26:07 2024 -0700

    init
  • Loading branch information
LeoDog896 committed Sep 26, 2024
1 parent 409f765 commit c98c772
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 14 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion crates/games-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Parser;
use games::{
chomp::Chomp, domineering::Domineering, nim::Nim, order_and_chaos::OrderAndChaos,
reversi::Reversi, tic_tac_toe::TicTacToe, util::cli::play, Games,
reversi::Reversi, sprouts::Sprouts, tic_tac_toe::TicTacToe, util::cli::play, Games,
};

/// `game-solver` is a solving utility that helps analyze various combinatorial games.
Expand All @@ -27,6 +27,7 @@ fn main() -> Result<()> {
Games::Nim(args) => play::<Nim>(args.try_into().unwrap(), cli.plain),
Games::Domineering(args) => play::<Domineering<5, 5>>(args.try_into().unwrap(), cli.plain),
Games::Chomp(args) => play::<Chomp>(args.try_into().unwrap(), cli.plain),
Games::Sprouts(args) => play::<Sprouts>(args.try_into().unwrap(), cli.plain),
};

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion crates/games/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ 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"
petgraph = { version = "0.6.5", features = ["serde-1"] }
castaway = "0.2.3"
ratatui = "0.28.1"
owo-colors = "4.1.0"
Expand Down
13 changes: 12 additions & 1 deletion crates/games/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod tic_tac_toe;
use crate::{
chomp::ChompArgs, domineering::DomineeringArgs, nim::NimArgs,
order_and_chaos::OrderAndChaosArgs, reversi::ReversiArgs, tic_tac_toe::TicTacToeArgs,
sprouts::SproutsArgs
};
use clap::Subcommand;
use once_cell::sync::Lazy;
Expand All @@ -24,16 +25,18 @@ pub enum Games {
Nim(NimArgs),
Domineering(DomineeringArgs),
Chomp(ChompArgs),
Sprouts(SproutsArgs)
}

pub static DEFAULT_GAMES: Lazy<[Games; 6]> = Lazy::new(|| {
pub static DEFAULT_GAMES: Lazy<[Games; 7]> = Lazy::new(|| {
[
Games::Reversi(Default::default()),
Games::TicTacToe(Default::default()),
Games::OrderAndChaos(Default::default()),
Games::Nim(Default::default()),
Games::Domineering(Default::default()),
Games::Chomp(Default::default()),
Games::Sprouts(Default::default())
]
});

Expand All @@ -46,6 +49,7 @@ impl Games {
Self::Nim(_) => "Nim".to_string(),
Self::Domineering(_) => "Domineering".to_string(),
Self::Chomp(_) => "Chomp".to_string(),
Self::Sprouts(_) => "Sprouts".to_string()
}
}

Expand All @@ -57,6 +61,7 @@ impl Games {
Self::Nim(_) => include_str!("./nim/README.md"),
Self::Domineering(_) => include_str!("./domineering/README.md"),
Self::Chomp(_) => include_str!("./chomp/README.md"),
Self::Sprouts(_) => include_str!("./sprouts/README.md")
}
}

Expand Down Expand Up @@ -100,6 +105,12 @@ impl Games {
&mut cache,
"crates/games/src/chomp/README.md"
),
Self::Sprouts(_) => egui_commonmark::commonmark_str!(
"sprouts",
ui,
&mut cache,
"crates/games/src/sprouts/README.md"
),
};
}
}
269 changes: 264 additions & 5 deletions crates/games/src/sprouts/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,270 @@
#![doc = include_str!("./README.md")]

use game_solver::game::Game;
use petgraph::matrix_graph::MatrixGraph;
use std::{fmt::{Debug, Display}, hash::Hash, str::FromStr};

use anyhow::Error;
use clap::Args;
use game_solver::{game::{Game, StateType}, player::ImpartialPlayer};
use itertools::Itertools;
use petgraph::{matrix_graph::{MatrixGraph, NodeIndex}, visit::{IntoEdgeReferences, IntoNodeIdentifiers}, Undirected};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::util::cli::move_failable;

/// We aren't dealing with large sprout counts for now.
pub type SproutsIx = u8;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
pub struct SproutsMove {
from: NodeIndex<SproutsIx>,
to: NodeIndex<SproutsIx>
}

impl Display for SproutsMove {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({} {})", self.from.index(), self.to.index())
}
}

type SproutsGraph = MatrixGraph<(), (), Undirected, Option<()>, SproutsIx>;

#[derive(Clone)]
pub struct Sprouts(MatrixGraph<(), ()>);
pub struct Sprouts(SproutsGraph);

// SproutsGraph, given that its vertices and edges are unlabelled,
// doesn't implement equality as that requires isomorphism checks.
// since we don't want these operations for reordering to be expensive,
// we simply check for equality as is.

impl Hash for Sprouts {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.node_count().hash(state);
for edge in self.0.edge_references() {
edge.hash(state);
}
}
}

impl PartialEq for Sprouts {
fn eq(&self, other: &Self) -> bool {
self.0.node_count() == other.0.node_count()
&& self.0.edge_references().collect::<Vec<_>>() == other.0.edge_references().collect::<Vec<_>>()
}

fn ne(&self, other: &Self) -> bool {
!self.eq(other)
}
}

impl Eq for Sprouts {}

impl Sprouts {
pub fn new(node_count: SproutsIx) -> Self {
let mut graph = SproutsGraph::default();

for _ in 0..node_count {
graph.add_node(());
}

Self(graph)
}
}

#[derive(Error, Debug, Clone)]
pub enum SproutsMoveError {
#[error("chosen index {0} from move {1:?} is out of bounds.")]
MoveOutOfBounds(SproutsIx, SproutsMove),
#[error("chosen index {0} from move {1:?} references a dead sprout.")]
DeadSprout(SproutsIx, SproutsMove),
#[error("a move for {0:?} has already been made")]
SproutsConnected(SproutsMove)
}

const MAX_SPROUTS: usize = 3;

impl Game for Sprouts {
type Move = SproutsMove;
type Iter<'a> = std::vec::IntoIter<Self::Move>;

type Player = ImpartialPlayer;
type MoveError = SproutsMoveError;

const STATE_TYPE: Option<StateType> = Some(StateType::Normal);

fn max_moves(&self) -> Option<usize> {
// TODO: i actually want to find what the proper paper is, but
// https://en.wikipedia.org/wiki/Sprouts_(game)#Maximum_number_of_moves
// is where this is from.
// TODO: use MAX_SPROUTS?
Some(3 * self.0.node_count() - 1)
}

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

fn make_move(&mut self, m: &Self::Move) -> Result<(), Self::MoveError> {
// There already exists an edge here!
if self.0.has_edge(m.from, m.to) {
return Err(SproutsMoveError::SproutsConnected(m.clone()));
}

// move index is out of bounds
{
if !self.0.node_identifiers().contains(&m.from) {
return Err(SproutsMoveError::MoveOutOfBounds(
m.from.index().try_into().unwrap(),
m.clone())
);
}

if !self.0.node_identifiers().contains(&m.to) {
return Err(SproutsMoveError::MoveOutOfBounds(
m.to.index().try_into().unwrap(),
m.clone())
);
}
}

// sprouts to use are dead
{
if self.0.edges(m.from).count() >= MAX_SPROUTS {
return Err(SproutsMoveError::DeadSprout(
m.from.index().try_into().unwrap(),
m.clone()
));
}

if self.0.edges(m.to).count() >= MAX_SPROUTS {
return Err(SproutsMoveError::DeadSprout(
m.to.index().try_into().unwrap(),
m.clone()
));
}
}

self.0.add_edge(m.from, m.to, ());

Ok(())
}

fn player(&self) -> Self::Player {
ImpartialPlayer::Next
}

fn possible_moves(&self) -> Self::Iter<'_> {
let mut sprouts_moves = vec![];

for id in self.0.node_identifiers() {
let edge_count = self.0.edges(id).count();

// TODO: use MAX_SPROUTS for all values
match edge_count {
0 | 1 => {
if !self.0.has_edge(id, id) {
sprouts_moves.push(SproutsMove { from: id, to: id });
}
for sub_id in self.0.node_identifiers() {
if id >= sub_id { continue; }
if self.0.edges(sub_id).count() >= MAX_SPROUTS { continue; }
if self.0.has_edge(id, sub_id) { continue; }
sprouts_moves.push(SproutsMove { from: id, to: sub_id })
}
},
2 => {
for sub_id in self.0.node_identifiers() {
if id >= sub_id { continue; }
if self.0.edges(sub_id).count() >= MAX_SPROUTS { continue; }
if self.0.has_edge(id, sub_id) { continue; }
sprouts_moves.push(SproutsMove { from: id, to: sub_id })
}
},
MAX_SPROUTS => (),
_ => panic!("No node should have more than three edges")
}
}

sprouts_moves.into_iter()
}

fn state(&self) -> game_solver::game::GameState<Self::Player> {
Self::STATE_TYPE.unwrap().state(self)
}
}

impl Debug for Sprouts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let references = self.0.edge_references().collect::<Vec<_>>();

writeln!(f, "graph of vertices count {}", references.len())?;

if references.is_empty() {
return Ok(());
}

for (i, j, _) in references {
write!(f, "{}-{} ", i.index(), j.index())?;
}
writeln!(f)?;

Ok(())
}
}

impl Display for Sprouts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<Self as Debug>::fmt(&self, f)
}
}

/// Analyzes Sprouts.
///
#[doc = include_str!("./README.md")]
#[derive(Args, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct SproutsArgs {
/// The amount of sprouts (nodes)
/// to start off with.
starting_sprouts: SproutsIx,
/// Sprouts moves, ordered as i1-j1 i2-j2 ...
#[arg(value_parser = clap::value_parser!(SproutsMove))]
moves: Vec<SproutsMove>
}

impl Default for SproutsArgs {
fn default() -> Self {
Self {
starting_sprouts: 6,
moves: vec![]
}
}
}

impl TryFrom<SproutsArgs> for Sprouts {
type Error = Error;

fn try_from(args: SproutsArgs) -> Result<Self, Self::Error> {
let mut game = Sprouts::new(args.starting_sprouts);

for sprouts_move in args.moves {
move_failable(&mut game, &sprouts_move)?;
}

Ok(game)
}
}

impl FromStr for SproutsMove {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let components = s.split("-").collect::<Vec<_>>();

// impl Game for Sprouts {
assert_eq!(components.len(), 2, "a move shouldn't connect more than two sprouts");

// }
Ok(SproutsMove {
from: str::parse::<SproutsIx>(components[0])?.into(),
to: str::parse::<SproutsIx>(components[1])?.into()
})
}
}
4 changes: 2 additions & 2 deletions crates/games/src/util/cli/robot.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use game_solver::{
game::{score_to_outcome, Game, GameScoreOutcome},
game::Game,
par_move_scores,
player::{ImpartialPlayer, TwoPlayer},
};
Expand All @@ -9,7 +9,7 @@ use std::{
hash::Hash,
};

use crate::util::{cli::report::scores::show_scores, move_score::normalize_move_scores};
use crate::util::cli::report::scores::show_scores;

pub fn robotic_output<
T: Game<Player = impl TwoPlayer + Debug + Sync + 'static>
Expand Down
Loading

0 comments on commit c98c772

Please sign in to comment.