use crate::error::BowlingError;
use crate::frame::{
BallOne, BallThree, BallTwo, FinalAfterOne, FinalAfterThree, FinalAfterTwo, FinalFillThree,
FinalFillTwo, FinalFrame, FrameNumber, FramePhase, RegAfterOne, RegAfterTwo, RegularFrame,
ScoredFrame,
};
use crate::pins::PinSet;
use crate::player::Player;
use crate::roll::Roll;
use crate::ruleset::Ruleset;
use crate::scorecard::ScoreCard;
use crate::scoring::{self, Scoreboard};
pub trait GamePhase<R: Ruleset>: std::fmt::Debug {
type Live: std::fmt::Debug;
}
#[derive(Debug)]
pub struct AwaitingRoll;
#[derive(Debug)]
pub struct Complete;
impl<R: Ruleset> GamePhase<R> for AwaitingRoll {
type Live = ActiveState<R>;
}
impl<R: Ruleset> GamePhase<R> for Complete {
type Live = ();
}
#[derive(Debug)]
pub struct ActiveState<R: Ruleset> {
current_player: usize,
current_frame_number: FrameNumber,
frame_state: FrameState<R>,
}
#[derive(Debug, Clone)]
pub struct Competitor {
player: Player,
card: ScoreCard,
}
impl Competitor {
pub fn player(&self) -> &Player {
&self.player
}
pub fn card(&self) -> &ScoreCard {
&self.card
}
}
#[derive(Debug)]
enum FrameState<R: Ruleset> {
RegularBallOne(RegularFrame<R, BallOne>),
RegularBallTwo(RegularFrame<R, BallTwo>),
RegularBallThree(RegularFrame<R, BallThree>),
FinalBallOne(FinalFrame<R, BallOne>),
FinalBallTwo(FinalFrame<R, BallTwo>),
FinalBallThree(FinalFrame<R, BallThree>),
FinalFillTwo(FinalFrame<R, FinalFillTwo>),
FinalFillThree(FinalFrame<R, FinalFillThree>),
}
impl<R: Ruleset> FrameState<R> {
fn standing(&self) -> PinSet {
match self {
Self::RegularBallOne(f) => f.standing(),
Self::RegularBallTwo(f) => f.standing(),
Self::RegularBallThree(f) => f.standing(),
Self::FinalBallOne(f) => f.standing(),
Self::FinalBallTwo(f) => f.standing(),
Self::FinalBallThree(f) => f.standing(),
Self::FinalFillTwo(f) => f.standing(),
Self::FinalFillThree(f) => f.standing(),
}
}
fn rolls(&self) -> &[Roll] {
match self {
Self::RegularBallOne(f) => f.phase().rolls(),
Self::RegularBallTwo(f) => f.phase().rolls(),
Self::RegularBallThree(f) => f.phase().rolls(),
Self::FinalBallOne(f) => f.phase().rolls(),
Self::FinalBallTwo(f) => f.phase().rolls(),
Self::FinalBallThree(f) => f.phase().rolls(),
Self::FinalFillTwo(f) => f.phase().rolls(),
Self::FinalFillThree(f) => f.phase().rolls(),
}
}
fn ball_number(&self) -> u8 {
match self {
Self::RegularBallOne(_) | Self::FinalBallOne(_) => 1,
Self::RegularBallTwo(_) | Self::FinalBallTwo(_) | Self::FinalFillTwo(_) => 2,
Self::RegularBallThree(_) | Self::FinalBallThree(_) | Self::FinalFillThree(_) => 3,
}
}
fn new_frame(frame_number: FrameNumber) -> Self {
if frame_number.get() == R::FRAME_COUNT {
Self::FinalBallOne(FinalFrame::new(frame_number))
} else {
Self::RegularBallOne(RegularFrame::new(frame_number))
}
}
fn roll(self, delivery: Roll) -> Result<FrameRollResult<R>, BowlingError> {
match self {
Self::RegularBallOne(f) => match f.roll(delivery)? {
RegAfterOne::Continue(f2) => {
Ok(FrameRollResult::Continue(Self::RegularBallTwo(f2)))
}
RegAfterOne::Done(sf) => Ok(FrameRollResult::Done(sf)),
},
Self::RegularBallTwo(f) => match f.roll(delivery)? {
RegAfterTwo::Continue(f3) => {
Ok(FrameRollResult::Continue(Self::RegularBallThree(f3)))
}
RegAfterTwo::Done(sf) => Ok(FrameRollResult::Done(sf)),
},
Self::RegularBallThree(f) => Ok(FrameRollResult::Done(f.roll(delivery)?)),
Self::FinalBallOne(f) => match f.roll(delivery)? {
FinalAfterOne::ToBallTwo(f2) => {
Ok(FrameRollResult::Continue(Self::FinalBallTwo(f2)))
}
FinalAfterOne::ToFillTwo(f2) => {
Ok(FrameRollResult::Continue(Self::FinalFillTwo(f2)))
}
},
Self::FinalBallTwo(f) => match f.roll(delivery)? {
FinalAfterTwo::ToBallThree(f3) => {
Ok(FrameRollResult::Continue(Self::FinalBallThree(f3)))
}
FinalAfterTwo::ToFillThree(f3) => {
Ok(FrameRollResult::Continue(Self::FinalFillThree(f3)))
}
FinalAfterTwo::Done(sf) => Ok(FrameRollResult::Done(sf)),
},
Self::FinalBallThree(f) => match f.roll(delivery)? {
FinalAfterThree::ToFillThree(f3) => {
Ok(FrameRollResult::Continue(Self::FinalFillThree(f3)))
}
FinalAfterThree::Done(sf) => Ok(FrameRollResult::Done(sf)),
},
Self::FinalFillTwo(f) => {
let f3 = f.roll(delivery)?;
Ok(FrameRollResult::Continue(Self::FinalFillThree(f3)))
}
Self::FinalFillThree(f) => Ok(FrameRollResult::Done(f.roll(delivery)?)),
}
}
}
enum FrameRollResult<R: Ruleset> {
Continue(FrameState<R>),
Done(ScoredFrame),
}
pub struct Game<R: Ruleset, P: GamePhase<R>> {
competitors: Vec<Competitor>,
live: P::Live,
}
impl<R: Ruleset, P: GamePhase<R>> std::fmt::Debug for Game<R, P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Game")
.field("ruleset", &R::name())
.field("competitors", &self.competitors.len())
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub enum Progress<R: Ruleset> {
AwaitingRoll(Game<R, AwaitingRoll>),
Complete(Game<R, Complete>),
}
impl<R: Ruleset> From<Game<R, AwaitingRoll>> for Progress<R> {
fn from(game: Game<R, AwaitingRoll>) -> Self {
Self::AwaitingRoll(game)
}
}
impl<R: Ruleset> Progress<R> {
pub fn is_complete(&self) -> bool {
matches!(self, Self::Complete(_))
}
pub fn competitors(&self) -> &[Competitor] {
match self {
Self::AwaitingRoll(g) => g.competitors(),
Self::Complete(g) => g.competitors(),
}
}
pub fn competitor_count(&self) -> usize {
match self {
Self::AwaitingRoll(g) => g.competitor_count(),
Self::Complete(g) => g.competitor_count(),
}
}
pub fn player(&self, index: usize) -> &Player {
match self {
Self::AwaitingRoll(g) => g.player(index),
Self::Complete(g) => g.player(index),
}
}
pub fn scorecard(&self, index: usize) -> &ScoreCard {
match self {
Self::AwaitingRoll(g) => g.scorecard(index),
Self::Complete(g) => g.scorecard(index),
}
}
pub fn scoreboard(&self, index: usize) -> Scoreboard {
match self {
Self::AwaitingRoll(g) => g.scoreboard(index),
Self::Complete(g) => g.scoreboard(index),
}
}
pub fn scoreboards(&self) -> Vec<Scoreboard> {
match self {
Self::AwaitingRoll(g) => g.scoreboards(),
Self::Complete(g) => g.scoreboards(),
}
}
}
#[derive(Debug)]
pub struct GameBuilder<R: Ruleset> {
competitors: Vec<Competitor>,
_ruleset: std::marker::PhantomData<R>,
}
impl<R: Ruleset> GameBuilder<R> {
pub fn new(player: Player) -> Self {
Self {
competitors: vec![Competitor {
player,
card: ScoreCard::new(),
}],
_ruleset: std::marker::PhantomData,
}
}
pub fn add_player(mut self, player: Player) -> Self {
self.competitors.push(Competitor {
player,
card: ScoreCard::new(),
});
self
}
pub fn build(self) -> Progress<R> {
let first_frame = const { FrameNumber::new(1).unwrap() };
let frame_state = FrameState::new_frame(first_frame);
Progress::AwaitingRoll(Game {
competitors: self.competitors,
live: ActiveState {
current_player: 0,
current_frame_number: first_frame,
frame_state,
},
})
}
}
impl<R: Ruleset, P: GamePhase<R>> Game<R, P> {
pub fn competitors(&self) -> &[Competitor] {
&self.competitors
}
pub fn competitor_count(&self) -> usize {
self.competitors.len()
}
pub fn player(&self, index: usize) -> &Player {
&self.competitors[index].player
}
pub fn scorecard(&self, index: usize) -> &ScoreCard {
&self.competitors[index].card
}
pub fn scoreboard(&self, index: usize) -> Scoreboard {
scoring::compute_scoreboard::<R>(self.competitors[index].card.frames())
}
pub fn scoreboards(&self) -> Vec<Scoreboard> {
self.competitors
.iter()
.map(|c| scoring::compute_scoreboard::<R>(c.card.frames()))
.collect()
}
}
impl<R: Ruleset> Game<R, AwaitingRoll> {
pub fn current_player(&self) -> &Player {
&self.competitors[self.live.current_player].player
}
pub fn current_frame_number(&self) -> FrameNumber {
self.live.current_frame_number
}
pub fn current_player_index(&self) -> usize {
self.live.current_player
}
pub fn current_frame_rolls(&self) -> &[Roll] {
self.live.frame_state.rolls()
}
pub fn current_ball_number(&self) -> u8 {
self.live.frame_state.ball_number()
}
pub fn roll_count(self, n: u8) -> Result<Progress<R>, BowlingError> {
let standing = self.standing_pins();
let knocked = pick_n_from(standing, n);
let delivery = Roll::clean(knocked);
self.roll(delivery)
}
pub fn standing_pins(&self) -> PinSet {
self.live.frame_state.standing()
}
pub fn roll(self, delivery: Roll) -> Result<Progress<R>, BowlingError> {
let mut competitors = self.competitors;
let ActiveState {
current_player,
current_frame_number,
frame_state,
} = self.live;
match frame_state.roll(delivery)? {
FrameRollResult::Continue(next_state) => {
Ok(Progress::AwaitingRoll(Game {
competitors,
live: ActiveState {
current_player,
current_frame_number,
frame_state: next_state,
},
}))
}
FrameRollResult::Done(scored) => {
competitors[current_player].card.push_frame(scored);
Ok(advance_turn::<R>(
competitors,
current_player,
current_frame_number,
))
}
}
}
}
fn advance_turn<R: Ruleset>(
competitors: Vec<Competitor>,
current_player: usize,
current_frame_number: FrameNumber,
) -> Progress<R> {
let next_player = current_player + 1;
if next_player < competitors.len() {
Progress::AwaitingRoll(Game {
competitors,
live: ActiveState {
current_player: next_player,
current_frame_number,
frame_state: FrameState::new_frame(current_frame_number),
},
})
} else {
match current_frame_number.checked_add(1) {
Some(next_frame) if next_frame.get() <= R::FRAME_COUNT => {
Progress::AwaitingRoll(Game {
competitors,
live: ActiveState {
current_player: 0,
current_frame_number: next_frame,
frame_state: FrameState::new_frame(next_frame),
},
})
}
_ => {
Progress::Complete(Game {
competitors,
live: (),
})
}
}
}
}
impl<R: Ruleset> Game<R, Complete> {
pub fn winners(&self) -> Vec<&Player> {
let boards = self.scoreboards();
let max = boards.iter().map(|b| b.total).max().unwrap_or(0);
self.competitors
.iter()
.zip(boards.iter())
.filter(|(_, b)| b.total == max)
.map(|(c, _)| &c.player)
.collect()
}
}
fn pick_n_from(standing: PinSet, n: u8) -> PinSet {
standing.into_iter().take(n as usize).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pins::PinSet;
use crate::roll::Roll;
use crate::ruleset::{Candlepin, Duckpin, TenPin};
use crate::scoring::FrameScore;
fn frame(n: u8) -> FrameNumber {
FrameNumber::new(n).unwrap()
}
fn play_perfect_game() -> Game<TenPin, Complete> {
let alice = Player::new("Alice").unwrap();
let mut progress = GameBuilder::<TenPin>::new(alice).build();
for _ in 0..12 {
match progress {
Progress::AwaitingRoll(g) => {
progress = g.roll_count(10).unwrap();
}
Progress::Complete(_) => break,
}
}
match progress {
Progress::Complete(g) => g,
Progress::AwaitingRoll(_) => panic!("game should be complete after 12 strikes"),
}
}
#[test]
fn perfect_game_scores_300() {
let game = play_perfect_game();
let sb = game.scoreboard(0);
assert_eq!(sb.total, 300);
}
#[test]
fn all_gutter_scores_zero() {
let bob = Player::new("Bob").unwrap();
let mut progress = GameBuilder::<TenPin>::new(bob).build();
for _ in 0..20 {
match progress {
Progress::AwaitingRoll(g) => {
progress = g.roll_count(0).unwrap();
}
Progress::Complete(_) => break,
}
}
match progress {
Progress::Complete(g) => {
assert_eq!(g.scoreboard(0).total, 0);
}
Progress::AwaitingRoll(_) => panic!("game should be complete"),
}
}
#[test]
fn two_player_game_alternates() {
let alice = Player::new("Alice").unwrap();
let bob = Player::new("Bob").unwrap();
let Progress::AwaitingRoll(game) =
GameBuilder::<TenPin>::new(alice).add_player(bob).build()
else {
unreachable!()
};
assert_eq!(game.current_player().name(), "Alice");
assert_eq!(game.current_frame_number(), frame(1));
let progress = game.roll_count(3).unwrap();
let Progress::AwaitingRoll(game) = progress else {
panic!("should continue")
};
assert_eq!(game.current_player().name(), "Alice");
let progress = game.roll_count(4).unwrap();
let Progress::AwaitingRoll(game) = progress else {
panic!("should continue")
};
assert_eq!(game.current_player().name(), "Bob");
assert_eq!(game.current_frame_number(), frame(1));
}
#[test]
fn empty_player_name_errors() {
let result = Player::new("");
assert!(result.is_err());
}
#[test]
fn all_spares_with_five() {
let charlie = Player::new("Charlie").unwrap();
let mut progress = GameBuilder::<TenPin>::new(charlie).build();
for i in 0..21 {
match progress {
Progress::AwaitingRoll(g) => {
progress = g.roll_count(5).unwrap();
}
Progress::Complete(_) => {
assert_eq!(i, 21, "should complete after 21 rolls");
break;
}
}
}
match progress {
Progress::Complete(g) => {
assert_eq!(g.scoreboard(0).total, 150);
}
Progress::AwaitingRoll(_) => panic!("game should be complete"),
}
}
#[test]
fn competitor_accessors_work() {
let alice = Player::new("Alice").unwrap();
let bob = Player::new("Bob").unwrap();
let progress = GameBuilder::<TenPin>::new(alice).add_player(bob).build();
assert_eq!(progress.competitor_count(), 2);
assert_eq!(progress.player(0).name(), "Alice");
assert_eq!(progress.player(1).name(), "Bob");
assert_eq!(progress.scorecard(0).frames_completed(), 0);
assert_eq!(progress.scorecard(1).frames_completed(), 0);
assert_eq!(progress.competitors().len(), 2);
}
#[test]
fn foul_strike_completes_frame_with_zero_base() {
let fouler = Player::new("Fouler").unwrap();
let Progress::AwaitingRoll(game) = GameBuilder::<TenPin>::new(fouler).build() else {
unreachable!()
};
let foul_strike = Roll::foul(PinSet::full::<10>());
let progress = game.roll(foul_strike).unwrap();
let Progress::AwaitingRoll(game) = progress else {
panic!("game should continue after one frame")
};
assert_eq!(game.current_frame_number(), frame(2));
let card = game.scorecard(0);
assert_eq!(card.frames_completed(), 1);
assert_eq!(card.frames()[0].base_score(), 0);
}
#[test]
fn candlepin_full_game_with_three_ball_frames() {
let candle = Player::new("Candle").unwrap();
let mut progress = GameBuilder::<Candlepin>::new(candle).build();
for _ in 0..30 {
match progress {
Progress::AwaitingRoll(g) => {
progress = g.roll_count(3).unwrap();
}
Progress::Complete(_) => break,
}
}
let Progress::Complete(game) = progress else {
panic!("candlepin game should complete after 30 rolls of 3")
};
let sb = game.scoreboard(0);
assert_eq!(sb.total, 90); }
#[test]
fn duckpin_alldown_gets_no_bonus() {
let duck = Player::new("Duck").unwrap();
let Progress::AwaitingRoll(game) = GameBuilder::<Duckpin>::new(duck).build() else {
unreachable!()
};
let progress = game.roll_count(3).unwrap();
let Progress::AwaitingRoll(g) = progress else {
panic!("should continue")
};
let progress = g.roll_count(4).unwrap();
let Progress::AwaitingRoll(g) = progress else {
panic!("should continue")
};
let progress = g.roll_count(3).unwrap();
let Progress::AwaitingRoll(g) = progress else {
panic!("should continue; frame 1 done, frame 2 starts")
};
assert_eq!(g.current_frame_number(), frame(2));
let progress = g.roll_count(5).unwrap();
let Progress::AwaitingRoll(g) = progress else {
panic!("should continue")
};
let progress = g.roll_count(2).unwrap();
let Progress::AwaitingRoll(g) = progress else {
panic!("should continue; duckpin has 3 balls per frame")
};
let progress = g.roll_count(1).unwrap();
let g = match progress {
Progress::AwaitingRoll(g) => g,
Progress::Complete(_) => panic!("game should not be complete after 2 frames"),
};
assert_eq!(g.current_frame_number(), frame(3));
let sb = g.scoreboard(0);
let FrameScore::Resolved {
base,
bonus,
cumulative,
..
} = &sb.frames[0]
else {
panic!("frame 1 should be resolved")
};
assert_eq!(*base, 10);
assert_eq!(*bonus, 0); assert_eq!(*cumulative, 10);
let FrameScore::Resolved {
base, cumulative, ..
} = &sb.frames[1]
else {
panic!("frame 2 should be resolved")
};
assert_eq!(*base, 8);
assert_eq!(*cumulative, 18); }
}