Julio Biason
5 years ago
2 changed files with 270 additions and 128 deletions
@ -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() |
||||||
} |
} |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue