Skip to content

Commit

Permalink
feat: full move control!
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoDog896 committed Sep 25, 2024
1 parent 02d90f9 commit 9b97653
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 17 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"Cargo.toml"
],
"rust-analyzer.check.features": "all",
"rust-analyzer.cargo.features": "all"
"rust-analyzer.cargo.features": "all",
"typos.config": "_typos.toml"
}
3 changes: 3 additions & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[default.extend-words]
nd = "nd"
misere = "misere"
58 changes: 52 additions & 6 deletions crates/game-solver/src/game.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Game trait and related types.
use std::cmp::Ordering;

use crate::player::Player;

/// Represents a move outcome
Expand Down Expand Up @@ -92,20 +94,40 @@ 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<Option<Self>, Self::MoveError> {
let mut best_non_winning_game: Option<Self> = None;

for m in &mut self.possible_moves() {
let mut new_self = self.clone();
new_self.make_move(&m)?;
if let GameState::Win(_) = new_self.state() {
return Ok(Some(new_self));
}
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)
}
};
}

Ok(None)
Ok(best_non_winning_game)
}

/// Returns the current state of the game.
/// Used for verifying initialization and isn't commonly called.
/// Used for verifying initialization and is commonly called.
///
/// If `Self::STATE_TYPE` isn't None,
/// the following implementation can be used:
Expand Down Expand Up @@ -150,7 +172,31 @@ pub trait Game: Clone {
fn player(&self) -> Self::Player;
}

/// Utility function to get the upper bound of a game.
/// 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<T: Game>(game: &T) -> isize {
game.max_moves().map_or(isize::MAX, |m| m as isize)
}

/// Represents an outcome of a game derived by a score and a valid instance of a game.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum GameScoreOutcome {
Win(usize),
Loss(usize),
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<T: Game>(game: &T, score: isize) -> GameScoreOutcome {
match score.cmp(&0) {
Ordering::Greater => GameScoreOutcome::Win((-score + upper_bound(game)) as usize),
Ordering::Equal => GameScoreOutcome::Tie,
Ordering::Less => GameScoreOutcome::Loss((score + upper_bound(game)) as usize)
}
}
13 changes: 12 additions & 1 deletion crates/game-solver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod player;
// pub mod reinforcement;
pub mod transposition;

use core::panic;
#[cfg(feature = "rayon")]
use std::hash::BuildHasher;

Expand All @@ -29,6 +30,8 @@ fn negamax<T: Game<Player = impl TwoPlayer> + Eq + Hash>(
mut alpha: isize,
mut beta: isize,
) -> Result<isize, T::MoveError> {
// TODO(perf): if find_immediately_resolvable_game satisfies its contract,
// we can ignore this at larger depths.
match game.state() {
GameState::Playable => (),
GameState::Tie => return Ok(0),
Expand All @@ -44,7 +47,15 @@ fn negamax<T: Game<Player = impl TwoPlayer> + Eq + Hash>(

// check if this is a winning configuration
if let Ok(Some(board)) = game.find_immediately_resolvable_game() {
return Ok(upper_bound(&board) - board.move_count() as isize + 1);
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));
}
}
}

// fetch values from the transposition table
Expand Down
30 changes: 25 additions & 5 deletions crates/game-solver/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ pub trait Player: Sized + Eq {
/// The previous player to play
#[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;
}

/// Represents a two player player.
Expand Down Expand Up @@ -57,6 +65,10 @@ impl Player for PartizanPlayer {
fn previous(self) -> Self {
self.next()
}

fn turn(self) -> Self {
self
}
}

impl TwoPlayer for PartizanPlayer {}
Expand Down Expand Up @@ -95,27 +107,31 @@ impl Player for ImpartialPlayer {
fn previous(self) -> Self {
self.next()
}

fn turn(self) -> Self {
self.next()
}
}

impl TwoPlayer for ImpartialPlayer {}

/// Represents a player in an N-player game.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub struct NPlayerConst<const N: usize>(usize);
pub struct NPlayerPartizanConst<const N: usize>(usize);

impl<const N: usize> NPlayerConst<N> {
pub fn new(index: usize) -> NPlayerConst<N> {
impl<const N: usize> NPlayerPartizanConst<N> {
pub fn new(index: usize) -> NPlayerPartizanConst<N> {
assert!(index < N, "Player index {index} >= max player count {N}");
Self(index)
}

pub fn new_unchecked(index: usize) -> NPlayerConst<N> {
pub fn new_unchecked(index: usize) -> NPlayerPartizanConst<N> {
debug_assert!(index < N, "Player index {index} >= max player count {N}");
Self(index)
}
}

impl<const N: usize> Player for NPlayerConst<N> {
impl<const N: usize> Player for NPlayerPartizanConst<N> {
fn count() -> usize {
N
}
Expand All @@ -136,4 +152,8 @@ impl<const N: usize> Player for NPlayerConst<N> {
Self::new_unchecked(self.0 - 1)
}
}

fn turn(self) -> Self {
self
}
}
8 changes: 6 additions & 2 deletions crates/games/src/util/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use core::hash::Hash;
use game_solver::{
game::{Game, GameState},
game::{score_to_outcome, Game, GameScoreOutcome, GameState},
par_move_scores,
player::{ImpartialPlayer, TwoPlayer},
};
Expand Down Expand Up @@ -39,7 +39,11 @@ pub fn play<
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);
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:")
}
current_move_score = Some(score);
}
print!("{}, ", &game_move);
Expand Down
2 changes: 0 additions & 2 deletions typos.toml

This file was deleted.

0 comments on commit 9b97653

Please sign in to comment.