use std::fmt;
use crate::frame::FrameNumber;
use crate::frame::scored::ScoredFrame;
use crate::roll::Roll;
use crate::ruleset::{BonusScheme, Ruleset};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrameScore {
Resolved {
frame: FrameNumber,
base: u16,
bonus: u16,
cumulative: u16,
},
Pending {
frame: FrameNumber,
base: u16,
},
}
impl FrameScore {
pub fn frame(&self) -> FrameNumber {
match self {
Self::Resolved { frame, .. } | Self::Pending { frame, .. } => *frame,
}
}
pub fn base(&self) -> u16 {
match self {
Self::Resolved { base, .. } | Self::Pending { base, .. } => *base,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Scoreboard {
pub frames: Vec<FrameScore>,
pub total: u16,
}
impl fmt::Display for Scoreboard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.frames.is_empty() {
return write!(f, "No frames recorded. Total: {}", self.total);
}
let col_width = 4;
write!(f, "Frame |")?;
for fs in &self.frames {
write!(f, "{:>w$} |", fs.frame(), w = col_width)?;
}
writeln!(f)?;
write!(f, "Base |")?;
for fs in &self.frames {
write!(f, "{:>w$} |", fs.base(), w = col_width)?;
}
writeln!(f)?;
write!(f, "Bonus |")?;
for fs in &self.frames {
match fs {
FrameScore::Resolved { bonus, .. } => {
write!(f, "{bonus:>col_width$} |")?;
}
FrameScore::Pending { .. } => {
write!(f, "{:>w$} |", "?", w = col_width)?;
}
}
}
writeln!(f)?;
write!(f, "Cum. |")?;
for fs in &self.frames {
match fs {
FrameScore::Resolved { cumulative, .. } => {
write!(f, "{cumulative:>col_width$} |")?;
}
FrameScore::Pending { .. } => {
write!(f, "{:>w$} |", "?", w = col_width)?;
}
}
}
writeln!(f)?;
write!(f, "Total: {}", self.total)
}
}
pub fn compute_scoreboard<R: Ruleset>(frames: &[ScoredFrame]) -> Scoreboard {
let all_rolls: Vec<&Roll> = frames.iter().flat_map(|f| f.rolls().iter()).collect();
let BonusScheme::Traditional {
strike_bonus_balls,
spare_bonus_balls,
} = R::BONUS;
let mut frame_scores = Vec::with_capacity(frames.len());
let mut cumulative: u16 = 0;
let mut roll_offset: usize = 0;
for frame in frames {
let frame_roll_count = frame.rolls().len();
let bonus_needed = frame.bonus_balls(strike_bonus_balls, spare_bonus_balls);
let bonus_start = roll_offset + frame_roll_count;
let bonus: u16 = all_rolls
.get(bonus_start..)
.unwrap_or_default()
.iter()
.take(bonus_needed as usize)
.map(|r| u16::from(r.score()))
.sum();
let available_bonus = all_rolls.len().saturating_sub(bonus_start);
let resolved = available_bonus >= bonus_needed as usize;
let base = frame.base_score();
if resolved {
cumulative += base + bonus;
frame_scores.push(FrameScore::Resolved {
frame: frame.number(),
base,
bonus,
cumulative,
});
} else {
frame_scores.push(FrameScore::Pending {
frame: frame.number(),
base,
});
}
roll_offset += frame_roll_count;
}
let total = frame_scores
.iter()
.rev()
.find_map(|fs| match fs {
FrameScore::Resolved { cumulative, .. } => Some(*cumulative),
FrameScore::Pending { .. } => None,
})
.unwrap_or(0);
Scoreboard {
frames: frame_scores,
total,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frame::FrameKind;
use crate::frame::FrameNumber;
use crate::frame::FramePosition;
use crate::pins::PinSet;
use crate::roll::Roll;
use crate::ruleset::TenPin;
fn frame(n: u8) -> FrameNumber {
FrameNumber::new(n).unwrap()
}
fn strike_frame(number: FrameNumber) -> ScoredFrame {
ScoredFrame::new(
number,
FramePosition::Regular,
FrameKind::Strike,
vec![Roll::clean(PinSet::full::<10>())],
10,
)
}
fn spare_frame(number: FrameNumber, first: u8) -> ScoredFrame {
let second = 10 - first;
let first_pins = PinSet::range(0, first);
let second_pins = PinSet::range(first, 10);
ScoredFrame::new(
number,
FramePosition::Regular,
FrameKind::Spare,
vec![Roll::clean(first_pins), Roll::clean(second_pins)],
u16::from(first + second),
)
}
fn open_frame(number: FrameNumber, first: u8, second: u8) -> ScoredFrame {
let first_pins = PinSet::range(0, first);
let second_pins = PinSet::range(first, first + second);
ScoredFrame::new(
number,
FramePosition::Regular,
FrameKind::Open,
vec![Roll::clean(first_pins), Roll::clean(second_pins)],
u16::from(first + second),
)
}
#[test]
fn perfect_game_scoring() {
let mut frames: Vec<ScoredFrame> = (1..=9).map(|n| strike_frame(frame(n))).collect();
frames.push(ScoredFrame::new(
frame(10),
FramePosition::Final,
FrameKind::Strike,
vec![
Roll::clean(PinSet::full::<10>()),
Roll::clean(PinSet::full::<10>()),
Roll::clean(PinSet::full::<10>()),
],
30,
));
let sb = compute_scoreboard::<TenPin>(&frames);
assert_eq!(sb.total, 300);
}
#[test]
fn all_gutter_scoring() {
let mut frames: Vec<ScoredFrame> = (1..=9).map(|n| open_frame(frame(n), 0, 0)).collect();
frames.push(ScoredFrame::new(
frame(10),
FramePosition::Final,
FrameKind::Open,
vec![Roll::clean(PinSet::EMPTY), Roll::clean(PinSet::EMPTY)],
0,
));
let sb = compute_scoreboard::<TenPin>(&frames);
assert_eq!(sb.total, 0);
}
#[test]
fn all_fives_spare_scoring() {
let mut frames: Vec<ScoredFrame> = (1..=9).map(|n| spare_frame(frame(n), 5)).collect();
frames.push(ScoredFrame::new(
frame(10),
FramePosition::Final,
FrameKind::Spare,
vec![
Roll::clean(PinSet::range(0, 5)),
Roll::clean(PinSet::range(5, 10)),
Roll::clean(PinSet::range(0, 5)),
],
15,
));
let sb = compute_scoreboard::<TenPin>(&frames);
assert_eq!(sb.total, 150);
}
#[test]
fn mixed_game_strike_spare_open() {
let mut frames = vec![
strike_frame(frame(1)),
spare_frame(frame(2), 3),
open_frame(frame(3), 5, 2),
];
for n in 4..=9 {
frames.push(open_frame(frame(n), 4, 3));
}
frames.push(ScoredFrame::new(
frame(10),
FramePosition::Final,
FrameKind::Open,
vec![
Roll::clean(PinSet::of([0, 1])),
Roll::clean(PinSet::of([2, 3, 4])),
],
7,
));
let sb = compute_scoreboard::<TenPin>(&frames);
let FrameScore::Resolved {
cumulative, bonus, ..
} = &sb.frames[0]
else {
panic!("frame 1 should be resolved")
};
assert_eq!(*bonus, 10); assert_eq!(*cumulative, 20);
let FrameScore::Resolved {
cumulative, bonus, ..
} = &sb.frames[1]
else {
panic!("frame 2 should be resolved")
};
assert_eq!(*bonus, 5); assert_eq!(*cumulative, 35);
let FrameScore::Resolved {
cumulative, bonus, ..
} = &sb.frames[2]
else {
panic!("frame 3 should be resolved")
};
assert_eq!(*bonus, 0);
assert_eq!(*cumulative, 42);
assert_eq!(sb.total, 91);
}
#[test]
fn consecutive_strikes_then_open() {
let mut frames = vec![
strike_frame(frame(1)),
strike_frame(frame(2)),
open_frame(frame(3), 3, 4),
];
for n in 4..=9 {
frames.push(open_frame(frame(n), 0, 0));
}
frames.push(ScoredFrame::new(
frame(10),
FramePosition::Final,
FrameKind::Open,
vec![Roll::clean(PinSet::EMPTY), Roll::clean(PinSet::EMPTY)],
0,
));
let sb = compute_scoreboard::<TenPin>(&frames);
let FrameScore::Resolved {
bonus, cumulative, ..
} = &sb.frames[0]
else {
panic!("frame 1 should be resolved")
};
assert_eq!(*bonus, 13); assert_eq!(*cumulative, 23);
let FrameScore::Resolved {
bonus, cumulative, ..
} = &sb.frames[1]
else {
panic!("frame 2 should be resolved")
};
assert_eq!(*bonus, 7); assert_eq!(*cumulative, 40);
assert_eq!(sb.total, 47);
}
#[test]
fn in_progress_game_has_pending_frames() {
let frames = vec![
open_frame(frame(1), 3, 4), open_frame(frame(2), 2, 5), open_frame(frame(3), 1, 1), open_frame(frame(4), 4, 4), strike_frame(frame(5)), ];
let sb = compute_scoreboard::<TenPin>(&frames);
assert_eq!(sb.frames.len(), 5);
for i in 0..4 {
assert!(
matches!(sb.frames[i], FrameScore::Resolved { .. }),
"frame {} should be Resolved",
i + 1
);
}
assert!(
matches!(sb.frames[4], FrameScore::Pending { .. }),
"frame 5 strike should be Pending (no bonus rolls available)"
);
let FrameScore::Resolved { cumulative, .. } = &sb.frames[3] else {
panic!("frame 4 should be resolved")
};
assert_eq!(sb.total, *cumulative);
assert_eq!(sb.total, 24);
}
#[test]
fn foul_zeroes_bonus_contribution() {
let foul_pins = PinSet::range(0, 5);
let clean_pins = PinSet::of([5, 6, 7]);
let foul_frame = ScoredFrame::new(
frame(2),
FramePosition::Regular,
FrameKind::Open,
vec![
Roll::foul(foul_pins), Roll::clean(clean_pins), ],
3, );
let mut frames = vec![strike_frame(frame(1)), foul_frame];
for n in 3..=9 {
frames.push(open_frame(frame(n), 0, 0));
}
frames.push(ScoredFrame::new(
frame(10),
FramePosition::Final,
FrameKind::Open,
vec![Roll::clean(PinSet::EMPTY), Roll::clean(PinSet::EMPTY)],
0,
));
let sb = compute_scoreboard::<TenPin>(&frames);
let FrameScore::Resolved {
bonus, cumulative, ..
} = &sb.frames[0]
else {
panic!("frame 1 should be resolved")
};
assert_eq!(*bonus, 3); assert_eq!(*cumulative, 13);
assert_eq!(sb.total, 16); }
#[test]
fn empty_frames_returns_zero() {
let sb = compute_scoreboard::<TenPin>(&[]);
assert!(sb.frames.is_empty());
assert_eq!(sb.total, 0);
}
}