use bowling::prelude::*;
use proptest::prelude::*;
fn pin_count(max: u8) -> impl Strategy<Value = u8> {
0..=max
}
fn play_game_with_counts<R: Ruleset>(counts: &[u8]) -> (Scoreboard, u32) {
let player = Player::new("Prop").unwrap();
let mut progress = GameBuilder::<R>::new(player).build();
let mut rolls_used = 0u32;
for &n in counts {
match progress {
Progress::AwaitingRoll(g) => {
let standing = g.standing_pins().count();
let clamped = n.min(standing);
progress = g.roll_count(clamped).unwrap();
rolls_used += 1;
}
Progress::Complete(_) => break,
}
}
(progress.scoreboard(0), rolls_used)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(500))]
#[test]
fn tenpin_score_in_valid_range(
counts in proptest::collection::vec(pin_count(10), 12..=21)
) {
let (sb, _) = play_game_with_counts::<TenPin>(&counts);
prop_assert!(sb.total <= 300, "ten-pin score must be <= 300, got {}", sb.total);
}
#[test]
fn tenpin_game_always_terminates(
counts in proptest::collection::vec(pin_count(10), 21..=30)
) {
let player = Player::new("Term").unwrap();
let mut progress = GameBuilder::<TenPin>::new(player).build();
let mut total_rolls = 0u32;
for &n in &counts {
match progress {
Progress::AwaitingRoll(g) => {
let standing = g.standing_pins().count();
let clamped = n.min(standing);
progress = g.roll_count(clamped).unwrap();
total_rolls += 1;
}
Progress::Complete(_) => break,
}
}
prop_assert!(total_rolls <= 21, "ten-pin game used {} rolls", total_rolls);
if let Progress::Complete(_) = progress {
} else {
}
}
#[test]
fn tenpin_perfect_game_is_300(
_dummy in Just(())
) {
let counts = vec![10u8; 12];
let (sb, _) = play_game_with_counts::<TenPin>(&counts);
prop_assert_eq!(sb.total, 300);
}
#[test]
fn tenpin_all_gutter_is_zero(
_dummy in Just(())
) {
let counts = vec![0u8; 20];
let (sb, _) = play_game_with_counts::<TenPin>(&counts);
prop_assert_eq!(sb.total, 0);
}
#[test]
fn tenpin_frame_scores_sum_to_total(
counts in proptest::collection::vec(pin_count(10), 21..=21)
) {
let (sb, _) = play_game_with_counts::<TenPin>(&counts);
if let Some(cum) = sb.frames.iter().rev().find_map(|f| match f {
bowling::scoring::FrameScore::Resolved { cumulative, .. } => Some(*cumulative),
bowling::scoring::FrameScore::Pending { .. } => None,
}) {
prop_assert_eq!(cum, sb.total);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn candlepin_score_in_valid_range(
counts in proptest::collection::vec(pin_count(10), 12..=30)
) {
let (sb, _) = play_game_with_counts::<Candlepin>(&counts);
prop_assert!(sb.total <= 300, "candlepin score must be <= 300, got {}", sb.total);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn duckpin_score_in_valid_range(
counts in proptest::collection::vec(pin_count(10), 12..=30)
) {
let (sb, _) = play_game_with_counts::<Duckpin>(&counts);
prop_assert!(sb.total <= 300, "duckpin score must be <= 300, got {}", sb.total);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn multiplayer_game_completes(
player_count in 2u8..=4,
counts in proptest::collection::vec(pin_count(10), 80..=100)
) {
let p0 = Player::new("P0").unwrap();
let mut builder = GameBuilder::<TenPin>::new(p0);
for i in 1..player_count {
let player = Player::new(format!("P{i}")).unwrap();
builder = builder.add_player(player);
}
let mut progress = builder.build();
for &n in &counts {
match progress {
Progress::AwaitingRoll(g) => {
let standing = g.standing_pins().count();
let clamped = n.min(standing);
progress = g.roll_count(clamped).unwrap();
}
Progress::Complete(_) => break,
}
}
if let Progress::Complete(g) = progress {
let boards = g.scoreboards();
prop_assert_eq!(boards.len(), player_count as usize);
for board in &boards {
prop_assert!(board.total <= 300);
}
}
}
}