yahtzee-engine 0.1.0

Yahtzee rules, scoring, and bots: a fast heuristic and an exact optimal expected-value solver
Documentation
//! Drivers that run strategies through turns and whole games.

use crate::{Game, GameError, ScoreDelta, Strategy, TurnAction};
use rand::Rng;

/// A strategy misbehaved: it returned an action the rules reject.
///
/// The game is left unchanged, so the caller may substitute another
/// action and continue.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum EngineError {
    /// The strategy for `seat` chose an illegal action.
    #[error("illegal action from seat {seat}")]
    IllegalAction {
        /// The offending seat.
        seat: usize,
        /// What the rules objected to.
        #[source]
        source: GameError,
    },
}

/// Plays one complete turn: rolls, consults `strategy`, and scores.
///
/// # Errors
///
/// [`EngineError::IllegalAction`] if the strategy chose an action the
/// rules reject, including acting on a finished game.
pub fn play_turn<R: Rng + ?Sized>(
    game: &mut Game,
    strategy: &mut dyn Strategy,
    rng: &mut R,
) -> Result<ScoreDelta, EngineError> {
    let seat = game.seat();
    let illegal = |source| EngineError::IllegalAction { seat, source };
    loop {
        match game.phase() {
            crate::Phase::Finished => return Err(illegal(GameError::Finished)),
            crate::Phase::AwaitingRoll => {
                game.roll_with(rng).map_err(illegal)?;
            }
            crate::Phase::AwaitingAction => {
                let action = if game.rolls_left() > 0 {
                    strategy.choose_action(&game.view())
                } else {
                    TurnAction::Score(strategy.choose_category(&game.view()))
                };
                if let Some(delta) = game.act(action).map_err(illegal)? {
                    return Ok(delta);
                }
            }
        }
    }
}

/// Plays the game to completion and returns the final totals in seat
/// order.
///
/// # Errors
///
/// [`EngineError::IllegalAction`] if any strategy misbehaves; the game
/// is left at the failing turn.
///
/// # Panics
///
/// Panics if `strategies` does not have one entry per player.
pub fn play_game<R: Rng + ?Sized>(
    game: &mut Game,
    strategies: &mut [&mut dyn Strategy],
    rng: &mut R,
) -> Result<Vec<u16>, EngineError> {
    assert_eq!(
        strategies.len(),
        game.scorecards().len(),
        "one strategy per player"
    );
    while !game.is_over() {
        play_turn(game, strategies[game.seat()], rng)?;
    }
    Ok(game.totals())
}