use crate::{Category, Dice, Keep, ScoreDelta, Scorecard, TurnAction, View};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Phase {
AwaitingRoll,
AwaitingAction,
Finished,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum GameError {
#[error("dice must be rolled before acting")]
RollPending,
#[error("an action is pending; the dice have already been rolled")]
ActionPending,
#[error("roll does not contain the held dice {kept}")]
KeptNotHeld {
kept: Keep,
},
#[error("keep {keep} is not part of the current dice")]
KeepNotInDice {
keep: Keep,
},
#[error("no rerolls left this turn")]
NoRerollsLeft,
#[error("cannot score {category} here")]
IllegalCategory {
category: Category,
},
#[error("the game is finished")]
Finished,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Game {
scorecards: Vec<Scorecard>,
seat: usize,
round: u8,
kept: Keep,
dice: Option<Dice>,
rolls_used: u8,
}
impl Game {
#[must_use]
pub fn new(players: usize) -> Self {
assert!(players > 0, "a game needs at least one player");
Self {
scorecards: vec![Scorecard::new(); players],
seat: 0,
round: 0,
kept: Keep::EMPTY,
dice: None,
rolls_used: 0,
}
}
#[must_use]
pub const fn phase(&self) -> Phase {
if self.round >= 13 {
Phase::Finished
} else if self.dice.is_none() {
Phase::AwaitingRoll
} else {
Phase::AwaitingAction
}
}
#[must_use]
pub const fn is_over(&self) -> bool {
self.round >= 13
}
#[must_use]
pub const fn seat(&self) -> usize {
self.seat
}
#[must_use]
pub const fn round(&self) -> u8 {
self.round
}
#[must_use]
pub const fn rolls_left(&self) -> u8 {
if self.is_over() {
0
} else {
3 - self.rolls_used
}
}
#[must_use]
pub const fn kept(&self) -> Keep {
self.kept
}
#[must_use]
pub const fn dice(&self) -> Option<Dice> {
self.dice
}
#[must_use]
pub fn scorecard(&self, seat: usize) -> &Scorecard {
&self.scorecards[seat]
}
#[must_use]
pub fn scorecards(&self) -> &[Scorecard] {
&self.scorecards
}
#[must_use]
pub fn totals(&self) -> Vec<u16> {
self.scorecards.iter().map(|card| card.total()).collect()
}
#[must_use]
pub const fn view(&self) -> View<'_> {
View::new(self)
}
pub fn roll(&mut self, dice: Dice) -> Result<(), GameError> {
match self.phase() {
Phase::Finished => Err(GameError::Finished),
Phase::AwaitingAction => Err(GameError::ActionPending),
Phase::AwaitingRoll if !dice.contains(self.kept) => {
Err(GameError::KeptNotHeld { kept: self.kept })
}
Phase::AwaitingRoll => {
self.dice = Some(dice);
self.rolls_used += 1;
Ok(())
}
}
}
#[cfg(feature = "rand")]
pub fn roll_with<R: rand::Rng + ?Sized>(&mut self, rng: &mut R) -> Result<Dice, GameError> {
use rand::RngExt as _;
let mut counts = self.kept.counts();
for _ in self.kept.len()..5 {
counts[rng.random_range(0..6)] += 1;
}
let dice = Dice::from_counts(counts).expect("kept dice plus rerolls make five");
self.roll(dice).map(|()| dice)
}
pub fn act(&mut self, action: TurnAction) -> Result<Option<ScoreDelta>, GameError> {
let dice = match self.phase() {
Phase::Finished => return Err(GameError::Finished),
Phase::AwaitingRoll => return Err(GameError::RollPending),
Phase::AwaitingAction => self.dice.expect("dice are showing"),
};
match action {
TurnAction::Reroll(_) if self.rolls_left() == 0 => Err(GameError::NoRerollsLeft),
TurnAction::Reroll(keep) if !dice.contains(keep) => {
Err(GameError::KeepNotInDice { keep })
}
TurnAction::Reroll(keep) => {
self.kept = keep;
self.dice = None;
Ok(None)
}
TurnAction::Score(category) => {
let delta = self.scorecards[self.seat].score(category, dice)?;
self.kept = Keep::EMPTY;
self.dice = None;
self.rolls_used = 0;
self.seat += 1;
if self.seat == self.scorecards.len() {
self.seat = 0;
self.round += 1;
}
Ok(Some(delta))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dice(s: &str) -> Dice {
s.parse().expect("a valid roll")
}
fn keep(s: &str) -> Keep {
s.parse().expect("a valid keep")
}
#[test]
fn a_turn_runs_roll_keep_roll_keep_roll_score() {
let mut game = Game::new(1);
assert_eq!(game.phase(), Phase::AwaitingRoll);
assert_eq!(game.rolls_left(), 3);
game.roll(dice("13446")).expect("first roll");
assert_eq!(game.rolls_left(), 2);
game.act(TurnAction::Reroll(keep("44")))
.expect("hold fours");
assert_eq!(game.phase(), Phase::AwaitingRoll);
game.roll(dice("24445")).expect("second roll");
game.act(TurnAction::Reroll(keep("444")))
.expect("hold fours");
game.roll(dice("14446")).expect("third roll");
assert_eq!(game.rolls_left(), 0);
let delta = game
.act(TurnAction::Score(Category::FourOfAKind))
.expect("scoring is always legal");
assert_eq!(delta.expect("a score").value, 0);
assert_eq!(game.round(), 1);
assert_eq!(game.phase(), Phase::AwaitingRoll);
}
#[test]
fn rejections_leave_the_game_unchanged() {
let mut game = Game::new(1);
assert_eq!(
game.act(TurnAction::Score(Category::Chance)),
Err(GameError::RollPending)
);
game.roll(dice("13446")).expect("first roll");
let before = game.clone();
assert_eq!(game.roll(dice("13446")), Err(GameError::ActionPending));
assert_eq!(
game.act(TurnAction::Reroll(keep("55"))),
Err(GameError::KeepNotInDice { keep: keep("55") })
);
assert_eq!(game, before);
game.act(TurnAction::Reroll(keep("446"))).expect("legal");
assert_eq!(
game.roll(dice("11123")),
Err(GameError::KeptNotHeld { kept: keep("446") })
);
game.roll(dice("44612")).expect("second roll");
game.act(TurnAction::Reroll(keep("446"))).expect("legal");
game.roll(dice("44613")).expect("third roll");
assert_eq!(
game.act(TurnAction::Reroll(Keep::EMPTY)),
Err(GameError::NoRerollsLeft)
);
}
#[test]
fn players_alternate_and_the_game_ends_after_13_rounds() {
let mut game = Game::new(2);
for round in 0..13 {
for seat in 0..2 {
assert_eq!(game.seat(), seat);
assert_eq!(game.round(), round);
game.roll(dice("13446")).expect("a roll");
let category = Category::ALL[usize::from(round)];
game.act(TurnAction::Score(category)).expect("open box");
}
}
assert!(game.is_over());
assert_eq!(game.phase(), Phase::Finished);
assert_eq!(game.rolls_left(), 0);
assert_eq!(game.roll(dice("13446")), Err(GameError::Finished));
assert_eq!(game.totals().len(), 2);
}
}