Skip to content

Commit

Permalink
feat: more tui!
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoDog896 committed Sep 26, 2024
1 parent 82fa3c2 commit 84bc8f0
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 72 deletions.
86 changes: 68 additions & 18 deletions crates/game-solver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ use std::hash::Hash;
use thiserror::Error;

#[derive(Error, Debug)]
enum GameSolveError<T: Game> {
pub enum GameSolveError<T: Game> {
#[error("could not make a move")]
MoveError(T::MoveError),
#[error("the game was cancelled by the token")]
CancellationTokenError
CancellationTokenError,
}

/// Runs the two-player minimax variant on a zero-sum game.
Expand All @@ -43,14 +43,19 @@ fn negamax<T: Game<Player = impl TwoPlayer> + Eq + Hash>(
mut alpha: isize,
mut beta: isize,
stats: Option<&Stats>,
cancellation_token: &Option<Arc<AtomicBool>>
cancellation_token: &Option<Arc<AtomicBool>>,
) -> Result<isize, GameSolveError<T>> {
if let Some(token) = cancellation_token {
if token.load(Ordering::Relaxed) {
return Err(GameSolveError::CancellationTokenError);
}
}

// TODO: debug-based depth counting
// if let Some(stats) = stats {
// stats.max_depth.fetch_max(depth, Ordering::Relaxed);
// }

// TODO(perf): if find_immediately_resolvable_game satisfies its contract,
// we can ignore this at larger depths.
match game.state() {
Expand Down Expand Up @@ -144,21 +149,47 @@ fn negamax<T: Game<Player = impl TwoPlayer> + Eq + Hash>(

for m in &mut game.possible_moves() {
let mut board = game.clone();
board.make_move(&m).map_err(|err| GameSolveError::MoveError::<T>(err));
board
.make_move(&m)
.map_err(|err| GameSolveError::MoveError::<T>(err))?;

let score = if first_child {
-negamax(&board, transposition_table, -beta, -alpha, stats, &cancellation_token)?
-negamax(
&board,
transposition_table,
-beta,
-alpha,
stats,
&cancellation_token,
)?
} else {
let score = -negamax(&board, transposition_table, -alpha - 1, -alpha, stats, &cancellation_token)?;
let score = -negamax(
&board,
transposition_table,
-alpha - 1,
-alpha,
stats,
&cancellation_token,
)?;
if score > alpha {
-negamax(&board, transposition_table, -beta, -alpha, stats, &cancellation_token)?
-negamax(
&board,
transposition_table,
-beta,
-alpha,
stats,
&cancellation_token,
)?
} else {
score
}
};

// alpha-beta pruning - we can return early
if score >= beta {
if let Some(stats) = stats {
stats.pruning_cutoffs.fetch_add(1, Ordering::Relaxed);
}
transposition_table.insert(game.clone(), Score::LowerBound(score));
return Ok(beta);
}
Expand All @@ -185,7 +216,7 @@ pub fn solve<T: Game<Player = impl TwoPlayer> + Eq + Hash>(
game: &T,
transposition_table: &mut dyn TranspositionTable<T>,
stats: Option<&Stats>,
cancellation_token: Option<Arc<AtomicBool>>
cancellation_token: &Option<Arc<AtomicBool>>,
) -> Result<isize, GameSolveError<T>> {
let mut alpha = -upper_bound(game);
let mut beta = upper_bound(game) + 1;
Expand All @@ -195,8 +226,14 @@ pub fn solve<T: Game<Player = impl TwoPlayer> + Eq + Hash>(
let med = alpha + (beta - alpha) / 2;

// do a [null window search](https://www.chessprogramming.org/Null_Window)
let evaluation = negamax(game, transposition_table, med, med + 1, stats, &cancellation_token)
.map_err(|err| GameSolveError::MoveError(err))?;
let evaluation = negamax(
game,
transposition_table,
med,
med + 1,
stats,
cancellation_token,
)?;

if evaluation <= med {
beta = evaluation;
Expand All @@ -220,17 +257,23 @@ pub fn move_scores<'a, T: Game<Player = impl TwoPlayer> + Eq + Hash>(
game: &'a T,
transposition_table: &'a mut dyn TranspositionTable<T>,
stats: Option<&'a Stats>,
) -> impl Iterator<Item = Result<(T::Move, isize), T::MoveError>> + 'a {
cancellation_token: &'a Option<Arc<AtomicBool>>,
) -> impl Iterator<Item = Result<(T::Move, isize), GameSolveError<T>>> + 'a {
game.possible_moves().map(move |m| {
let mut board = game.clone();
board.make_move(&m)?;
board
.make_move(&m)
.map_err(|err| GameSolveError::MoveError(err))?;
// We flip the sign of the score because we want the score from the
// perspective of the player playing the move, not the player whose turn it is.
Ok((m, -solve(&board, transposition_table, stats)?))
Ok((
m,
-solve(&board, transposition_table, stats, cancellation_token)?,
))
})
}

pub type CollectedMoves<T> = Vec<Result<(<T as Game>::Move, isize), <T as Game>::MoveError>>;
pub type CollectedMoves<T> = Vec<Result<(<T as Game>::Move, isize), GameSolveError<T>>>;

/// Parallelized version of `move_scores`. (faster by a large margin)
/// This requires the `rayon` feature to be enabled.
Expand All @@ -248,6 +291,7 @@ pub fn par_move_scores_with_hasher<
>(
game: &T,
stats: Option<&Stats>,
cancellation_token: &Option<Arc<AtomicBool>>,
) -> CollectedMoves<T>
where
T::Move: Sync + Send,
Expand All @@ -266,11 +310,16 @@ where
.par_iter()
.map(move |m| {
let mut board = game.clone();
board.make_move(m)?;
board
.make_move(m)
.map_err(|err| GameSolveError::MoveError::<T>(err))?;
// We flip the sign of the score because we want the score from the
// perspective of the player pla`ying the move, not the player whose turn it is.
let mut map = Arc::clone(&hashmap);
Ok(((*m).clone(), -solve(&board, &mut map, stats)?))
Ok((
(*m).clone(),
-solve(&board, &mut map, stats, cancellation_token)?,
))
})
.collect::<Vec<_>>()
}
Expand All @@ -289,16 +338,17 @@ where
pub fn par_move_scores<T: Game<Player = impl TwoPlayer> + Eq + Hash + Sync + Send + 'static>(
game: &T,
stats: Option<&Stats>,
cancellation_token: &Option<Arc<AtomicBool>>,
) -> CollectedMoves<T>
where
T::Move: Sync + Send,
T::MoveError: Sync + Send,
{
if cfg!(feature = "xxhash") {
use twox_hash::RandomXxHashBuilder64;
par_move_scores_with_hasher::<T, RandomXxHashBuilder64>(game, stats)
par_move_scores_with_hasher::<T, RandomXxHashBuilder64>(game, stats, cancellation_token)
} else {
use std::collections::hash_map::RandomState;
par_move_scores_with_hasher::<T, RandomState>(game, stats)
par_move_scores_with_hasher::<T, RandomState>(game, stats, cancellation_token)
}
}
2 changes: 2 additions & 0 deletions crates/game-solver/src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct Stats {
pub states_explored: AtomicU64,
pub max_depth: AtomicUsize,
pub cache_hits: AtomicU64,
pub pruning_cutoffs: AtomicU64,
pub terminal_ends: TerminalEnds,
}

Expand All @@ -31,6 +32,7 @@ impl Default for Stats {
states_explored: AtomicU64::new(0),
max_depth: AtomicUsize::new(0),
cache_hits: AtomicU64::new(0),
pruning_cutoffs: AtomicU64::new(0),
terminal_ends: TerminalEnds::default(),
}
}
Expand Down
14 changes: 10 additions & 4 deletions crates/games/src/chomp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;

use std::{
fmt::{Display, Formatter},
fmt::{Debug, Display, Formatter},
hash::Hash,
};

Expand Down Expand Up @@ -144,6 +144,12 @@ impl Display for Chomp {
}
}

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

impl TryFrom<ChompArgs> for Chomp {
type Error = Error;

Expand All @@ -163,15 +169,15 @@ impl TryFrom<ChompArgs> for Chomp {
mod tests {
use std::collections::HashMap;

use game_solver::move_scores;
use game_solver::{move_scores, GameSolveError};

use super::*;

#[test]
fn test_chomp() {
let game = Chomp::new(6, 4);
let mut move_scores = move_scores(&game, &mut HashMap::new(), None)
.collect::<Result<Vec<_>, ChompMoveError>>()
let mut move_scores = move_scores(&game, &mut HashMap::new(), None, &None)
.collect::<Result<Vec<_>, GameSolveError<Chomp>>>()
.unwrap();
move_scores.sort();

Expand Down
16 changes: 11 additions & 5 deletions crates/games/src/domineering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ impl<const WIDTH: usize, const HEIGHT: usize> Display for Domineering<WIDTH, HEI
}
}

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

/// Analyzes Domineering.
///
#[doc = include_str!("./README.md")]
Expand Down Expand Up @@ -249,7 +255,7 @@ impl<const WIDTH: usize, const HEIGHT: usize> TryFrom<DomineeringArgs>
mod tests {
use std::collections::HashMap;

use game_solver::move_scores;
use game_solver::{move_scores, GameSolveError};

use super::*;

Expand All @@ -258,8 +264,8 @@ mod tests {
orientation: Orientation,
) -> Option<PartizanPlayer> {
let game = Domineering::<WIDTH, HEIGHT>::new_orientation(orientation);
let mut move_scores = move_scores(&game, &mut HashMap::new(), None)
.collect::<Result<Vec<_>, DomineeringMoveError>>()
let mut move_scores = move_scores(&game, &mut HashMap::new(), None, &None)
.collect::<Result<Vec<_>, GameSolveError<Domineering<WIDTH, HEIGHT>>>>()
.unwrap();

if move_scores.is_empty() {
Expand Down Expand Up @@ -302,8 +308,8 @@ mod tests {
#[test]
fn test_domineering() {
let game = Domineering::<5, 5>::new_orientation(Orientation::Horizontal);
let mut move_scores = move_scores(&game, &mut HashMap::new(), None)
.collect::<Result<Vec<_>, DomineeringMoveError>>()
let mut move_scores = move_scores(&game, &mut HashMap::new(), None, &None)
.collect::<Result<Vec<_>, GameSolveError<Domineering<5, 5>>>>()
.unwrap();

assert_eq!(move_scores.len(), game.possible_moves().len());
Expand Down
11 changes: 10 additions & 1 deletion crates/games/src/nim/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ use game_solver::{
player::ImpartialPlayer,
};
use serde::{Deserialize, Serialize};
use std::{fmt::Display, hash::Hash};
use std::{
fmt::{Debug, Display},
hash::Hash,
};
use thiserror::Error;

use crate::util::{cli::move_failable, move_natural::NaturalMove};
Expand Down Expand Up @@ -123,6 +126,12 @@ impl Display for Nim {
}
}

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

/// Analyzes Nim.
///
#[doc = include_str!("./README.md")]
Expand Down
14 changes: 13 additions & 1 deletion crates/games/src/order_and_chaos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use game_solver::{
};
use serde::{Deserialize, Serialize};
use std::{
fmt::{Display, Formatter},
fmt::{Debug, Display, Formatter},
hash::Hash,
};
use thiserror::Error;
Expand Down Expand Up @@ -283,6 +283,18 @@ impl<
}
}

impl<
const WIDTH: usize,
const HEIGHT: usize,
const MIN_WIN_LENGTH: usize,
const MAX_WIN_LENGTH: usize,
> Debug for OrderAndChaos<WIDTH, HEIGHT, MIN_WIN_LENGTH, MAX_WIN_LENGTH>
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
<Self as Display>::fmt(&self, f)
}
}

/// Analyzes Order and Chaos.
///
#[doc = include_str!("./README.md")]
Expand Down
8 changes: 7 additions & 1 deletion crates/games/src/reversi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use game_solver::{
player::{PartizanPlayer, Player},
};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::{self, Debug};
use std::hash::Hash;

use crate::util::{cli::move_failable, move_natural::NaturalMove};
Expand Down Expand Up @@ -234,6 +234,12 @@ impl fmt::Display for Reversi {
}
}

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

/// Analyzes Reversi.
///
#[doc = include_str!("./README.md")]
Expand Down
14 changes: 10 additions & 4 deletions crates/games/src/tic_tac_toe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;

use std::{
fmt::{Display, Formatter},
fmt::{Debug, Display, Formatter},
hash::Hash,
iter::FilterMap,
};
Expand Down Expand Up @@ -292,15 +292,21 @@ impl Display for TicTacToe {
}
}

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

#[cfg(test)]
mod tests {
use super::*;
use game_solver::move_scores;
use game_solver::{move_scores, GameSolveError};
use std::collections::HashMap;

fn move_scores_unwrapped(game: &TicTacToe) -> Vec<(TicTacToeMove, isize)> {
move_scores(game, &mut HashMap::new(), None)
.collect::<Result<Vec<_>, TicTacToeMoveError>>()
move_scores(game, &mut HashMap::new(), None, &None)
.collect::<Result<Vec<_>, GameSolveError<TicTacToe>>>()
.unwrap()
}

Expand Down
Loading

0 comments on commit 84bc8f0

Please sign in to comment.