pub mod number;
pub mod scored;
use std::marker::PhantomData;
use crate::error::BowlingError;
use crate::pins::PinSet;
use crate::roll::Roll;
use crate::ruleset::Ruleset;
pub use number::FrameNumber;
pub use scored::{FrameKind, FramePosition, ScoredFrame};
pub trait FramePhase: std::fmt::Debug {
fn standing(&self) -> PinSet;
fn rolls(&self) -> &[Roll];
}
#[derive(Debug)]
pub struct BallOne {
standing: PinSet,
}
impl FramePhase for BallOne {
fn standing(&self) -> PinSet {
self.standing
}
fn rolls(&self) -> &[Roll] {
&[]
}
}
#[derive(Debug)]
pub struct BallTwo {
standing: PinSet,
rolls: Vec<Roll>,
}
impl FramePhase for BallTwo {
fn standing(&self) -> PinSet {
self.standing
}
fn rolls(&self) -> &[Roll] {
&self.rolls
}
}
#[derive(Debug)]
pub struct BallThree {
standing: PinSet,
rolls: Vec<Roll>,
}
impl FramePhase for BallThree {
fn standing(&self) -> PinSet {
self.standing
}
fn rolls(&self) -> &[Roll] {
&self.rolls
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FirstBallOutcome {
Strike,
NonStrike,
}
#[derive(Debug)]
pub struct FinalFillTwo {
standing: PinSet,
rolls: Vec<Roll>,
}
impl FramePhase for FinalFillTwo {
fn standing(&self) -> PinSet {
self.standing
}
fn rolls(&self) -> &[Roll] {
&self.rolls
}
}
#[derive(Debug)]
pub struct FinalFillThree {
standing: PinSet,
rolls: Vec<Roll>,
first_ball_outcome: FirstBallOutcome,
}
impl FramePhase for FinalFillThree {
fn standing(&self) -> PinSet {
self.standing
}
fn rolls(&self) -> &[Roll] {
&self.rolls
}
}
#[derive(Debug)]
pub struct RegularFrame<R: Ruleset, P: FramePhase> {
number: FrameNumber,
phase: P,
_ruleset: PhantomData<R>,
}
#[derive(Debug)]
pub enum RegAfterOne<R: Ruleset> {
Continue(RegularFrame<R, BallTwo>),
Done(ScoredFrame),
}
#[derive(Debug)]
pub enum RegAfterTwo<R: Ruleset> {
Continue(RegularFrame<R, BallThree>),
Done(ScoredFrame),
}
impl<R: Ruleset, P: FramePhase> RegularFrame<R, P> {
pub fn standing(&self) -> PinSet {
self.phase.standing()
}
pub fn phase(&self) -> &P {
&self.phase
}
}
impl<R: Ruleset> RegularFrame<R, BallOne> {
pub fn new(number: FrameNumber) -> Self {
Self {
number,
phase: BallOne {
standing: R::full_rack(),
},
_ruleset: PhantomData,
}
}
pub fn roll(self, delivery: Roll) -> Result<RegAfterOne<R>, BowlingError> {
validate_delivery(self.phase.standing, delivery)?;
let new_standing = self.phase.standing - delivery.knocked();
let mut rolls = Vec::with_capacity(R::BALLS_PER_FRAME as usize);
rolls.push(delivery);
if new_standing.is_empty() {
return Ok(RegAfterOne::Done(finish_regular(
self.number,
rolls,
FrameKind::Strike,
)));
}
Ok(RegAfterOne::Continue(RegularFrame {
number: self.number,
phase: BallTwo {
standing: new_standing,
rolls,
},
_ruleset: PhantomData,
}))
}
}
impl<R: Ruleset> RegularFrame<R, BallTwo> {
pub fn roll(mut self, delivery: Roll) -> Result<RegAfterTwo<R>, BowlingError> {
validate_delivery(self.phase.standing, delivery)?;
let new_standing = self.phase.standing - delivery.knocked();
self.phase.rolls.push(delivery);
if new_standing.is_empty() {
return Ok(RegAfterTwo::Done(finish_regular(
self.number,
self.phase.rolls,
FrameKind::Spare,
)));
}
if R::BALLS_PER_FRAME <= 2 {
return Ok(RegAfterTwo::Done(finish_regular(
self.number,
self.phase.rolls,
FrameKind::Open,
)));
}
Ok(RegAfterTwo::Continue(RegularFrame {
number: self.number,
phase: BallThree {
standing: new_standing,
rolls: self.phase.rolls,
},
_ruleset: PhantomData,
}))
}
}
impl<R: Ruleset> RegularFrame<R, BallThree> {
pub fn roll(mut self, delivery: Roll) -> Result<ScoredFrame, BowlingError> {
validate_delivery(self.phase.standing, delivery)?;
let new_standing = self.phase.standing - delivery.knocked();
self.phase.rolls.push(delivery);
if new_standing.is_empty() {
let kind = if R::ALL_DOWN_IS_SPARE {
FrameKind::Spare
} else {
FrameKind::AllDown
};
return Ok(finish_regular(self.number, self.phase.rolls, kind));
}
Ok(finish_regular(
self.number,
self.phase.rolls,
FrameKind::Open,
))
}
}
#[derive(Debug)]
pub struct FinalFrame<R: Ruleset, P: FramePhase> {
number: FrameNumber,
phase: P,
_ruleset: PhantomData<R>,
}
#[derive(Debug)]
pub enum FinalAfterOne<R: Ruleset> {
ToBallTwo(FinalFrame<R, BallTwo>),
ToFillTwo(FinalFrame<R, FinalFillTwo>),
}
#[derive(Debug)]
pub enum FinalAfterTwo<R: Ruleset> {
ToBallThree(FinalFrame<R, BallThree>),
ToFillThree(FinalFrame<R, FinalFillThree>),
Done(ScoredFrame),
}
#[derive(Debug)]
pub enum FinalAfterThree<R: Ruleset> {
ToFillThree(FinalFrame<R, FinalFillThree>),
Done(ScoredFrame),
}
impl<R: Ruleset, P: FramePhase> FinalFrame<R, P> {
pub fn standing(&self) -> PinSet {
self.phase.standing()
}
pub fn phase(&self) -> &P {
&self.phase
}
}
impl<R: Ruleset> FinalFrame<R, BallOne> {
pub fn new(number: FrameNumber) -> Self {
Self {
number,
phase: BallOne {
standing: R::full_rack(),
},
_ruleset: PhantomData,
}
}
pub fn roll(self, delivery: Roll) -> Result<FinalAfterOne<R>, BowlingError> {
validate_delivery(self.phase.standing, delivery)?;
let new_standing = self.phase.standing - delivery.knocked();
let mut rolls = Vec::with_capacity(3);
rolls.push(delivery);
if new_standing.is_empty() {
return Ok(FinalAfterOne::ToFillTwo(FinalFrame {
number: self.number,
phase: FinalFillTwo {
standing: R::full_rack(),
rolls,
},
_ruleset: PhantomData,
}));
}
Ok(FinalAfterOne::ToBallTwo(FinalFrame {
number: self.number,
phase: BallTwo {
standing: new_standing,
rolls,
},
_ruleset: PhantomData,
}))
}
}
impl<R: Ruleset> FinalFrame<R, BallTwo> {
pub fn roll(mut self, delivery: Roll) -> Result<FinalAfterTwo<R>, BowlingError> {
validate_delivery(self.phase.standing, delivery)?;
let new_standing = self.phase.standing - delivery.knocked();
self.phase.rolls.push(delivery);
if new_standing.is_empty() {
return Ok(FinalAfterTwo::ToFillThree(FinalFrame {
number: self.number,
phase: FinalFillThree {
standing: R::full_rack(),
rolls: self.phase.rolls,
first_ball_outcome: FirstBallOutcome::NonStrike,
},
_ruleset: PhantomData,
}));
}
if R::BALLS_PER_FRAME <= 2 {
return Ok(FinalAfterTwo::Done(finish_final(
self.number,
self.phase.rolls,
FrameKind::Open,
)));
}
Ok(FinalAfterTwo::ToBallThree(FinalFrame {
number: self.number,
phase: BallThree {
standing: new_standing,
rolls: self.phase.rolls,
},
_ruleset: PhantomData,
}))
}
}
impl<R: Ruleset> FinalFrame<R, BallThree> {
pub fn roll(mut self, delivery: Roll) -> Result<FinalAfterThree<R>, BowlingError> {
validate_delivery(self.phase.standing, delivery)?;
let new_standing = self.phase.standing - delivery.knocked();
self.phase.rolls.push(delivery);
if new_standing.is_empty() {
if R::ALL_DOWN_IS_SPARE {
return Ok(FinalAfterThree::ToFillThree(FinalFrame {
number: self.number,
phase: FinalFillThree {
standing: R::full_rack(),
rolls: self.phase.rolls,
first_ball_outcome: FirstBallOutcome::NonStrike,
},
_ruleset: PhantomData,
}));
}
return Ok(FinalAfterThree::Done(finish_final(
self.number,
self.phase.rolls,
FrameKind::AllDown,
)));
}
Ok(FinalAfterThree::Done(finish_final(
self.number,
self.phase.rolls,
FrameKind::Open,
)))
}
}
impl<R: Ruleset> FinalFrame<R, FinalFillTwo> {
pub fn roll(mut self, delivery: Roll) -> Result<FinalFrame<R, FinalFillThree>, BowlingError> {
validate_delivery(self.phase.standing, delivery)?;
let new_standing = self.phase.standing - delivery.knocked();
self.phase.rolls.push(delivery);
if new_standing.is_empty() {
return Ok(FinalFrame {
number: self.number,
phase: FinalFillThree {
standing: R::full_rack(),
rolls: self.phase.rolls,
first_ball_outcome: FirstBallOutcome::Strike,
},
_ruleset: PhantomData,
});
}
Ok(FinalFrame {
number: self.number,
phase: FinalFillThree {
standing: new_standing,
rolls: self.phase.rolls,
first_ball_outcome: FirstBallOutcome::Strike,
},
_ruleset: PhantomData,
})
}
}
impl<R: Ruleset> FinalFrame<R, FinalFillThree> {
pub fn roll(mut self, delivery: Roll) -> Result<ScoredFrame, BowlingError> {
validate_delivery(self.phase.standing, delivery)?;
self.phase.rolls.push(delivery);
let kind = match self.phase.first_ball_outcome {
FirstBallOutcome::Strike => FrameKind::Strike,
FirstBallOutcome::NonStrike => FrameKind::Spare,
};
Ok(finish_final(self.number, self.phase.rolls, kind))
}
}
fn validate_delivery(standing: PinSet, delivery: Roll) -> Result<(), BowlingError> {
if !standing.contains_all(delivery.knocked()) {
return Err(BowlingError::InvalidDelivery {
knocked: delivery.knocked(),
standing,
});
}
Ok(())
}
fn finish_regular(number: FrameNumber, rolls: Vec<Roll>, kind: FrameKind) -> ScoredFrame {
let base_score = rolls.iter().map(|r| u16::from(r.score())).sum();
ScoredFrame::new(number, FramePosition::Regular, kind, rolls, base_score)
}
fn finish_final(number: FrameNumber, rolls: Vec<Roll>, kind: FrameKind) -> ScoredFrame {
let base_score = rolls.iter().map(|r| u16::from(r.score())).sum();
ScoredFrame::new(number, FramePosition::Final, kind, rolls, base_score)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ruleset::{Candlepin, Duckpin, TenPin};
fn frame(n: u8) -> FrameNumber {
FrameNumber::new(n).unwrap()
}
fn strike_roll() -> Roll {
Roll::clean(PinSet::full::<10>())
}
fn knock(n: u8) -> Roll {
if n == 0 {
Roll::clean(PinSet::EMPTY)
} else {
Roll::clean(PinSet::range(0, n))
}
}
#[test]
fn regular_frame_strike() {
let frame = RegularFrame::<TenPin, BallOne>::new(frame(1));
let outcome = frame.roll(strike_roll()).unwrap();
let RegAfterOne::Done(sf) = outcome else {
panic!("expected Done")
};
assert_eq!(sf.kind(), FrameKind::Strike);
assert_eq!(sf.base_score(), 10);
assert_eq!(sf.rolls().len(), 1);
}
#[test]
fn regular_frame_spare() {
let frame = RegularFrame::<TenPin, BallOne>::new(frame(1));
let outcome = frame.roll(knock(7)).unwrap();
let RegAfterOne::Continue(frame2) = outcome else {
panic!("expected Continue")
};
let remaining = Roll::clean(PinSet::range(7, 10));
let outcome2 = frame2.roll(remaining).unwrap();
let RegAfterTwo::Done(sf) = outcome2 else {
panic!("expected Done")
};
assert_eq!(sf.kind(), FrameKind::Spare);
assert_eq!(sf.base_score(), 10);
}
#[test]
fn regular_frame_open() {
let frame = RegularFrame::<TenPin, BallOne>::new(frame(1));
let outcome = frame.roll(knock(3)).unwrap();
let RegAfterOne::Continue(frame2) = outcome else {
panic!("expected Continue")
};
let second = Roll::clean(PinSet::of([3, 4]));
let outcome2 = frame2.roll(second).unwrap();
let RegAfterTwo::Done(sf) = outcome2 else {
panic!("expected Done")
};
assert_eq!(sf.kind(), FrameKind::Open);
assert_eq!(sf.base_score(), 5);
}
#[test]
fn invalid_delivery_rejected() {
let frame = RegularFrame::<TenPin, BallOne>::new(frame(1));
let bad = Roll::clean(PinSet::of([15]));
assert!(frame.roll(bad).is_err());
}
#[test]
fn final_frame_three_strikes() {
let f = FinalFrame::<TenPin, BallOne>::new(frame(10));
let FinalAfterOne::ToFillTwo(f2) = f.roll(strike_roll()).unwrap() else {
panic!("expected ToFillTwo")
};
let f3 = f2.roll(strike_roll()).unwrap();
let sf = f3.roll(strike_roll()).unwrap();
assert_eq!(sf.kind(), FrameKind::Strike);
assert_eq!(sf.base_score(), 30);
assert_eq!(sf.rolls().len(), 3);
assert!(sf.is_final());
}
#[test]
fn final_frame_spare_plus_fill() {
let f = FinalFrame::<TenPin, BallOne>::new(frame(10));
let FinalAfterOne::ToBallTwo(f2) = f.roll(knock(7)).unwrap() else {
panic!("expected ToBallTwo")
};
let spare_roll = Roll::clean(PinSet::range(7, 10));
let FinalAfterTwo::ToFillThree(f3) = f2.roll(spare_roll).unwrap() else {
panic!("expected ToFillThree")
};
let sf = f3.roll(knock(5)).unwrap();
assert_eq!(sf.kind(), FrameKind::Spare);
assert_eq!(sf.base_score(), 15);
assert!(sf.is_final());
}
#[test]
fn regular_frame_gutter_then_spare() {
let frame = RegularFrame::<TenPin, BallOne>::new(frame(1));
let outcome = frame.roll(knock(0)).unwrap();
let RegAfterOne::Continue(frame2) = outcome else {
panic!("expected Continue after gutter")
};
assert_eq!(frame2.standing().count(), 10);
let outcome2 = frame2.roll(strike_roll()).unwrap();
let RegAfterTwo::Done(sf) = outcome2 else {
panic!("expected Done")
};
assert_eq!(sf.kind(), FrameKind::Spare);
assert_eq!(sf.base_score(), 10);
}
#[test]
fn regular_frame_foul_removes_pins_but_scores_zero() {
let foul_delivery = Roll::foul(PinSet::of([0, 1, 2]));
let frame = RegularFrame::<TenPin, BallOne>::new(frame(1));
let outcome = frame.roll(foul_delivery).unwrap();
let RegAfterOne::Continue(frame2) = outcome else {
panic!("expected Continue; foul didn't clear all pins")
};
assert_eq!(frame2.standing().count(), 7);
let ball2 = Roll::clean(PinSet::of([3, 4, 5, 6]));
let outcome2 = frame2.roll(ball2).unwrap();
let RegAfterTwo::Done(sf) = outcome2 else {
panic!("expected Done")
};
assert_eq!(sf.kind(), FrameKind::Open);
assert_eq!(sf.base_score(), 4);
}
#[test]
fn final_frame_strike_then_open_fill() {
let f = FinalFrame::<TenPin, BallOne>::new(frame(10));
let FinalAfterOne::ToFillTwo(f2) = f.roll(strike_roll()).unwrap() else {
panic!("expected ToFillTwo")
};
assert_eq!(f2.standing().count(), 10);
let fill2 = knock(6);
let f3 = f2.roll(fill2).unwrap();
assert_eq!(f3.standing().count(), 4);
let fill3 = Roll::clean(PinSet::of([6, 7]));
let sf = f3.roll(fill3).unwrap();
assert_eq!(sf.kind(), FrameKind::Strike);
assert_eq!(sf.base_score(), 18); assert!(sf.is_final());
assert_eq!(sf.rolls().len(), 3);
}
#[test]
fn final_frame_open() {
let f = FinalFrame::<TenPin, BallOne>::new(frame(10));
let FinalAfterOne::ToBallTwo(f2) = f.roll(knock(3)).unwrap() else {
panic!("expected ToBallTwo")
};
let ball2 = Roll::clean(PinSet::of([3, 4, 5, 6]));
let FinalAfterTwo::Done(sf) = f2.roll(ball2).unwrap() else {
panic!("expected Done (open)")
};
assert_eq!(sf.kind(), FrameKind::Open);
assert_eq!(sf.base_score(), 7);
assert!(sf.is_final());
}
#[test]
fn final_frame_gutter_gutter() {
let f = FinalFrame::<TenPin, BallOne>::new(frame(10));
let FinalAfterOne::ToBallTwo(f2) = f.roll(knock(0)).unwrap() else {
panic!("expected ToBallTwo")
};
let FinalAfterTwo::Done(sf) = f2.roll(knock(0)).unwrap() else {
panic!("expected Done")
};
assert_eq!(sf.kind(), FrameKind::Open);
assert_eq!(sf.base_score(), 0);
assert!(sf.is_final());
}
fn knock_n_from(standing: PinSet, n: u8) -> Roll {
Roll::clean(standing.into_iter().take(n as usize).collect())
}
#[test]
fn candlepin_regular_three_ball_spare() {
let frame = RegularFrame::<Candlepin, BallOne>::new(frame(1));
let outcome = frame.roll(knock(3)).unwrap();
let RegAfterOne::Continue(f2) = outcome else {
panic!("expected Continue")
};
assert_eq!(f2.standing().count(), 7);
let ball2 = knock_n_from(f2.standing(), 4);
let outcome2 = f2.roll(ball2).unwrap();
let RegAfterTwo::Continue(f3) = outcome2 else {
panic!("expected Continue to BallThree")
};
assert_eq!(f3.standing().count(), 3);
let ball3 = knock_n_from(f3.standing(), 3);
let sf = f3.roll(ball3).unwrap();
assert_eq!(sf.kind(), FrameKind::Spare);
assert_eq!(sf.base_score(), 10);
assert!(!sf.is_final());
}
#[test]
fn duckpin_regular_three_ball_alldown() {
let frame = RegularFrame::<Duckpin, BallOne>::new(frame(1));
let outcome = frame.roll(knock(3)).unwrap();
let RegAfterOne::Continue(f2) = outcome else {
panic!("expected Continue")
};
let ball2 = knock_n_from(f2.standing(), 4);
let outcome2 = f2.roll(ball2).unwrap();
let RegAfterTwo::Continue(f3) = outcome2 else {
panic!("expected Continue to BallThree")
};
let ball3 = knock_n_from(f3.standing(), 3);
let sf = f3.roll(ball3).unwrap();
assert_eq!(sf.kind(), FrameKind::AllDown);
assert_eq!(sf.base_score(), 10);
assert!(!sf.is_final());
}
#[test]
fn candlepin_final_three_ball_spare_earns_fill() {
let f = FinalFrame::<Candlepin, BallOne>::new(frame(10));
let FinalAfterOne::ToBallTwo(f2) = f.roll(knock(3)).unwrap() else {
panic!("expected ToBallTwo")
};
let ball2 = knock_n_from(f2.standing(), 4);
let FinalAfterTwo::ToBallThree(f3) = f2.roll(ball2).unwrap() else {
panic!("expected ToBallThree (3-ball variant)")
};
let ball3 = knock_n_from(f3.standing(), 3);
let FinalAfterThree::ToFillThree(fill) = f3.roll(ball3).unwrap() else {
panic!("expected ToFillThree; candlepin 3-ball spare earns fill")
};
assert_eq!(fill.standing().count(), 10);
let sf = fill.roll(knock(6)).unwrap();
assert_eq!(sf.kind(), FrameKind::Spare);
assert_eq!(sf.base_score(), 16); assert!(sf.is_final());
}
#[test]
fn duckpin_final_three_ball_alldown_no_fill() {
let f = FinalFrame::<Duckpin, BallOne>::new(frame(10));
let FinalAfterOne::ToBallTwo(f2) = f.roll(knock(3)).unwrap() else {
panic!("expected ToBallTwo")
};
let ball2 = knock_n_from(f2.standing(), 4);
let FinalAfterTwo::ToBallThree(f3) = f2.roll(ball2).unwrap() else {
panic!("expected ToBallThree (3-ball variant)")
};
let ball3 = knock_n_from(f3.standing(), 3);
let FinalAfterThree::Done(sf) = f3.roll(ball3).unwrap() else {
panic!("expected Done; duckpin AllDown does NOT earn a fill ball")
};
assert_eq!(sf.kind(), FrameKind::AllDown);
assert_eq!(sf.base_score(), 10);
assert!(sf.is_final());
assert_eq!(sf.rolls().len(), 3);
}
}