Browse Source

The basic idea is here, we still have problems with the mutability

master
Julio Biason 5 years ago
parent
commit
de87cc3564
  1. 367
      rust/bowling/src/lib.rs
  2. 13
      rust/bowling/tests/bowling.rs

367
rust/bowling/src/lib.rs

@ -1,186 +1,315 @@
#[deny(warnings, missing_docs)]
/// Errors created when in the game
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Error { pub enum Error {
/// The throw is higher the number of pins available.
NotEnoughPinsLeft, NotEnoughPinsLeft,
/// The game is over, no more throws are possible.
GameComplete, GameComplete,
} }
/// A throw; holds the information on how many pins where knocked or None if the throw wasn't done /// A throw.
/// yet. // I'd usually move those to their own file, but since Exercism wants a single file... here we go.
#[derive(Debug)] mod throw {
struct Throw(Option<u16>); #[deny(warnings, missing_docs)]
impl Throw { use std::fmt;
fn new() -> Self {
Self(None) #[derive(PartialEq)]
enum ThrowType {
/// This throw doesn't exist yet; the player did something (a strike, a spare) that gave
/// them another throw.
Free,
/// Pins knocked (or not, if the value is 0).
Knock(u16),
} }
/// True if the throw is a strike pub struct Throw {
fn is_strike(&self) -> bool { /// The throw id; it is used only for our debugging reference.
self.0.unwrap_or(0) == 10 id: usize,
/// The result of the throw.
result: ThrowType,
} }
/// This throw happened. impl Throw {
fn is_some(&self) -> bool { /// Create a free Throw
self.0.is_some() pub fn new_free(id: usize) -> Self {
Self {
id: id,
result: ThrowType::Free,
}
} }
fn score(&self) -> u16 { /// Create a throw that knocked some pins
self.0.unwrap_or(0) pub fn new(id: usize, pins: u16) -> Self {
Self {
id: id,
result: ThrowType::Knock(pins),
}
} }
}
enum RollType { /// In-place update of a free throw
Normal, pub fn update(&mut self, pins: u16) {
Streak, // Silently refusing to update a non-free throw result.
Strike, if self.result == ThrowType::Free {
} self.result = ThrowType::Knock(pins);
}
}
/// A normal Roll; the player has the chance of throwing two balls in this. /// Indicate that the Throw is a free and has no value.
#[derive(Debug)] pub fn is_free_throw(&self) -> bool {
struct Normal(Throw, Throw); self.result == ThrowType::Free
impl Normal {
fn new() -> Self {
Self(Throw::new(), Throw::new())
} }
fn is_complete(&self) -> bool { /// Indicate that the player made a strike with this throw.
self.0.is_strike() || (self.0.is_some() && self.1.is_some()) pub fn is_strike(&self) -> bool {
self.result == ThrowType::Knock(10)
} }
fn roll(&mut self, pins: u16) { /// Score of this throw.
if self.0.is_some() { pub fn score(&self) -> Option<u16> {
self.1 = Throw(Some(pins)) match self.result {
} else { ThrowType::Free => None,
self.0 = Throw(Some(pins)) ThrowType::Knock(x) => Some(x),
}
} }
} }
fn score(&self) -> u16 { /// A different debug information, so it will be easier to read.
self.0.score() + self.1.score() impl fmt::Debug for Throw {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}-{}",
self.id,
match self.result {
ThrowType::Free => "Free".into(),
ThrowType::Knock(x) => format!("{}", x),
}
)
}
} }
fn post_streak_score(&self) -> u16 { #[cfg(test)]
self.0.score() * 2 + self.1.score() mod throw_tests {
#[test]
fn make_free() {
let throw = super::Throw::new_free(1);
assert!(throw.is_free_throw());
assert_eq!("1-Free", format!("{:?}", throw));
} }
fn roll_type(&self) -> RollType { #[test]
if self.0.score() == 10 { fn make_strike() {
RollType::Strike let throw = super::Throw::new(1, 10);
} else { assert!(!throw.is_free_throw());
if self.0.score() + self.1.score() == 10 { assert_eq!("1-10", format!("{:?}", throw));
RollType::Streak assert!(throw.is_strike());
} else {
RollType::Normal
} }
#[test]
fn update() {
let mut throw = super::Throw::new_free(1);
assert_eq!(throw.score(), None);
throw.update(5);
assert_eq!(throw.score(), Some(5));
} }
} }
} }
/// The last Roll is special: It can be three if the player hits at least one strike or make a /// A game frame
/// streak. mod frame {
#[derive(Debug)] #[deny(warnings, missing_docs)]
struct Last(Throw, Throw, Throw); use std::fmt;
impl Last { use std::rc::Rc;
fn new() -> Self {
Self(Throw::new(), Throw::new(), Throw::new()) use super::throw::Throw;
pub struct Frame {
id: usize,
throws: Vec<Rc<Throw>>,
} }
fn is_complete(&self) -> bool { impl Frame {
self.0.is_some() && self.1.is_some() pub fn new(id: usize) -> Self {
Self {
id: id,
throws: vec![],
}
} }
fn roll(&mut self, pins: u16) { pub fn add_throw(&mut self, throw: &Rc<Throw>) {
if self.0.is_some() { self.throws.push(throw.clone());
if self.1.is_some() {
self.2 = Throw(Some(pins))
} else {
self.1 = Throw(Some(pins))
} }
pub fn score(&self) -> Option<u16> {
if self.throws.len() == 0 || self.throws.iter().any(|throw| throw.is_free_throw()) {
None
} else { } else {
self.0 = Throw(Some(pins)) Some(self.throws.iter().map(|throw| throw.score().unwrap()).sum())
}
}
pub fn is_spare(&self) -> bool {
self.throws.len() == 2 && self.score() == Some(10)
}
}
/// A different debug information, so it will be easier to read.
impl fmt::Debug for Frame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}-{} ({})",
self.id,
match self.score() {
None => "??".into(),
Some(x) => format!("{}", x),
},
self.throws
.iter()
.map(|x| format!("{:?}", x))
.collect::<Vec<String>>()
.join(",")
)
} }
} }
fn score(&self) -> u16 { #[cfg(test)]
self.0.score() + self.1.score() + self.2.score() mod frame_tests {
#[test]
fn no_score() {
let frame = super::Frame::new(1);
assert_eq!(frame.score(), None);
}
#[test]
fn score() {
let mut frame = super::Frame::new(1);
let throw = super::Throw::new(1, 10);
frame.add_throw(&super::Rc::new(throw));
assert_eq!(frame.score(), Some(10));
}
#[test]
fn no_score_with_free_throws() {
let mut frame = super::Frame::new(1);
let throw1 = super::Throw::new(1, 5);
let throw2 = super::Throw::new_free(2);
frame.add_throw(&super::Rc::new(throw1));
frame.add_throw(&super::Rc::new(throw2));
assert_eq!(frame.score(), None);
}
#[test]
fn debug() {
let mut frame = super::Frame::new(1);
let throw1 = super::Throw::new(1, 5);
let throw2 = super::Throw::new_free(2);
frame.add_throw(&super::Rc::new(throw1));
frame.add_throw(&super::Rc::new(throw2));
assert_eq!("1-?? (1-5,2-Free)", format!("{:?}", frame));
}
} }
} }
use std::rc::Rc;
pub struct BowlingGame { pub struct BowlingGame {
frames: [Normal; 9], frames: Vec<frame::Frame>,
last: Last, throws: Vec<Rc<throw::Throw>>,
current_frame: usize,
} }
impl BowlingGame { impl BowlingGame {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
frames: [ frames: vec![],
Normal::new(), throws: vec![],
Normal::new(),
Normal::new(),
Normal::new(),
Normal::new(),
Normal::new(),
Normal::new(),
Normal::new(),
Normal::new(),
],
last: Last::new(),
current_frame: 0,
} }
} }
pub fn roll(&mut self, pins: u16) -> Result<(), Error> { pub fn roll(&mut self, pins: u16) -> Result<(), Error> {
if pins > 10 { if !self.has_free_throws() && self.is_finished() {
Err(Error::NotEnoughPinsLeft) Err(Error::GameComplete)
} else {
// is this the last throw or a normal throw?
if dbg!(self.current_frame == 9) {
self.roll_last(pins)
} else { } else {
self.roll_normal(pins) let throw = self.make_throw(pins);
let last_id = self.throws.len();
let frame = self.last_frame();
frame.add_throw(&throw);
// check if the player gained any free throws.
if pins == 10 {
// a strike, you get 2 free throws
let new_throw = Rc::new(throw::Throw::new_free(last_id + 1));
self.throws.push(new_throw.clone());
frame.add_throw(&new_throw);
let new_throw = Rc::new(throw::Throw::new_free(last_id + 2));
self.throws.push(new_throw.clone());
frame.add_throw(&new_throw);
} else if frame.is_spare() {
// in a spare, just one throw
let new_throw = Rc::new(throw::Throw::new_free(last_id + 1));
self.throws.push(new_throw.clone());
frame.add_throw(&new_throw);
} }
dbg!(&self.throws);
dbg!(&self.frames);
Ok(())
} }
} }
fn roll_last(&mut self, pins: u16) -> Result<(), Error> { pub fn score(&self) -> Option<u16> {
if self.last.is_complete() { None
Err(Error::GameComplete) // if !self.is_finished() {
} else { // None
self.last.roll(pins); // } else {
dbg!(&self.last); // Some(self.frames.iter().map(|x| x.score()).sum())
Ok(()) // }
} }
/// If there are free throws, update the most recent one; if there are none, create a new
/// throw.
fn make_throw(&mut self, pins: u16) -> Rc<throw::Throw> {
let new_id = self.throws.len() + 1;
self.throws
.iter_mut()
.filter(|x| x.is_free_throw())
.take(1)
.next()
.map_or(Rc::new(throw::Throw::new_free(new_id)), |x| {
let throw = Rc::get_mut(x).unwrap();
throw.update(pins);
x.clone()
})
} }
fn roll_normal(&mut self, pins: u16) -> Result<(), Error> { /// Check if the list of throws in the game there are at least one free.
if dbg!(self.frames[self.current_frame].is_complete()) { fn has_free_throws(&self) -> bool {
self.current_frame += 1; self.throws.iter().any(|x| x.is_free_throw())
self.roll(pins)
} else {
Ok(self.frames[self.current_frame].roll(pins))
} }
/// The game is over when there are 10 frames of scores.
fn is_finished(&self) -> bool {
self.frames.len() == 10
} }
pub fn score(&self) -> Option<u16> { /// Get the last/current frame.
// the only way to have a score is when all throws when done. And for that, we know that fn last_frame(&mut self) -> &mut frame::Frame {
// the last throw must have something. if self.frames.len() == 0 {
if self.last.is_complete() { let new_id = self.frames.len() + 1;
// Accumulator: (total_score_so_far, roll_type_in_the_previous_frame) self.frames.push(frame::Frame::new(new_id));
let (total, _) =
self.frames
.iter()
.fold((0, RollType::Normal), |(total, previous_roll), frame| {
let frame_score = match previous_roll {
RollType::Strike => frame.score() * 2,
RollType::Streak => frame.post_streak_score(),
RollType::Normal => frame.score(),
};
(total + frame_score, frame.roll_type())
});
Some(total + self.last.score())
} else {
None
} }
self.frames.iter_mut().last().unwrap()
} }
} }

13
rust/bowling/tests/bowling.rs

@ -1,18 +1,21 @@
use bowling::*; use bowling::*;
#[test] #[test]
#[ignore]
fn roll_returns_a_result() { fn roll_returns_a_result() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
assert!(game.roll(0).is_ok()); assert!(game.roll(0).is_ok());
} }
#[test] #[test]
#[ignore]
fn you_cannot_roll_more_than_ten_pins_in_a_single_roll() { fn you_cannot_roll_more_than_ten_pins_in_a_single_roll() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
assert_eq!(game.roll(11), Err(Error::NotEnoughPinsLeft)); assert_eq!(game.roll(11), Err(Error::NotEnoughPinsLeft));
} }
#[test] #[test]
#[ignore]
fn a_game_score_is_some_if_ten_frames_have_been_rolled() { fn a_game_score_is_some_if_ten_frames_have_been_rolled() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
@ -25,12 +28,14 @@ fn a_game_score_is_some_if_ten_frames_have_been_rolled() {
} }
#[test] #[test]
#[ignore]
fn you_cannot_score_a_game_with_no_rolls() { fn you_cannot_score_a_game_with_no_rolls() {
let game = BowlingGame::new(); let game = BowlingGame::new();
assert_eq!(game.score(), None); assert_eq!(game.score(), None);
} }
#[test] #[test]
#[ignore]
fn a_game_score_is_none_if_fewer_than_ten_frames_have_been_rolled() { fn a_game_score_is_none_if_fewer_than_ten_frames_have_been_rolled() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
@ -43,6 +48,7 @@ fn a_game_score_is_none_if_fewer_than_ten_frames_have_been_rolled() {
} }
#[test] #[test]
#[ignore]
fn a_roll_is_err_if_the_game_is_done() { fn a_roll_is_err_if_the_game_is_done() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
@ -55,6 +61,7 @@ fn a_roll_is_err_if_the_game_is_done() {
} }
#[test] #[test]
#[ignore]
fn twenty_zero_pin_rolls_scores_zero() { fn twenty_zero_pin_rolls_scores_zero() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
@ -66,6 +73,7 @@ fn twenty_zero_pin_rolls_scores_zero() {
} }
#[test] #[test]
#[ignore]
fn ten_frames_without_a_strike_or_spare() { fn ten_frames_without_a_strike_or_spare() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
@ -78,6 +86,7 @@ fn ten_frames_without_a_strike_or_spare() {
} }
#[test] #[test]
#[ignore]
fn spare_in_the_first_frame_followed_by_zeros() { fn spare_in_the_first_frame_followed_by_zeros() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
@ -92,6 +101,7 @@ fn spare_in_the_first_frame_followed_by_zeros() {
} }
#[test] #[test]
#[ignore]
fn points_scored_in_the_roll_after_a_spare_are_counted_twice_as_a_bonus() { fn points_scored_in_the_roll_after_a_spare_are_counted_twice_as_a_bonus() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
@ -107,6 +117,7 @@ fn points_scored_in_the_roll_after_a_spare_are_counted_twice_as_a_bonus() {
} }
#[test] #[test]
#[ignore]
fn consecutive_spares_each_get_a_one_roll_bonus() { fn consecutive_spares_each_get_a_one_roll_bonus() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
@ -140,6 +151,7 @@ fn if_the_last_frame_is_a_spare_you_get_one_extra_roll_that_is_scored_once() {
} }
#[test] #[test]
#[ignore]
fn a_strike_earns_ten_points_in_a_frame_with_a_single_roll() { fn a_strike_earns_ten_points_in_a_frame_with_a_single_roll() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();
@ -153,6 +165,7 @@ fn a_strike_earns_ten_points_in_a_frame_with_a_single_roll() {
} }
#[test] #[test]
#[ignore]
fn points_scored_in_the_two_rolls_after_a_strike_are_counted_twice_as_a_bonus() { fn points_scored_in_the_two_rolls_after_a_strike_are_counted_twice_as_a_bonus() {
let mut game = BowlingGame::new(); let mut game = BowlingGame::new();

Loading…
Cancel
Save