From b76f37b693ed010a2f7449d913c6856220993e2d Mon Sep 17 00:00:00 2001 From: Tristan F <26509014+LeoDog896@users.noreply.github.com> Date: Sun, 2 Jun 2024 20:52:49 -0400 Subject: [PATCH] refactor: begin dropping duplicate cli code --- Cargo.lock | 1 + crates/games-ui/src/app.rs | 3 + crates/games/Cargo.toml | 4 ++ crates/games/src/lib.rs | 11 ++++ crates/games/src/nim/gui.rs | 0 crates/games/src/reversi/cli.rs | 35 +----------- crates/games/src/reversi/mod.rs | 23 +++++--- crates/games/src/tic_tac_toe/cli.rs | 32 +++-------- crates/games/src/tic_tac_toe/mod.rs | 86 +++++++++++++++++++---------- crates/games/src/util/cli/mod.rs | 1 + crates/games/src/util/cli/play.rs | 37 +++++++++++++ crates/games/src/util/mod.rs | 2 + crates/games/src/util/state.rs | 12 ++++ 13 files changed, 153 insertions(+), 94 deletions(-) create mode 100644 crates/games/src/nim/gui.rs create mode 100644 crates/games/src/util/cli/mod.rs create mode 100644 crates/games/src/util/cli/play.rs create mode 100644 crates/games/src/util/state.rs diff --git a/Cargo.lock b/Cargo.lock index b716dd3..7d81178 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1538,6 +1538,7 @@ dependencies = [ "anyhow", "array2d", "clap", + "egui", "game-solver", "itertools", "ndarray", diff --git a/crates/games-ui/src/app.rs b/crates/games-ui/src/app.rs index 153cdf1..ea17d24 100644 --- a/crates/games-ui/src/app.rs +++ b/crates/games-ui/src/app.rs @@ -73,6 +73,9 @@ impl eframe::App for TemplateApp { if let Some(game) = &self.selected_game { ui.heading(game.name()); + ui.collapsing("See game description", |ui| { + ui.label(game.description()); + }); } else { ui.label("To get started, select a game from the above dropdown."); } diff --git a/crates/games/Cargo.toml b/crates/games/Cargo.toml index 6c30127..b25c42a 100644 --- a/crates/games/Cargo.toml +++ b/crates/games/Cargo.toml @@ -15,3 +15,7 @@ ordinal = "0.3.2" serde = { version = "1", features = ["derive"] } serde-big-array = "0.5.1" once_cell = "1.19.0" +egui = { version = "0.27", optional = true } + +[features] +"egui" = ["dep:egui"] diff --git a/crates/games/src/lib.rs b/crates/games/src/lib.rs index 70d2e52..4962430 100644 --- a/crates/games/src/lib.rs +++ b/crates/games/src/lib.rs @@ -48,4 +48,15 @@ impl Games { &Self::Chomp(_) => "Chomp".to_string(), } } + + pub fn description(&self) -> &str { + match self { + &Self::Reversi(_) => include_str!("./reversi/README.md"), + &Self::TicTacToe(_) => include_str!("./tic_tac_toe/README.md"), + &Self::OrderAndChaos(_) => include_str!("./order_and_chaos/README.md"), + &Self::Nim(_) => include_str!("./nim/README.md"), + &Self::Domineering(_) => include_str!("./domineering/README.md"), + &Self::Chomp(_) => include_str!("./chomp/README.md"), + } + } } diff --git a/crates/games/src/nim/gui.rs b/crates/games/src/nim/gui.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/games/src/reversi/cli.rs b/crates/games/src/reversi/cli.rs index 51ea372..4cb85b4 100644 --- a/crates/games/src/reversi/cli.rs +++ b/crates/games/src/reversi/cli.rs @@ -2,13 +2,10 @@ use std::fmt; use crate::{ reversi::{Reversi, ReversiMove}, - util::move_natural::NaturalMove, + util::{cli::play::play, move_natural::NaturalMove}, }; use clap::Args; -use game_solver::{ - game::{Game, ZeroSumPlayer}, - par_move_scores, -}; +use game_solver::game::{Game, ZeroSumPlayer}; use serde::{Deserialize, Serialize}; use super::{HEIGHT, WIDTH}; @@ -69,31 +66,5 @@ pub fn main(args: ReversiArgs) { }); print!("{}", game); - println!("Player {:?} to move", game.player()); - - let mut move_scores = par_move_scores(&game); - - if move_scores.is_empty() { - game.winning_player().map_or_else( - || { - println!("Game tied!"); - }, - |player| { - println!("Player {:?} won!", player.opponent()); - }, - ) - } else { - move_scores.sort_by_key(|m| m.1); - move_scores.reverse(); - - let mut current_move_score = None; - for (game_move, score) in move_scores { - if current_move_score != Some(score) { - println!("\n\nBest moves @ score {}:", score); - current_move_score = Some(score); - } - print!("{}, ", game_move); - } - println!(); - } + play(game); } diff --git a/crates/games/src/reversi/mod.rs b/crates/games/src/reversi/mod.rs index c93f96a..08d3d72 100644 --- a/crates/games/src/reversi/mod.rs +++ b/crates/games/src/reversi/mod.rs @@ -6,7 +6,10 @@ use array2d::Array2D; use game_solver::game::{Game, ZeroSumPlayer}; use std::hash::Hash; -use crate::util::move_natural::NaturalMove; +use crate::util::{ + move_natural::NaturalMove, + state::{GameState, State}, +}; pub const WIDTH: usize = 6; pub const HEIGHT: usize = 6; @@ -117,8 +120,14 @@ impl Reversi { Some(tiles_to_flip) } } +} + +impl GameState for Reversi { + fn state(&self) -> State { + if self.possible_moves().len() > 0 { + return State::Continuing; + } - fn winning_player(&self) -> Option { let mut player_one_count = 0; let mut player_two_count = 0; @@ -133,9 +142,9 @@ impl Reversi { } match player_one_count.cmp(&player_two_count) { - std::cmp::Ordering::Greater => Some(ZeroSumPlayer::One), - std::cmp::Ordering::Less => Some(ZeroSumPlayer::Two), - std::cmp::Ordering::Equal => None, + std::cmp::Ordering::Greater => State::Player(ZeroSumPlayer::One), + std::cmp::Ordering::Less => State::Player(ZeroSumPlayer::Two), + std::cmp::Ordering::Equal => State::Tie, } } } @@ -193,7 +202,7 @@ impl Game for Reversi { let mut board = self.clone(); board.make_move(m); if board.possible_moves().next().is_none() { - if board.winning_player() == Some(self.player()) { + if board.state() == State::Player(self.player()) { Some(self.player()) } else { None @@ -204,6 +213,6 @@ impl Game for Reversi { } fn is_draw(&self) -> bool { - self.winning_player().is_none() && self.possible_moves().next().is_none() + self.state() == State::Tie } } diff --git a/crates/games/src/tic_tac_toe/cli.rs b/crates/games/src/tic_tac_toe/cli.rs index cd5d47d..255f006 100644 --- a/crates/games/src/tic_tac_toe/cli.rs +++ b/crates/games/src/tic_tac_toe/cli.rs @@ -1,9 +1,12 @@ use clap::Args; -use game_solver::{game::Game, par_move_scores}; +use game_solver::game::Game; use ndarray::IntoDimension; use serde::{Deserialize, Serialize}; -use crate::tic_tac_toe::{format_dim, TicTacToe}; +use crate::{ + tic_tac_toe::{TicTacToe, TicTacToeMove}, + util::cli::play::play, +}; /// Analyzes Tic Tac Toe. /// @@ -45,30 +48,9 @@ pub fn main(args: TicTacToeArgs) { .map(|num| num.parse::().expect("Not a number!")) .collect(); - game.make_move(&numbers.into_dimension()); + game.make_move(&TicTacToeMove(numbers.into_dimension())); }); print!("{}", game); - println!("Player {:?} to move", game.player()); - - let mut move_scores = par_move_scores(&game); - - if game.won() { - println!("Player {:?} won!", game.player().opponent()); - } else if move_scores.is_empty() { - println!("No moves left! Game tied!"); - } else { - move_scores.sort_by_key(|m| m.1); - move_scores.reverse(); - - let mut current_move_score = None; - for (game_move, score) in move_scores { - if current_move_score != Some(score) { - println!("\n\nBest moves @ score {}:", score); - current_move_score = Some(score); - } - print!("{}, ", format_dim(&game_move)); - } - println!(); - } + play(game); } diff --git a/crates/games/src/tic_tac_toe/mod.rs b/crates/games/src/tic_tac_toe/mod.rs index 313d665..02b8f4f 100644 --- a/crates/games/src/tic_tac_toe/mod.rs +++ b/crates/games/src/tic_tac_toe/mod.rs @@ -12,6 +12,8 @@ use std::{ iter::FilterMap, }; +use crate::util::state::{GameState, State}; + #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] enum Square { Empty, @@ -28,6 +30,9 @@ struct TicTacToe { move_count: usize, } +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct TicTacToeMove(pub Dim); + fn add_checked(a: Dim, b: Vec) -> Option> { let mut result = a; for (i, j) in result.as_array_view_mut().iter_mut().zip(b.iter()) { @@ -87,8 +92,10 @@ impl TicTacToe { n >= self.size } +} - fn won(&self) -> bool { +impl GameState for TicTacToe { + fn state(&self) -> State { // check every move for (index, square) in self.board.indexed_iter() { if square == &Square::Empty { @@ -98,20 +105,25 @@ impl TicTacToe { let point = index.into_dimension(); for offset in offsets(&point, self.size) { if self.winning_line(&point, &offset) { - return true; + return State::Player(self.player().opponent()); } } } - false + // check if tie + if Some(self.move_count()) == self.max_moves() { + return State::Tie; + } + + State::Continuing } } impl Game for TicTacToe { - type Move = Dim; + type Move = TicTacToeMove; type Iter<'a> = FilterMap< - IndexedIter<'a, Square, Self::Move>, - fn((Self::Move, &Square)) -> Option, + IndexedIter<'a, Square, Dim>, + fn((Dim, &Square)) -> Option, >; type Player = ZeroSumPlayer; @@ -132,14 +144,14 @@ impl Game for TicTacToe { } fn make_move(&mut self, m: &Self::Move) -> bool { - if *self.board.get(m.clone()).unwrap() == Square::Empty { + if *self.board.get(m.0.clone()).unwrap() == Square::Empty { let square = if self.player() == ZeroSumPlayer::One { Square::X } else { Square::O }; - *self.board.get_mut(m).unwrap() = square; + *self.board.get_mut(m.0.clone()).unwrap() = square; self.move_count += 1; true } else { @@ -152,7 +164,7 @@ impl Game for TicTacToe { .indexed_iter() .filter_map(move |(index, square)| { if square == &Square::Empty { - Some(index) + Some(TicTacToeMove(index)) } else { None } @@ -178,8 +190,8 @@ impl Game for TicTacToe { // - it increases // - it decreases // e.g. (0, 0, 2), (0, 1, 1), (0, 2, 0) wins - for offset in offsets(m, self.size) { - if board.winning_line(m, &offset) { + for offset in offsets(&m.0, self.size) { + if board.winning_line(&m.0, &offset) { return Some(self.player()); } } @@ -215,14 +227,16 @@ fn offsets(dim: &Dim, size: usize) -> Vec> { .collect() } -fn format_dim(dim: &Dim) -> String { - format!("{:?}", dim.as_array_view().as_slice().unwrap()) +impl Display for TicTacToeMove { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0.as_array_view().as_slice().unwrap()) + } } impl Display for TicTacToe { fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { for (index, square) in self.board.indexed_iter() { - writeln!(f, "{:?} @ {}", square, format_dim(&index))?; + writeln!(f, "{:?} @ {}", square, TicTacToeMove(index))?; } Ok(()) } @@ -237,13 +251,13 @@ mod tests { fn best_moves(game: &TicTacToe) -> Option> { move_scores(game, &mut HashMap::new()) .max_by(|(_, a), (_, b)| a.cmp(b)) - .map(|(m, _)| m) + .map(|(m, _)| m.0) } #[test] fn test_middle_move() { let mut game = TicTacToe::new(2, 3); - game.make_move(&vec![0, 0].into_dimension()); + game.make_move(&TicTacToeMove(vec![0, 0].into_dimension())); let best_move = best_moves(&game).unwrap(); @@ -261,28 +275,40 @@ mod tests { fn test_win() { let mut game = TicTacToe::new(2, 3); - game.make_move(&vec![0, 2].into_dimension()); // X - game.make_move(&vec![0, 1].into_dimension()); // O - game.make_move(&vec![1, 1].into_dimension()); // X - game.make_move(&vec![0, 0].into_dimension()); // O - game.make_move(&vec![2, 0].into_dimension()); // X + game.make_move(&TicTacToeMove(vec![0, 2].into_dimension())); // X + game.make_move(&TicTacToeMove(vec![0, 1].into_dimension())); // O + game.make_move(&TicTacToeMove(vec![1, 1].into_dimension())); // X + game.make_move(&TicTacToeMove(vec![0, 0].into_dimension())); // O + game.make_move(&TicTacToeMove(vec![2, 0].into_dimension())); // X + + assert!(game.state() == State::Player(ZeroSumPlayer::One)); + } + + #[test] + fn test_no_win() { + let mut game = TicTacToe::new(2, 3); + + game.make_move(&TicTacToeMove(vec![0, 2].into_dimension())); // X + game.make_move(&TicTacToeMove(vec![0, 1].into_dimension())); // O + game.make_move(&TicTacToeMove(vec![1, 1].into_dimension())); // X + game.make_move(&TicTacToeMove(vec![0, 0].into_dimension())); // O - assert!(game.won()); + assert!(game.state() == State::Continuing); } #[test] fn test_win_3d() { let mut game = TicTacToe::new(3, 3); - game.make_move(&vec![0, 0, 0].into_dimension()); // X - game.make_move(&vec![0, 0, 1].into_dimension()); // O - game.make_move(&vec![0, 1, 1].into_dimension()); // X - game.make_move(&vec![0, 0, 2].into_dimension()); // O - game.make_move(&vec![0, 2, 2].into_dimension()); // X - game.make_move(&vec![0, 1, 0].into_dimension()); // O - game.make_move(&vec![0, 2, 0].into_dimension()); // X + game.make_move(&TicTacToeMove(vec![0, 0, 0].into_dimension())); // X + game.make_move(&TicTacToeMove(vec![0, 0, 1].into_dimension())); // O + game.make_move(&TicTacToeMove(vec![0, 1, 1].into_dimension())); // X + game.make_move(&TicTacToeMove(vec![0, 0, 2].into_dimension())); // O + game.make_move(&TicTacToeMove(vec![0, 2, 2].into_dimension())); // X + game.make_move(&TicTacToeMove(vec![0, 1, 0].into_dimension())); // O + game.make_move(&TicTacToeMove(vec![0, 2, 0].into_dimension())); // X - assert!(game.won()); + assert!(game.state() == State::Player(ZeroSumPlayer::One)); } #[test] diff --git a/crates/games/src/util/cli/mod.rs b/crates/games/src/util/cli/mod.rs new file mode 100644 index 0000000..34f2061 --- /dev/null +++ b/crates/games/src/util/cli/mod.rs @@ -0,0 +1 @@ +pub mod play; diff --git a/crates/games/src/util/cli/play.rs b/crates/games/src/util/cli/play.rs new file mode 100644 index 0000000..83e2c85 --- /dev/null +++ b/crates/games/src/util/cli/play.rs @@ -0,0 +1,37 @@ +use core::hash::Hash; +use game_solver::{ + game::{Game, ZeroSumPlayer}, + par_move_scores, +}; +use std::fmt::Display; + +use crate::util::state::{GameState, State}; + +pub fn play(game: T) +where + T: Game + Clone + Eq + Hash + Sync + Send + GameState + 'static, + T::Move: Sync + Send + Display, +{ + println!("Player {:?} to move", game.player()); + + let mut move_scores = par_move_scores(&game); + + if game.state() == State::Continuing { + println!("Player {:?} won!", game.player().opponent()); + } else if move_scores.is_empty() { + println!("No moves left! Game tied!"); + } else { + move_scores.sort_by_key(|m| m.1); + move_scores.reverse(); + + let mut current_move_score = None; + for (game_move, score) in move_scores { + if current_move_score != Some(score) { + println!("\n\nBest moves @ score {}:", score); + current_move_score = Some(score); + } + print!("{}, ", &game_move); + } + println!(); + } +} diff --git a/crates/games/src/util/mod.rs b/crates/games/src/util/mod.rs index 6a0b566..2a4e484 100644 --- a/crates/games/src/util/mod.rs +++ b/crates/games/src/util/mod.rs @@ -1 +1,3 @@ +pub mod cli; pub mod move_natural; +pub mod state; diff --git a/crates/games/src/util/state.rs b/crates/games/src/util/state.rs new file mode 100644 index 0000000..c8d6830 --- /dev/null +++ b/crates/games/src/util/state.rs @@ -0,0 +1,12 @@ +use game_solver::game::ZeroSumPlayer; + +#[derive(Eq, PartialEq)] +pub enum State { + Player(ZeroSumPlayer), + Tie, + Continuing, +} + +pub trait GameState { + fn state(&self) -> State; +}