Skip to main content

yahtzee_engine/
driver.rs

1//! Drivers that run strategies through turns and whole games.
2
3use crate::{Game, GameError, ScoreDelta, Strategy, TurnAction};
4use rand::Rng;
5
6/// A strategy misbehaved: it returned an action the rules reject.
7///
8/// The game is left unchanged, so the caller may substitute another
9/// action and continue.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
11#[non_exhaustive]
12pub enum EngineError {
13    /// The strategy for `seat` chose an illegal action.
14    #[error("illegal action from seat {seat}")]
15    IllegalAction {
16        /// The offending seat.
17        seat: usize,
18        /// What the rules objected to.
19        #[source]
20        source: GameError,
21    },
22}
23
24/// Plays one complete turn: rolls, consults `strategy`, and scores.
25///
26/// # Errors
27///
28/// [`EngineError::IllegalAction`] if the strategy chose an action the
29/// rules reject, including acting on a finished game.
30pub fn play_turn<R: Rng + ?Sized>(
31    game: &mut Game,
32    strategy: &mut dyn Strategy,
33    rng: &mut R,
34) -> Result<ScoreDelta, EngineError> {
35    let seat = game.seat();
36    let illegal = |source| EngineError::IllegalAction { seat, source };
37    loop {
38        match game.phase() {
39            crate::Phase::Finished => return Err(illegal(GameError::Finished)),
40            crate::Phase::AwaitingRoll => {
41                game.roll_with(rng).map_err(illegal)?;
42            }
43            crate::Phase::AwaitingAction => {
44                let action = if game.rolls_left() > 0 {
45                    strategy.choose_action(&game.view())
46                } else {
47                    TurnAction::Score(strategy.choose_category(&game.view()))
48                };
49                if let Some(delta) = game.act(action).map_err(illegal)? {
50                    return Ok(delta);
51                }
52            }
53        }
54    }
55}
56
57/// Plays the game to completion and returns the final totals in seat
58/// order.
59///
60/// # Errors
61///
62/// [`EngineError::IllegalAction`] if any strategy misbehaves; the game
63/// is left at the failing turn.
64///
65/// # Panics
66///
67/// Panics if `strategies` does not have one entry per player.
68pub fn play_game<R: Rng + ?Sized>(
69    game: &mut Game,
70    strategies: &mut [&mut dyn Strategy],
71    rng: &mut R,
72) -> Result<Vec<u16>, EngineError> {
73    assert_eq!(
74        strategies.len(),
75        game.scorecards().len(),
76        "one strategy per player"
77    );
78    while !game.is_over() {
79        play_turn(game, strategies[game.seat()], rng)?;
80    }
81    Ok(game.totals())
82}