Skip to content

Commit

Permalink
feat: nim tests, expand to 0
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoDog896 committed Sep 26, 2024
1 parent 84bc8f0 commit 658127b
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 51 deletions.
18 changes: 13 additions & 5 deletions crates/game-solver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,25 @@ fn negamax<T: Game<Player = impl TwoPlayer> + Eq + Hash>(
return Ok(0);
}
GameState::Win(winning_player) => {
// The next player is the winning player - the score should be positive.
// if the next player is the winning player,
// the score should be positive.
if game.player() == winning_player {
if let Some(stats) = stats {
stats.terminal_ends.winning.fetch_add(1, Ordering::Relaxed);
}
return Ok(upper_bound(game) - game.move_count() as isize);
// we add one to make sure games that use up every move
// aren't represented by ties.
//
// take the 2 heap game where each heap has one object in Nim, for example
// player 2 will always win since 2 moves will always be used,
// but since the upper bound is 2, 2 - 2 = 0,
// but we reserve 0 for ties.
return Ok(upper_bound(game) - game.move_count() as isize + 1);
} else {
if let Some(stats) = stats {
stats.terminal_ends.losing.fetch_add(1, Ordering::Relaxed);
}
return Ok(-(upper_bound(game) - game.move_count() as isize));
return Ok(-(upper_bound(game) - game.move_count() as isize + 1));
}
}
};
Expand All @@ -97,12 +105,12 @@ fn negamax<T: Game<Player = impl TwoPlayer> + Eq + Hash>(
if let Some(stats) = stats {
stats.terminal_ends.winning.fetch_add(1, Ordering::Relaxed);
}
return Ok(upper_bound(&board) - board.move_count() as isize);
return Ok(upper_bound(&board) - board.move_count() as isize + 1);
} else {
if let Some(stats) = stats {
stats.terminal_ends.losing.fetch_add(1, Ordering::Relaxed);
}
return Ok(-(upper_bound(&board) - board.move_count() as isize));
return Ok(-(upper_bound(&board) - board.move_count() as isize + 1));
}
}
}
Expand Down
46 changes: 23 additions & 23 deletions crates/games/src/chomp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,29 +182,29 @@ mod tests {
move_scores.sort();

let mut new_scores = vec![
(NaturalMove([2, 2]), 13),
(NaturalMove([5, 0]), -12),
(NaturalMove([4, 0]), -12),
(NaturalMove([3, 0]), -12),
(NaturalMove([2, 0]), -12),
(NaturalMove([0, 0]), -12),
(NaturalMove([5, 1]), -12),
(NaturalMove([4, 1]), -12),
(NaturalMove([3, 1]), -12),
(NaturalMove([2, 1]), -12),
(NaturalMove([0, 1]), -12),
(NaturalMove([5, 2]), -12),
(NaturalMove([4, 2]), -12),
(NaturalMove([3, 2]), -12),
(NaturalMove([5, 3]), -12),
(NaturalMove([1, 0]), -16),
(NaturalMove([1, 1]), -16),
(NaturalMove([1, 2]), -16),
(NaturalMove([4, 3]), -16),
(NaturalMove([3, 3]), -16),
(NaturalMove([2, 3]), -16),
(NaturalMove([0, 2]), -22),
(NaturalMove([1, 3]), -22),
(NaturalMove([2, 2]), 14),
(NaturalMove([5, 0]), -13),
(NaturalMove([4, 0]), -13),
(NaturalMove([3, 0]), -13),
(NaturalMove([2, 0]), -13),
(NaturalMove([0, 0]), -13),
(NaturalMove([5, 1]), -13),
(NaturalMove([4, 1]), -13),
(NaturalMove([3, 1]), -13),
(NaturalMove([2, 1]), -13),
(NaturalMove([0, 1]), -13),
(NaturalMove([5, 2]), -13),
(NaturalMove([4, 2]), -13),
(NaturalMove([3, 2]), -13),
(NaturalMove([5, 3]), -13),
(NaturalMove([1, 0]), -17),
(NaturalMove([1, 1]), -17),
(NaturalMove([1, 2]), -17),
(NaturalMove([4, 3]), -17),
(NaturalMove([3, 3]), -17),
(NaturalMove([2, 3]), -17),
(NaturalMove([0, 2]), -23),
(NaturalMove([1, 3]), -23),
];

new_scores.sort();
Expand Down
40 changes: 20 additions & 20 deletions crates/games/src/domineering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,26 +317,26 @@ mod tests {
move_scores.sort();

let mut current_scores = vec![
(DomineeringMove(3, 4), -13),
(DomineeringMove(0, 4), -13),
(DomineeringMove(3, 3), -13),
(DomineeringMove(2, 3), -13),
(DomineeringMove(1, 3), -13),
(DomineeringMove(0, 3), -13),
(DomineeringMove(3, 2), -13),
(DomineeringMove(0, 2), -13),
(DomineeringMove(3, 1), -13),
(DomineeringMove(2, 1), -13),
(DomineeringMove(1, 1), -13),
(DomineeringMove(0, 1), -13),
(DomineeringMove(3, 0), -13),
(DomineeringMove(0, 0), -13),
(DomineeringMove(2, 4), -15),
(DomineeringMove(1, 4), -15),
(DomineeringMove(2, 2), -15),
(DomineeringMove(1, 2), -15),
(DomineeringMove(2, 0), -15),
(DomineeringMove(1, 0), -15),
(DomineeringMove(3, 4), -14),
(DomineeringMove(0, 4), -14),
(DomineeringMove(3, 3), -14),
(DomineeringMove(2, 3), -14),
(DomineeringMove(1, 3), -14),
(DomineeringMove(0, 3), -14),
(DomineeringMove(3, 2), -14),
(DomineeringMove(0, 2), -14),
(DomineeringMove(3, 1), -14),
(DomineeringMove(2, 1), -14),
(DomineeringMove(1, 1), -14),
(DomineeringMove(0, 1), -14),
(DomineeringMove(3, 0), -14),
(DomineeringMove(0, 0), -14),
(DomineeringMove(2, 4), -16),
(DomineeringMove(1, 4), -16),
(DomineeringMove(2, 2), -16),
(DomineeringMove(1, 2), -16),
(DomineeringMove(2, 0), -16),
(DomineeringMove(1, 0), -16),
];

current_scores.sort();
Expand Down
43 changes: 43 additions & 0 deletions crates/games/src/nim/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,53 @@ impl TryFrom<NimArgs> for Nim {

#[cfg(test)]
mod tests {
use std::collections::HashMap;

use game_solver::{move_scores, CollectedMoves};
use itertools::Itertools;

use crate::util::move_score::best_move_score_testing;

use super::*;

fn play(nim: Nim) -> CollectedMoves<Nim> {
move_scores(&nim, &mut HashMap::new(), None, &None).collect_vec()
}

#[test]
fn max_moves_is_heap_sum() {
assert_eq!(Nim::new(vec![3, 5, 7]).max_moves(), Some(3 + 5 + 7));
assert_eq!(Nim::new(vec![0, 2, 2]).max_moves(), Some(0 + 2 + 2));
}

#[test]
fn single_heap() {
// p1 always wins for single-heap stacks
// the score is equivalent to the stack, as we can make 1 move to win,
// and score is always (max moves - moves made (+ 1 to account for ties))
assert_eq!(best_move_score_testing(play(Nim::new(vec![7]))).1, 7);
assert_eq!(best_move_score_testing(play(Nim::new(vec![4]))).1, 4);
assert_eq!(best_move_score_testing(play(Nim::new(vec![20]))).1, 20);
}

#[test]
fn empty_heap() {
// unless the heaps have nothing, in which we cant play
assert!(play(Nim::new(vec![0])).is_empty());
assert!(play(Nim::new(vec![0, 0])).is_empty());
}

#[test]
fn symmetrical_nim_wins() {
// a loss in 4 moves: take 1, other player takes from other, take 1, other player takes from other
assert_eq!(best_move_score_testing(play(Nim::new(vec![2, 2]))).1, -1);

// generalize this for more cases:
assert_eq!(best_move_score_testing(play(Nim::new(vec![6, 6]))).1, -1);
assert_eq!(
best_move_score_testing(play(Nim::new(vec![5, 5, 3, 3]))).1,
-1
);
assert_eq!(best_move_score_testing(play(Nim::new(vec![7, 7]))).1, -1);
}
}
7 changes: 4 additions & 3 deletions crates/games/src/util/cli/human.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use std::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread, time::Duration,
thread,
time::Duration,
};

use anyhow::Result;
Expand Down Expand Up @@ -146,7 +147,7 @@ impl Widget for &App {
.load(Ordering::Relaxed)
.to_string()
.yellow(),
")".into()
")".into(),
]),
// TODO: depth
// Line::from(vec![
Expand Down Expand Up @@ -192,7 +193,7 @@ where
let mut app = App {
exit: exit.clone(),
stats: stats.clone(),
exit_ui: exit_ui.clone()
exit_ui: exit_ui.clone(),
};

let game_thread = thread::spawn(move || {
Expand Down
19 changes: 19 additions & 0 deletions crates/games/src/util/move_score.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use game_solver::{game::Game, CollectedMoves, GameSolveError};

/// Takes a move list and returns a sorted list, from positive to negative, of
/// the scores.
pub fn normalize_move_scores<T: Game>(
move_scores: CollectedMoves<T>,
) -> Result<Vec<(T::Move, isize)>, GameSolveError<T>> {
Expand All @@ -12,3 +14,20 @@ pub fn normalize_move_scores<T: Game>(

Ok(move_scores)
}

pub fn best_move_score<T: Game>(
move_scores: CollectedMoves<T>,
) -> Result<Option<(T::Move, isize)>, GameSolveError<T>> {
let move_scores = move_scores
.into_iter()
.collect::<Result<Vec<_>, GameSolveError<T>>>()?;

Ok(move_scores.iter().max_by_key(|x| x.1).cloned())
}

#[cfg(test)]
pub fn best_move_score_testing<T: Game + std::fmt::Debug>(
move_scores: CollectedMoves<T>,
) -> (T::Move, isize) {
best_move_score(move_scores).unwrap().unwrap()
}

0 comments on commit 658127b

Please sign in to comment.