diff --git a/crates/game-solver/src/game.rs b/crates/game-solver/src/game.rs index 6f42779..27338d4 100644 --- a/crates/game-solver/src/game.rs +++ b/crates/game-solver/src/game.rs @@ -94,31 +94,33 @@ pub trait Game: Clone { /// /// Rather, this function asks if there exists some game in the possible games set /// which has a resolvable, positive or negative, outcome. - /// + /// /// This function must act in the Next player's best interest. /// Positive games should have highest priority, then tied games, then lost games. /// Exact order of what game is returned doesn't matter past its outcome equivalency, /// as the score is dependent on move count. - /// + /// /// (If this function returns a losing game when a positive game exists /// in the set of immediately resolvable games, that is a violation of this /// function's contract). - /// + /// /// This function's default implementation is quite slow, /// and it's encouraged to use a custom implementation. fn find_immediately_resolvable_game(&self) -> Result, Self::MoveError> { - let mut best_non_winning_game: Option = None; - + let mut best_non_winning_game: Option = None; + for m in &mut self.possible_moves() { let mut new_self = self.clone(); new_self.make_move(&m)?; match new_self.state() { GameState::Playable => continue, GameState::Tie => best_non_winning_game = Some(new_self), - GameState::Win(winning_player) => if winning_player == self.player().turn() { - return Ok(Some(new_self)) - } else if best_non_winning_game.is_none() { - best_non_winning_game = Some(new_self) + GameState::Win(winning_player) => { + if winning_player == self.player().turn() { + return Ok(Some(new_self)); + } else if best_non_winning_game.is_none() { + best_non_winning_game = Some(new_self) + } } }; } @@ -173,11 +175,11 @@ pub trait Game: Clone { } /// Utility function to get the upper score bound of a game. -/// +/// /// Essentially, score computation generally gives some max (usually max moves), /// and penalizes the score by the amount of moves that have been made, as we're /// trying to encourage winning in the shortest amount of time - God's algorithm. -/// +/// /// Note: Despite this returning isize, this function will always be positive. pub fn upper_bound(game: &T) -> isize { game.max_moves().map_or(isize::MAX, |m| m as isize) @@ -190,15 +192,19 @@ pub enum GameScoreOutcome { Win(usize), /// The inner field represents the amount of moves till a loss. Loss(usize), - Tie + Tie, } /// Utility function to convert a score to the /// amount of moves to a win or loss, or a tie. pub fn score_to_outcome(game: &T, score: isize) -> GameScoreOutcome { match score.cmp(&0) { - Ordering::Greater => GameScoreOutcome::Win((-score + upper_bound(game) - game.move_count() as isize) as usize), + Ordering::Greater => GameScoreOutcome::Win( + (-score + upper_bound(game) - game.move_count() as isize) as usize, + ), Ordering::Equal => GameScoreOutcome::Tie, - Ordering::Less => GameScoreOutcome::Loss((score + upper_bound(game) - game.move_count() as isize) as usize) + Ordering::Less => GameScoreOutcome::Loss( + (score + upper_bound(game) - game.move_count() as isize) as usize, + ), } } diff --git a/crates/game-solver/src/lib.rs b/crates/game-solver/src/lib.rs index 4c22b3b..e21dd08 100644 --- a/crates/game-solver/src/lib.rs +++ b/crates/game-solver/src/lib.rs @@ -38,11 +38,11 @@ fn negamax + Eq + Hash>( GameState::Win(winning_player) => { // The next player is the winning player - the score should be positive. if game.player() == winning_player { - return Ok(upper_bound(game) - game.move_count() as isize) + return Ok(upper_bound(game) - game.move_count() as isize); } else { - return Ok(-(upper_bound(game) - game.move_count() as isize)) + return Ok(-(upper_bound(game) - game.move_count() as isize)); } - }, + } }; // check if this is a winning configuration @@ -50,10 +50,12 @@ fn negamax + Eq + Hash>( match board.state() { GameState::Playable => panic!("A resolvable game should not be playable."), GameState::Tie => return Ok(0), - GameState::Win(winning_player) => if game.player().turn() == winning_player { - return Ok(upper_bound(&board) - board.move_count() as isize); - } else { - return Ok(-(upper_bound(&board) - board.move_count() as isize)); + GameState::Win(winning_player) => { + if game.player().turn() == winning_player { + return Ok(upper_bound(&board) - board.move_count() as isize); + } else { + return Ok(-(upper_bound(&board) - board.move_count() as isize)); + } } } } diff --git a/crates/game-solver/src/player.rs b/crates/game-solver/src/player.rs index ae372e5..031d1d2 100644 --- a/crates/game-solver/src/player.rs +++ b/crates/game-solver/src/player.rs @@ -13,10 +13,10 @@ pub trait Player: Sized + Eq { #[must_use] fn previous(self) -> Self; /// How the player instance 'changes' on the next move. - /// + /// /// For partizan games, the player doesn't change: /// Left stays left; right stays right. - /// + /// /// For impartial games, the player does change: /// Next turns into previous, and previous turns into next fn turn(self) -> Self; @@ -152,7 +152,7 @@ impl Player for NPlayerPartizanConst { Self::new_unchecked(self.0 - 1) } } - + fn turn(self) -> Self { self } diff --git a/crates/games/src/nim/mod.rs b/crates/games/src/nim/mod.rs index be2c8b6..9e9d5be 100644 --- a/crates/games/src/nim/mod.rs +++ b/crates/games/src/nim/mod.rs @@ -52,7 +52,7 @@ impl Game for Nim { /// where Move is a tuple of the heap index and the number of objects to remove type Move = NimMove; type Iter<'a> = std::vec::IntoIter; - + /// Define Nim as a zero-sum impartial game type Player = ImpartialPlayer; type MoveError = NimMoveError; diff --git a/crates/games/src/order_and_chaos/mod.rs b/crates/games/src/order_and_chaos/mod.rs index 33afaa8..6c503c1 100644 --- a/crates/games/src/order_and_chaos/mod.rs +++ b/crates/games/src/order_and_chaos/mod.rs @@ -11,7 +11,8 @@ use game_solver::{ }; use serde::{Deserialize, Serialize}; use std::{ - fmt::{Display, Formatter}, hash::Hash + fmt::{Display, Formatter}, + hash::Hash, }; use thiserror::Error; @@ -37,25 +38,44 @@ impl Display for CellType { } #[derive(Clone, Hash, Eq, PartialEq)] -pub struct OrderAndChaos { +pub struct OrderAndChaos< + const WIDTH: usize, + const HEIGHT: usize, + const MIN_WIN_LENGTH: usize, + const MAX_WIN_LENGTH: usize, +> { board: Array2D>, move_count: usize, } -impl Default for OrderAndChaos { +impl< + const WIDTH: usize, + const HEIGHT: usize, + const MIN_WIN_LENGTH: usize, + const MAX_WIN_LENGTH: usize, + > Default for OrderAndChaos +{ fn default() -> Self { Self::new() } } -impl OrderAndChaos { +impl< + const WIDTH: usize, + const HEIGHT: usize, + const MIN_WIN_LENGTH: usize, + const MAX_WIN_LENGTH: usize, + > OrderAndChaos +{ /// Create a new game of Nim with the given heaps, /// where heaps is a list of the number of objects in each heap. pub fn new() -> Self { assert!(MIN_WIN_LENGTH <= MAX_WIN_LENGTH, "MIN > MAX win length?"); // [a, b][(a < b) as usize] is essentially the max function: https://stackoverflow.com/a/53646925/7589775 - assert!(MAX_WIN_LENGTH <= [WIDTH, HEIGHT][(WIDTH < HEIGHT) as usize], "Win length should not be "); - + assert!( + MAX_WIN_LENGTH <= [WIDTH, HEIGHT][(WIDTH < HEIGHT) as usize], + "Win length should not be " + ); Self { board: Array2D::filled_with(None, HEIGHT, WIDTH), @@ -85,7 +105,13 @@ impl Display for OrderAndChaosMove { } } -impl Game for OrderAndChaos { +impl< + const WIDTH: usize, + const HEIGHT: usize, + const MIN_WIN_LENGTH: usize, + const MAX_WIN_LENGTH: usize, + > Game for OrderAndChaos +{ /// where Move is a tuple of: /// ((row, column), player) type Move = OrderAndChaosMove; @@ -157,7 +183,7 @@ impl Display for OrderAndChaos { +impl< + const WIDTH: usize, + const HEIGHT: usize, + const MIN_WIN_LENGTH: usize, + const MAX_WIN_LENGTH: usize, + > Display for OrderAndChaos +{ fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { for row in 0..HEIGHT { for column in 0..WIDTH { @@ -259,7 +291,13 @@ pub struct OrderAndChaosArgs { moves: Vec, } -impl TryFrom for OrderAndChaos { +impl< + const WIDTH: usize, + const HEIGHT: usize, + const MIN_WIN_LENGTH: usize, + const MAX_WIN_LENGTH: usize, + > TryFrom for OrderAndChaos +{ type Error = Error; fn try_from(value: OrderAndChaosArgs) -> Result { @@ -295,91 +333,114 @@ mod tests { use super::*; fn from_string(string: &str) -> OrderAndChaos<6, 6, 5, 6> { - let board_internal = string.chars().map(|ch| { - match ch { + let board_internal = string + .chars() + .map(|ch| match ch { 'X' => Some(Some(CellType::X)), 'O' => Some(Some(CellType::O)), '.' => Some(None), '\n' => None, - _ => panic!("There shouldn't be other characters in the string!") - } - }).filter_map(|x| x).collect::>(); + _ => panic!("There shouldn't be other characters in the string!"), + }) + .filter_map(|x| x) + .collect::>(); let element_count = board_internal.iter().filter(|x| x.is_some()).count(); let board = Array2D::from_row_major(&board_internal, 6, 6).unwrap(); - OrderAndChaos { board, move_count: element_count } + OrderAndChaos { + board, + move_count: element_count, + } } #[test] fn win_empty() { - let empty_board = from_string("......\ + let empty_board = from_string( + "......\ ......\ ......\ ......\ ......\ - ......"); + ......", + ); assert_eq!(empty_board.state(), GameState::Playable); } #[test] fn lose_horizontal_tiny() { - let horizontal_board = from_string("......\ + let horizontal_board = from_string( + "......\ .XOXXX\ .X....\ .OOOO.\ ......\ - ......"); + ......", + ); - assert_eq!(horizontal_board.state(), GameState::Win(PartizanPlayer::Left)); + assert_eq!( + horizontal_board.state(), + GameState::Win(PartizanPlayer::Left) + ); } #[test] fn win_horizontal() { - let horizontal_board = from_string("......\ + let horizontal_board = from_string( + "......\ .XOXXX\ .X....\ .OOOOO\ ......\ - ......"); + ......", + ); - assert_eq!(horizontal_board.state(), GameState::Win(PartizanPlayer::Left)); + assert_eq!( + horizontal_board.state(), + GameState::Win(PartizanPlayer::Left) + ); } #[test] fn win_vertical() { - let vertical_board = from_string("......\ + let vertical_board = from_string( + "......\ .XOOXX\ .X.X..\ .OOXOO\ ...X..\ - ...X.."); + ...X..", + ); assert_eq!(vertical_board.state(), GameState::Win(PartizanPlayer::Left)); } #[test] fn lose_vertical_tiny() { - let vertical_board = from_string("......\ + let vertical_board = from_string( + "......\ .XOOXX\ .X.X..\ .OOXOO\ ...X..\ - ......"); + ......", + ); assert_eq!(vertical_board.state(), GameState::Win(PartizanPlayer::Left)); } #[test] fn win_diagonal() { - let diagonal_board = from_string("......\ + let diagonal_board = from_string( + "......\ .OOOXX\ .XX...\ .OOXOO\ ...XX.\ - ...X.X"); + ...X.X", + ); assert_eq!(diagonal_board.state(), GameState::Win(PartizanPlayer::Left)); } diff --git a/crates/games/src/util/cli/mod.rs b/crates/games/src/util/cli/mod.rs index d42ec4d..daefb27 100644 --- a/crates/games/src/util/cli/mod.rs +++ b/crates/games/src/util/cli/mod.rs @@ -5,7 +5,10 @@ use game_solver::{ par_move_scores, player::{ImpartialPlayer, TwoPlayer}, }; -use std::{any::TypeId, fmt::{Debug, Display}}; +use std::{ + any::TypeId, + fmt::{Debug, Display}, +}; pub fn play< T: Game + Eq + Hash + Sync + Send + Display + 'static, @@ -40,9 +43,19 @@ pub fn play< for (game_move, score) in move_scores { if current_move_score != Some(score) { match score_to_outcome(&game, score) { - GameScoreOutcome::Win(moves) => println!("\n\nWin in {} move{} (score {}):", moves, if moves == 1 { "" } else { "s" }, score), - GameScoreOutcome::Loss(moves) => println!("\n\nLose in {} move{} (score {}):", moves, if moves == 1 { "" } else { "s" }, score), - GameScoreOutcome::Tie => println!("\n\nTie with the following moves:") + GameScoreOutcome::Win(moves) => println!( + "\n\nWin in {} move{} (score {}):", + moves, + if moves == 1 { "" } else { "s" }, + score + ), + GameScoreOutcome::Loss(moves) => println!( + "\n\nLose in {} move{} (score {}):", + moves, + if moves == 1 { "" } else { "s" }, + score + ), + GameScoreOutcome::Tie => println!("\n\nTie with the following moves:"), } current_move_score = Some(score); } @@ -70,9 +83,11 @@ where match game.state() { GameState::Playable => (), GameState::Tie => return Err(anyhow!("Can't continue - game is tied.")), - GameState::Win(player) => return Err(anyhow!( - "Can't continue game if player {player:?} already won." - )), + GameState::Win(player) => { + return Err(anyhow!( + "Can't continue game if player {player:?} already won." + )) + } }; game.make_move(m)