diff --git a/config.json b/config.json index 01bb3e9a0..89a8fd46b 100644 --- a/config.json +++ b/config.json @@ -30,6 +30,7 @@ "robot-simulator", "bracket-push", "queen-attack", + "bowling", "sublist", "space-age", "allergies", diff --git a/exercises/bowling/.gitignore b/exercises/bowling/.gitignore new file mode 100644 index 000000000..0e49cdd58 --- /dev/null +++ b/exercises/bowling/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/exercises/bowling/Cargo.toml b/exercises/bowling/Cargo.toml new file mode 100644 index 000000000..0964c18d9 --- /dev/null +++ b/exercises/bowling/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "bowling" +version = "0.0.0" diff --git a/exercises/bowling/example.rs b/exercises/bowling/example.rs new file mode 100644 index 000000000..089ef3dcb --- /dev/null +++ b/exercises/bowling/example.rs @@ -0,0 +1,134 @@ + +pub struct BowlingGame { + frames: Vec, +} + +struct Frame { + rolls: Vec, + bonus: Vec, +} + +impl Frame { + fn score(&self) -> u16 { + self.roll_score() + self.bonus_score() + } + + fn roll_score(&self) -> u16 { + self.rolls.iter().sum() + } + + fn bonus_score(&self) -> u16 { + self.bonus.iter().sum() + } + + fn is_valid(&self) -> bool { + self.rolls_valid() && self.bonus_valid() + } + + fn rolls_valid(&self) -> bool { + self.roll_score() <= 10 + } + + fn bonus_valid(&self) -> bool { + if self.is_open() || !self.bonus_done() { + return true; + } + + if self.is_spare() { + return self.bonus_score() <= 10; + } + + if let Some(first) = self.bonus.iter().next() { + if *first == 10 { + self.bonus_score() <= 20 + } else { + self.bonus_score() <= 10 + } + } else { + unreachable!(); + } + } + + fn is_complete(&self) -> bool { + self.is_open() || self.bonus_done() + } + + fn rolls_done(&self) -> bool { + self.rolls.len() == 2 || self.is_strike() + } + + fn bonus_done(&self) -> bool { + (self.is_spare() && self.bonus.len() == 1) || (self.is_strike() && self.bonus.len() == 2) + } + + fn is_open(&self) -> bool { + self.rolls.len() == 2 && self.roll_score() < 10 + } + + fn is_spare(&self) -> bool { + self.rolls.len() == 2 && self.roll_score() == 10 + } + + fn is_strike(&self) -> bool { + self.rolls.len() == 1 && self.roll_score() == 10 + } + + fn add_roll(&mut self, roll: u16) { + if !self.is_complete() { + if self.is_spare() || self.is_strike() { + self.bonus.push(roll) + } else { + self.rolls.push(roll) + } + } + } + + fn new() -> Self { + Frame { + rolls: vec![], + bonus: vec![], + } + } +} + +impl BowlingGame { + pub fn new() -> Self { + BowlingGame { frames: vec![Frame::new()] } + } + + pub fn roll(&mut self, pins: u16) -> Result<(), &'static str> { + if pins > 10 { + Err("Greater than 10 pins") + } else { + if self.score().is_ok() { + return Err("Game Finished. No more rolls."); + } + + for mut frame in self.frames.iter_mut() { + frame.add_roll(pins) + } + + if self.frames.iter().any(|f| !f.is_valid()) { + return Err("Invalid Roll"); + } + + if self.frames.iter().last().unwrap().rolls_done() && self.frames.len() < 10 { + self.frames.push(Frame::new()); + } + + Ok(()) + } + } + + pub fn score(&self) -> Result { + if !self.is_done() { + Err("Game Incomplete") + } else { + Ok(self.frames.iter().fold(0, |acc, r| acc + r.score())) + } + } + + fn is_done(&self) -> bool { + self.frames.len() == 10 && self.frames.iter().all(|f| f.is_complete()) + } +} diff --git a/exercises/bowling/tests/bowling.rs b/exercises/bowling/tests/bowling.rs new file mode 100644 index 000000000..7a4b2b9b0 --- /dev/null +++ b/exercises/bowling/tests/bowling.rs @@ -0,0 +1,373 @@ +extern crate bowling; + +use bowling::*; + +#[test] +fn roll_returns_a_result() { + let mut game = BowlingGame::new(); + assert!(game.roll(0).is_ok()); +} + +#[test] +#[ignore] +fn you_can_not_roll_more_than_ten_pins_in_a_single_roll() { + let mut game = BowlingGame::new(); + + assert!(game.roll(11).is_err()); +} + +#[test] +#[ignore] +fn a_game_score_is_ok_if_ten_frames_have_been_rolled() { + let mut game = BowlingGame::new(); + + for _ in 0..10 { + let _ = game.roll(0); + let _ = game.roll(0); + } + + assert!(game.score().is_ok()); +} + +#[test] +#[ignore] +fn you_can_not_score_a_game_with_no_rolls() { + let game = BowlingGame::new(); + + assert!(game.score().is_err()); +} + +#[test] +#[ignore] +fn a_game_score_is_err_if_fewer_than_ten_frames_have_been_rolled() { + let mut game = BowlingGame::new(); + + for _ in 0..9 { + let _ = game.roll(0); + let _ = game.roll(0); + } + + assert!(game.score().is_err()); +} + +#[test] +#[ignore] +fn a_roll_is_err_if_the_game_is_done() { + let mut game = BowlingGame::new(); + + for _ in 0..10 { + let _ = game.roll(0); + let _ = game.roll(0); + } + + assert!(game.roll(0).is_err()); +} + +#[test] +#[ignore] +fn twenty_zero_pin_rolls_scores_zero() { + let mut game = BowlingGame::new(); + + for _ in 0..20 { + let _ = game.roll(0); + } + + assert_eq!(game.score().unwrap(), 0); +} + +#[test] +#[ignore] +fn ten_frames_without_a_strike_or_spare() { + let mut game = BowlingGame::new(); + + for _ in 0..10 { + let _ = game.roll(3); + let _ = game.roll(6); + } + + assert_eq!(game.score().unwrap(), 90); +} + +#[test] +#[ignore] +fn spare_in_the_first_frame_followed_by_zeros() { + let mut game = BowlingGame::new(); + + let _ = game.roll(6); + let _ = game.roll(4); + + for _ in 0..18 { + let _ = game.roll(0); + } + + assert_eq!(game.score().unwrap(), 10); +} + +#[test] +#[ignore] +fn points_scored_in_the_roll_after_a_spare_are_counted_twice_as_a_bonus() { + let mut game = BowlingGame::new(); + + let _ = game.roll(6); + let _ = game.roll(4); + let _ = game.roll(3); + + for _ in 0..17 { + let _ = game.roll(0); + } + + assert_eq!(game.score().unwrap(), 16); +} + +#[test] +#[ignore] +fn consecutive_spares_each_get_a_one_roll_bonus() { + let mut game = BowlingGame::new(); + + let _ = game.roll(5); + let _ = game.roll(5); + let _ = game.roll(3); + let _ = game.roll(7); + let _ = game.roll(4); + + for _ in 0..15 { + let _ = game.roll(0); + } + + assert_eq!(game.score().unwrap(), 31); +} + +#[test] +#[ignore] +fn if_the_last_frame_is_a_spare_you_get_one_extra_roll_that_is_scored_once() { + let mut game = BowlingGame::new(); + + for _ in 0..18 { + let _ = game.roll(0); + } + + let _ = game.roll(5); + let _ = game.roll(5); + let _ = game.roll(7); + + assert_eq!(game.score().unwrap(), 17); +} + +#[test] +#[ignore] +fn a_strike_earns_ten_points_in_a_frame_with_a_single_roll() { + let mut game = BowlingGame::new(); + + let _ = game.roll(10); + + for _ in 0..18 { + let _ = game.roll(0); + } + + assert_eq!(game.score().unwrap(), 10); +} + +#[test] +#[ignore] +fn points_scored_in_the_two_rolls_after_a_strike_are_counted_twice_as_a_bonus() { + let mut game = BowlingGame::new(); + + let _ = game.roll(10); + let _ = game.roll(5); + let _ = game.roll(3); + + for _ in 0..16 { + let _ = game.roll(0); + } + + assert_eq!(game.score().unwrap(), 26); +} + +#[test] +#[ignore] +fn consecutive_strikes_each_get_the_two_roll_bonus() { + let mut game = BowlingGame::new(); + + let _ = game.roll(10); + let _ = game.roll(10); + let _ = game.roll(10); + let _ = game.roll(5); + let _ = game.roll(3); + + for _ in 0..12 { + let _ = game.roll(0); + } + + assert_eq!(game.score().unwrap(), 81); +} + +#[test] +#[ignore] +fn a_strike_in_the_last_frame_earns_a_two_roll_bonus_that_is_counted_once() { + let mut game = BowlingGame::new(); + + for _ in 0..18 { + let _ = game.roll(0); + } + + let _ = game.roll(10); + let _ = game.roll(7); + let _ = game.roll(1); + + assert_eq!(game.score().unwrap(), 18); +} + +#[test] +#[ignore] +fn a_spare_with_the_two_roll_bonus_does_not_get_a_bonus_roll() { + let mut game = BowlingGame::new(); + + for _ in 0..18 { + let _ = game.roll(0); + } + + let _ = game.roll(10); + let _ = game.roll(7); + let _ = game.roll(3); + + assert_eq!(game.score().unwrap(), 20); +} + +#[test] +#[ignore] +fn strikes_with_the_two_roll_bonus_do_not_get_a_bonus_roll() { + let mut game = BowlingGame::new(); + + for _ in 0..18 { + let _ = game.roll(0); + } + + let _ = game.roll(10); + let _ = game.roll(10); + let _ = game.roll(10); + + assert_eq!(game.score().unwrap(), 30); +} + +#[test] +#[ignore] +fn a_strike_with_the_one_roll_bonus_after_a_spare_in_the_last_frame_does_not_get_a_bonus() { + let mut game = BowlingGame::new(); + + for _ in 0..18 { + let _ = game.roll(0); + } + + let _ = game.roll(7); + let _ = game.roll(3); + let _ = game.roll(10); + + assert_eq!(game.score().unwrap(), 20); +} + +#[test] +#[ignore] +fn all_strikes_is_a_perfect_score_of_300() { + let mut game = BowlingGame::new(); + + for _ in 0..12 { + let _ = game.roll(10); + } + + assert_eq!(game.score().unwrap(), 300); +} + +#[test] +#[ignore] +fn you_can_not_roll_more_than_ten_pins_in_a_single_frame() { + let mut game = BowlingGame::new(); + + assert!(game.roll(5).is_ok()); + assert!(game.roll(6).is_err()); +} + +#[test] +#[ignore] +fn the_two_balls_after_a_final_strike_can_not_score_an_invalid_number_of_pins() { + let mut game = BowlingGame::new(); + + for _ in 0..18 { + let _ = game.roll(0); + } + + let _ = game.roll(10); + + assert!(game.roll(5).is_ok()); + assert!(game.roll(6).is_err()); +} + +#[test] +#[ignore] +fn the_two_balls_after_a_final_strike_can_be_a_strike_and_non_strike() { + let mut game = BowlingGame::new(); + + for _ in 0..18 { + let _ = game.roll(0); + } + + let _ = game.roll(10); + + assert!(game.roll(10).is_ok()); + assert!(game.roll(6).is_ok()); +} + +#[test] +#[ignore] +fn the_two_balls_after_a_final_strike_can_not_be_a_non_strike_followed_by_a_strike() { + let mut game = BowlingGame::new(); + + for _ in 0..18 { + let _ = game.roll(0); + } + + let _ = game.roll(10); + + assert!(game.roll(6).is_ok()); + assert!(game.roll(10).is_err()); +} + +#[test] +#[ignore] +fn if_the_last_frame_is_a_strike_you_can_not_score_before_the_extra_rolls_are_taken() { + let mut game = BowlingGame::new(); + + for _ in 0..18 { + let _ = game.roll(0); + } + + let _ = game.roll(10); + + assert!(game.score().is_err()); + + let _ = game.roll(10); + + assert!(game.score().is_err()); + + let _ = game.roll(10); + + assert!(game.score().is_ok()); +} + +#[test] +#[ignore] +fn if_the_last_frame_is_a_spare_you_can_not_create_a_score_before_extra_roll_is_taken() { + let mut game = BowlingGame::new(); + + for _ in 0..18 { + let _ = game.roll(0); + } + + let _ = game.roll(5); + let _ = game.roll(5); + + assert!(game.score().is_err()); + + let _ = game.roll(10); + + assert!(game.score().is_ok()); +} diff --git a/problems.md b/problems.md index 642f02940..41df24594 100644 --- a/problems.md +++ b/problems.md @@ -49,6 +49,7 @@ grade-school | struct, entry api, Vec, Option robot-simulator | Immutability, enum bracket-push | From trait, stack or recursion queen-attack | struct, trait (optional), Result +bowling | struct, Result, goofy bowling logic sublist | enum, generic over type space-age | Custom Trait, From Trait, Default Trait implementation allergies | struct, enum, bitwise (probably), vectors, filter