use crate::{Player, RoundResult, Rules, Shutout};
use thiserror::Error;
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum GameError {
#[error("The game is already over")]
AlreadyOver,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct FinalScore {
pub winner: Player,
pub totals: [u16; 2],
pub shutout: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Game {
rules: Rules,
scores: [u16; 2],
boxes: [u16; 2],
next_dealer: Player,
}
impl Game {
#[must_use]
#[inline]
pub const fn new(rules: Rules, first_dealer: Player) -> Self {
Self {
rules,
scores: [0; 2],
boxes: [0; 2],
next_dealer: first_dealer,
}
}
#[must_use]
#[inline]
pub const fn rules(&self) -> &Rules {
&self.rules
}
#[must_use]
#[inline]
pub const fn score(&self, player: Player) -> u16 {
self.scores[player as usize]
}
#[must_use]
#[inline]
pub const fn boxes(&self, player: Player) -> u16 {
self.boxes[player as usize]
}
#[must_use]
#[inline]
pub const fn next_dealer(&self) -> Player {
self.next_dealer
}
#[must_use]
pub const fn is_over(&self) -> bool {
self.scores[0] >= self.rules.game_target || self.scores[1] >= self.rules.game_target
}
#[must_use]
pub const fn winner(&self) -> Option<Player> {
if self.scores[0] >= self.rules.game_target {
Some(Player::One)
} else if self.scores[1] >= self.rules.game_target {
Some(Player::Two)
} else {
None
}
}
pub fn record(&mut self, result: RoundResult) -> Result<(), GameError> {
if self.is_over() {
return Err(GameError::AlreadyOver);
}
if let Some(winner) = result.winner() {
let immediate = if self.rules.immediate_boxes {
self.rules.box_bonus
} else {
0
};
self.scores[winner as usize] += result.points(&self.rules) + immediate;
self.boxes[winner as usize] += 1;
self.next_dealer = winner;
}
Ok(())
}
#[cfg(feature = "rand")]
#[must_use]
pub fn deal(&self, rng: &mut (impl rand::Rng + ?Sized)) -> crate::Round {
crate::Round::deal(self.rules, self.next_dealer, rng)
}
#[must_use]
pub fn final_score(&self) -> Option<FinalScore> {
let winner = self.winner()?;
let loser = winner.opponent();
let shutout = self.scores[loser as usize] == 0;
let mut totals = self.scores;
if !self.rules.immediate_boxes {
for player in Player::ALL {
totals[player as usize] += self.boxes[player as usize] * self.rules.box_bonus;
}
}
totals[winner as usize] += self.rules.game_bonus;
if shutout {
totals[winner as usize] = match self.rules.shutout {
Shutout::Double => totals[winner as usize] * 2,
Shutout::Flat(bonus) => totals[winner as usize] + bonus,
};
}
Some(FinalScore {
winner,
totals,
shutout,
})
}
}