By doing this exercise we implement functionality to count a score of a pin bowling game
“Hello, world!”
A classical example of the introduction to a new programming language is printing out “Hello, world!” to the standard output. However, I start learning programming languages with code exercise described in this post. The exercise is very simple and shows basic language syntax.
Project layout
If you start the project as described in the previous post you should have project structure as follows:
code-katas/
|
+-src/
| |
| +-lib.rs
|
+-Cargo.toml
Now we need to create Rust files for bowling game kata; in terms of Rust, each file is a module. To do that you need to create file src/bowling_game_kata.rs
and add pub mod bowling_game_kata;
line to src/lib.rs
(Remove empty it_works
test from src/lib.rs
files if you haven’t done it yet).
Now your project should look like:
code-katas/
|
+-src/
| |
| +-bowling_game_kata.rs
| +-lib.rs
|
+-Cargo.toml
In our case,
src/lib.rs
is the root module of the crate. The others*.rs
files will be modules with the same name as the files. You may declare sub-module inside*.rs
file by typing:mod module_name { //module stuff is here }
A folder can be a module as well. But you have to create
mod.rs
file inside it.crate/ | +-src/ | | | +-folder_mod/ | | | | | +-mod.rs //module name is 'folder_mod' | | +-sub_module_of_folder_mod.rs | +- lib.rs +-Cargo.toml
Set up tests environment
This is a step that I perform before start my daily code kata to make sure that I can run tests and able to check that I haven’t broken anything in my code. Type the following code into src/bowling_game_kata.rs
:
#[cfg(test)]
mod tests {
#[test]
fn nothing() {
}
}
And then run cargo test
in your project directory. You should see similar to:
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
Finished dev [unoptimized + debuginfo] target(s) in 0.67 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 1 test
test bowling_game_kata::tests::nothing ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests code-katas
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Cargo runs two types of tests. Tests for code and documentation. Doc-tests you may ignore, we are learning how to write code not, a documentation.
Make sure that our bowling_game_kata::tests::nothing
was run. This is an indicator that we can continue our exercise.
Marking module by
#[cfg(test)]
attribute we make it special for the compiler, it won’t be in the binary of Rust project. It is idiomatic for Rust to put unit tests inside a sub-module. To make any function a test you need to mark it by#[test]
attribute. That’s it.
The very first test
All who teach TDD say that you need to start with the simplest test case. Sometimes, the simplest thing is a very hard thing. If you are in the beginning of practicing TDD or don’t know what the first test should be; think what your first line of production code would be. In our case for counting bowling the game score we definitely need to create a Game
struct
, so let’s write a test for this.
Remove our nothing
test and write a test for creating an object of bowling game structure.
#[test]
fn creates_bowling_game() {
let game = Game;
}
Run our test.
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
error[E0425]: cannot find value `Game` in this scope
--> src/bowling_game_kata.rs:5:20
|
5 | let game = Game;
| ^^^^ not found in this scope
error: aborting due to previous error
error: Could not compile `code-katas`.
Build failed, waiting for other jobs to finish...
error: build failed
Heh, nothing special. Just a compile time error. Rust compiler tells us that it can’t find Game
. Let’s help him. Declare Game
struct
in our bowling_game_kata
module by typing pub struct Game;
line. Run the test again.
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
warning: struct is never used: `Game`, #[warn(dead_code)] on by default
--> src/bowling_game_kata.rs:1:1
|
1 | pub struct Game;
| ^^^^^^^^^^^^^^^^
error[E0425]: cannot find value `Game` in this scope
--> src/bowling_game_kata.rs:7:20
|
7 | let game = Game;
| ^^^^ not found in this scope
|
= help: possible candidate is found in another module, you can import it into scope:
`use bowling_game_kata::Game;`
error: aborting due to previous error
error: Could not compile `code-katas`.
Build failed, waiting for other jobs to finish...
error: build failed
Ah, we actually forget to import Game
struct into our tests
module.
By the way, notice that compiler gives us a hint how to fix error by printing
= help: possible candidate is found in another module, you can import it into scope: `use bowling_game_kata::Game;`
So add use bowling_game_kata::Game;
to our tests
module. Your code should look like this by now:
pub struct Game;
#[cfg(test)]
mod tests {
use bowling_game_kata::Game;
#[test]
fn creates_bowling_game() {
let game = Game;
}
}
Run the test.
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
warning: unused variable: `game`, #[warn(unused_variables)] on by default
--> src/bowling_game_kata.rs:9:13
|
9 | let game = Game;
| ^^^^
Finished dev [unoptimized + debuginfo] target(s) in 0.56 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 1 test
test bowling_game_kata::tests::creates_bowling_game ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Hooray, we pass the first test. However, TDD is not just to write tests and pass them. TDD has three stages RED
, when we write a test that fails, GREEN
, when we write least production code to make the test pass, and BLUE
, when we refactor our code and rerun tests to make sure that we haven’t screwed up anything. At this point we in the BLUE
stage, but what can be refactored? Not that much. The Game;
part of our test is not saying anything useful, for example, that we instantiate a Game
object. Let’s write a static factory function. Functions that have access to a struct
should be defined in impl
block.
pub struct Game;
impl Game {
pub fn new() -> Game {
Game
}
}
#[cfg(test)]
mod tests {
use bowling_game_kata::Game;
#[test]
fn creates_bowling_game() {
let game = Game::new();
}
}
And check that we haven’t broken anything.
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
warning: unused variable: `game`, #[warn(unused_variables)] on by default
--> src/bowling_game_kata.rs:15:13
|
15 | let game = Game::new();
| ^^^^
Finished dev [unoptimized + debuginfo] target(s) in 0.69 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 1 test
test bowling_game_kata::tests::creates_bowling_game ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
We got a compiler warning but who cares, we pass the test and we can move on.
Rust does not have constructors as a language item. Idiomatically static functions in
impl
blocks are used for this purpose.
Gutter game
When you are doing TDD, tests should drive your development (that’s why TDD is called TDD). You need to write tests that test behavior, not API. So what behavior we need to test next? Do not forget a test case should be the simplest as possible. How about a player rolls a ball and has knocked down none of the pins for the entire game? Let’s write this test and run.
#[test]
fn gutter_game() {
let mut game = Game::new();
for _ in 0..20 {
game.roll(0);
}
assert_eq!(game.score(), 0);
}
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
error: no method named `roll` found for type `bowling_game_kata::Game` in the current scope
--> src/bowling_game_kata.rs:22:18
|
22 | game.roll(0);
| ^^^^
error: no method named `score` found for type `bowling_game_kata::Game` in the current scope
--> src/bowling_game_kata.rs:25:25
|
25 | assert_eq!(game.score(), 0);
| ^^^^^
error: aborting due to 2 previous errors
error: Could not compile `code-katas`.
To learn more, run the command again with --verbose.
By default Rust variables are immutable. To make them mutable we need to specify it by using
mut
keyword in their declaration.
To fix this mess we need define two more functions: roll
to roll a bowling ball and score
to count the game score.
impl Game {
pub fn new() -> Game {
Game
}
pub fn roll(&mut self, pins: i32) {
}
pub fn score(self) -> i32 {
-1
}
}
$ cargo test
Finished dev [unoptimized + debuginfo] target(s) in 1.1 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 2 tests
test bowling_game_kata::tests::creates_bowling_game ... ok
test bowling_game_kata::tests::gutter_game ... FAILED
failures:
---- bowling_game_kata::tests::gutter_game stdout ----
thread 'bowling_game_kata::tests::gutter_game' panicked at 'assertion failed: `(left == right)` (left: `-1`, right: `0`)', src/bowling_game_kata.rs:32
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
bowling_game_kata::tests::gutter_game
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
error: test failed
I deliberately made score
return -1
. Why? To make sure that the test does something useful. “If tests test production code. Who tests the tests?” - it is a common question of newbies who start learning TDD. By failing the test we make sure that it covers some piece of our production functionality.
Let’s change -1
to 0
to pass the test.
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
//warnings are omitted
Finished dev [unoptimized + debuginfo] target(s) in 1.8 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 2 tests
test bowling_game_kata::tests::gutter_game ... ok
test bowling_game_kata::tests::creates_bowling_game ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
Some notes on writing
mut
keyword near a function arguments. One may argue that it is too verbose to writemut
to define that an argument will be mutated by the function and compiler could figure it out from the context. However, by doing TDD you first define API that you would like your program has and than implement it; when I typemut
I start thinking “Do I really need to mutate this argument? Can I come up with API that doesn’t mutate inner state?”. The other benefit from this is that when you look at a function signature you immediately see that the function has side effects, which is good to know.
As you may know, if you pass an argument to a function by value, ownership will be moved to the called function. What is happening when the function takes
self
by value? After returning from the function call compiler will prevent us from using this object and the allocated memory, used by the object, will be freed. For example:fn some_function() { let mut game = Game::new(); play_the_game(&mut game); let score = game.score(); //game.score() //<- compile time error error[E0382]: use of moved value: `game` //do stuff with score //here game object will be dropped and memory freed }
Now we can refactor. i32
is a signed type, however, the number of pins
and game score
can’t be negative, so let’s change them to u32
type. Run the tests, it still works. Tests are code, therefore, we need to clean them up too. As you may notice gutter_game
test creates Game
, thus, we can remove creates_bowling_game
test. Now your code should look like:
pub struct Game;
impl Game {
pub fn new() -> Game {
Game
}
pub fn roll(&mut self, pins: u32) {
}
pub fn score(self) -> u32 {
0
}
}
#[cfg(test)]
mod tests {
use bowling_game_kata::Game;
#[test]
fn gutter_game() {
let mut game = Game::new();
for _ in 0..20 {
game.roll(0);
}
assert_eq!(game.score(), 0);
}
}
All ones
The next test case is a little bit more complex than previous. We have a bowling game when only one pin was knocked down on a roll. Here is a test for it:
#[test]
fn all_ones() {
let mut game = Game::new();
for _ in 0..20 {
game.roll(1);
}
assert_eq!(game.score(), 20);
}
Run the tests.
$ cargo test
Finished dev [unoptimized + debuginfo] target(s) in 1.23 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 2 tests
test bowling_game_kata::tests::gutter_game ... ok
test bowling_game_kata::tests::all_ones ... FAILED
failures:
---- bowling_game_kata::tests::all_ones stdout ----
thread 'bowling_game_kata::tests::all_ones' panicked at 'assertion failed: `(left == right)` (left: `0`, right: `20`)', src/bowling_game_kata.rs:37
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
bowling_game_kata::tests::all_ones
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
error: test failed
To make it pass we can introduce a field points
to our Game
struct
and add a number of pins knocked down when roll
ing a bowling ball (Actually, score
is better name for the field, but I don’t want to mix score
field and function names here).
pub struct Game {
points: u32
}
impl Game {
pub fn new() -> Game {
Game { points: 0 }
}
pub fn roll(&mut self, pins: u32) {
self.points += pins;
}
pub fn score(self) -> u32 {
self.points
}
}
Run the tests. We passed them. Making all tests GREEN
we are allowed to refactor. We have code duplication in tests. These for
loops can be moved into help function. Let’s called it roll_many
; it will take three arguments: game
, times
and pins
.
fn roll_many(game: &mut Game, times: u32, pins: u32) {
for _ in 0..times {
game.roll(pins);
}
}
#[test]
fn gutter_game() {
let mut game = Game::new();
roll_many(&mut game, 20, 0);
assert_eq!(game.score(), 0);
}
#[test]
fn all_ones() {
let mut game = Game::new();
roll_many(&mut game, 20, 1);
assert_eq!(game.score(), 20);
}
Do not forget to rerun the tests to make sure they are passed.
Roll one spare
Now we are going to test functionality which takes into account spare
. When all 10
frame pins knocked down by two rolls, is called spare
. For each spare
a player earns bonus points which are the number of pins knocked down by the next roll. So the test is the following:
#[test]
fn one_spare() {
let mut game = Game::new();
game.roll(5);
game.roll(5);
game.roll(3);
roll_many(&mut game, 17, 0);
assert_eq!(game.score(), 16);
}
Run the tests:
$ cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 3 tests
test bowling_game_kata::tests::all_ones ... ok
test bowling_game_kata::tests::gutter_game ... ok
test bowling_game_kata::tests::one_spare ... FAILED
failures:
---- bowling_game_kata::tests::one_spare stdout ----
thread 'bowling_game_kata::tests::one_spare' panicked at 'assertion failed: `(left == right)` (left: `13`, right: `16`)', src/bowling_game_kata.rs:56
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
bowling_game_kata::tests::one_spare
test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured
error: test failed
If you look at current implementation you will notice that we need to somehow save previous rolls and a frame index in roll
function. It is too complicated. Let’s refactor our code. But we can’t do that because we have the failing test! #[ignore]
attribute is going to rescue us. #[ignore]
attribute signals Cargo that test should not be run. Put it on the one_spare
test as follows:
#[test]
#[ignore]
fn one_spare() {
let mut game = Game::new();
game.roll(5);
game.roll(5);
game.roll(3);
roll_many(&mut game, 17, 0);
assert_eq!(game.score(), 16);
}
Run the tests:
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
Finished dev [unoptimized + debuginfo] target(s) in 1.11 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 3 tests
test bowling_game_kata::tests::one_spare ... ignored
test bowling_game_kata::tests::gutter_game ... ok
test bowling_game_kata::tests::all_ones ... ok
test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured
Great, we’ve got GREEN
tests and we can refactor. The problem with current implementation is that we have computation where we are not expecting. We are counting the game score in roll
function instead of score
. Let’s introduce rolls
filed which type will be std::vec::Vec
. It is a simple vector; we can append it by push
ing items into it. We push
the number of knocked down pins to rolls
in each invocation of roll
function and then looping the vector counting the game score in score
function. So the code looks like:
pub struct Game {
points: u32,
rolls: Vec<u32>
}
impl Game {
pub fn new() -> Game {
Game { points: 0, rolls: Vec::new() }
}
pub fn roll(&mut self, pins: u32) {
self.rolls.push(pins);
}
pub fn score(self) -> u32 {
let mut score = 0;
for pins in self.rolls {
score += pins;
}
score
}
}
Run the tests:
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
warning: field is never used: `points`, #[warn(dead_code)] on by default
--> src/bowling_game_kata.rs:2:5
|
2 | points: u32,
| ^^^^^^^^^^^
warning: field is never used: `points`, #[warn(dead_code)] on by default
--> src/bowling_game_kata.rs:2:5
|
2 | points: u32,
| ^^^^^^^^^^^
Finished dev [unoptimized + debuginfo] target(s) in 0.95 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 3 tests
test bowling_game_kata::tests::one_spare ... ignored
test bowling_game_kata::tests::all_ones ... ok
test bowling_game_kata::tests::gutter_game ... ok
test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured
Hah! Let’s remove useless points
field. However, a bowling game has 10
frames each of them has 2
rolls (if it is not a strike
). So let rewrite loop to emphasize our business logic.
pub fn score(self) -> u32 {
let mut score = 0;
let mut roll_index = 0;
for _ in 0..10 {
score += self.rolls[roll_index] + self.rolls[roll_index + 1];
roll_index += 2;
}
score
}
Run the tests:
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
Finished dev [unoptimized + debuginfo] target(s) in 1.43 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 3 tests
test bowling_game_kata::tests::one_spare ... ignored
test bowling_game_kata::tests::all_ones ... ok
test bowling_game_kata::tests::gutter_game ... ok
test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured
So now we can go further. Let’s remove #[ignore]
attribute and make sure that we have a failing test.
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
Finished dev [unoptimized + debuginfo] target(s) in 0.94 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 3 tests
test bowling_game_kata::tests::gutter_game ... ok
test bowling_game_kata::tests::all_ones ... ok
test bowling_game_kata::tests::one_spare ... FAILED
failures:
---- bowling_game_kata::tests::one_spare stdout ----
thread 'bowling_game_kata::tests::one_spare' panicked at 'assertion failed: `(left == right)` (left: `13`, right: `16`)', src/bowling_game_kata.rs:64
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
bowling_game_kata::tests::one_spare
test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured
error: test failed
Indeed, a miracle has not happened we need to fix the one_spare
test. To do so we need to check that two rolls of the same frame knocked down 10
pins and add the bonus - the number of pins knocked down by the next roll:
for _ in 0..10 {
if self.rolls[roll_index] + self.rolls[roll_index + 1] == 10 {
score += 10 + self.rolls[roll_index + 2];
} else {
score += self.rolls[roll_index] + self.rolls[roll_index + 1];
}
roll_index += 2;
}
Run the tests:
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
Finished dev [unoptimized + debuginfo] target(s) in 1.18 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 3 tests
test bowling_game_kata::tests::gutter_game ... ok
test bowling_game_kata::tests::one_spare ... ok
test bowling_game_kata::tests::all_ones ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
Hooray, let’s refactor a bit. This line self.rolls[roll_index] + self.rolls[roll_index + 1] == 10
is hard to read and actually has special meaning - it is a spare
, so let’s move it to a separated function called is_spare
; it needs to take roll_index
as argument. And self.rolls[roll_index + 2]
move to spare_bonus
function to underline what it really is.
pub fn score(self) -> u32 {
let mut score = 0;
let mut roll_index = 0;
for _ in 0..10 {
if self.is_spare(roll_index) {
score += 10 + self.spare_bonus(roll_index);
} else {
score += self.rolls[roll_index] + self.rolls[roll_index + 1];
}
roll_index += 2;
}
score
}
fn is_spare(&self, roll_index: usize) -> bool {
self.rolls[roll_index] + self.rolls[roll_index + 1] == 10
}
fn spare_bonus(&self, roll_index: usize) -> u32 {
self.rolls[roll_index + 2]
}
Run the tests:
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
Finished dev [unoptimized + debuginfo] target(s) in 1.23 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 3 tests
test bowling_game_kata::tests::gutter_game ... ok
test bowling_game_kata::tests::one_spare ... ok
test bowling_game_kata::tests::all_ones ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
Do not forget to clean up your tests too. These two game.roll(5);
lines can be moved to roll_spare
help function.
The code should look like this at the current stage:
pub struct Game {
rolls: Vec<u32>
}
impl Game {
pub fn new() -> Game {
Game { rolls: Vec::new() }
}
pub fn roll(&mut self, pins: u32) {
self.rolls.push(pins);
}
pub fn score(self) -> u32 {
let mut score = 0;
let mut roll_index = 0;
for _ in 0..10 {
if self.is_spare(roll_index) {
score += 10 + self.spare_bonus(roll_index);
} else {
score += self.rolls[roll_index] + self.rolls[roll_index + 1];
}
roll_index += 2;
}
score
}
fn is_spare(&self, roll_index: usize) -> bool {
self.rolls[roll_index] + self.rolls[roll_index + 1] == 10
}
fn spare_bonus(&self, roll_index: usize) -> u32 {
self.rolls[roll_index + 2]
}
}
#[cfg(test)]
mod tests {
use bowling_game_kata::Game;
fn roll_many(game: &mut Game, times: u32, pins: u32) {
for _ in 0..times {
game.roll(pins);
}
}
fn roll_spare(game: &mut Game) {
game.roll(5);
game.roll(5);
}
#[test]
fn gutter_game() {
let mut game = Game::new();
roll_many(&mut game, 20, 0);
assert_eq!(game.score(), 0);
}
#[test]
fn all_ones() {
let mut game = Game::new();
roll_many(&mut game, 20, 1);
assert_eq!(game.score(), 20);
}
#[test]
fn one_spare() {
let mut game = Game::new();
roll_spare(&mut game);
game.roll(3);
roll_many(&mut game, 17, 0);
assert_eq!(game.score(), 16);
}
}
One strike
Now we are ready to roll a strike
. Bonus for the strike
is the sum of pins knocked down by the next two rolls. So the test is as follows:
#[test]
fn one_strike() {
let mut game = Game::new();
game.roll(10);
game.roll(3);
game.roll(4);
roll_many(&mut game, 16, 0);
assert_eq!(game.score(), 24);
}
Run the tests:
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
Finished dev [unoptimized + debuginfo] target(s) in 1.17 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 4 tests
test bowling_game_kata::tests::gutter_game ... ok
test bowling_game_kata::tests::all_ones ... ok
test bowling_game_kata::tests::one_spare ... ok
test bowling_game_kata::tests::one_strike ... FAILED
failures:
---- bowling_game_kata::tests::one_strike stdout ----
thread 'bowling_game_kata::tests::one_strike' panicked at 'index out of bounds: the len is 19 but the index is 19', /Users/rustbuild/src/rust-buildbot/slave/nightly-dist-rustc-mac/build/src/libcollections/vec.rs:1392
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
bowling_game_kata::tests::one_strike
test result: FAILED. 3 passed; 1 failed; 0 ignored; 0 measured
error: test failed
It fails. Progress! Now we can implement the functionality. The strategy is the same as with the spare
. Check if a roll knocked down 10
pins and sum up the roll with the next two rolls pins.
pub fn score(self) -> u32 {
let mut score = 0;
let mut roll_index = 0;
for _ in 0..10 {
if self.rolls[roll_index] == 10 {
score += 10 + self.rolls[roll_index + 1] + self.rolls[roll_index + 2];
roll_index += 1;
} else if self.is_spare(roll_index) {
score += 10 + self.spare_bonus(roll_index);
roll_index += 2;
} else {
score += self.rolls[roll_index] + self.rolls[roll_index + 1];
roll_index += 2;
}
}
score
}
Run the tests:
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
Finished dev [unoptimized + debuginfo] target(s) in 1.41 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 4 tests
test bowling_game_kata::tests::all_ones ... ok
test bowling_game_kata::tests::gutter_game ... ok
test bowling_game_kata::tests::one_spare ... ok
test bowling_game_kata::tests::one_strike ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured
GREEN
! It is about time for refactoring. Again the strategy is the same as with the spare
. self.rolls[roll_index] == 10
condition move to a separated function is_strike
and self.rolls[roll_index + 1] + self.rolls[roll_index + 2]
move to strike_bonus
function. Also, move game.roll(10);
to roll_strike()
in our tests.
pub struct Game {
rolls: Vec<u32>
}
impl Game {
pub fn new() -> Game {
Game { rolls: Vec::new() }
}
pub fn roll(&mut self, pins: u32) {
self.rolls.push(pins);
}
pub fn score(self) -> u32 {
let mut score = 0;
let mut roll_index = 0;
for _ in 0..10 {
if self.is_strike(roll_index) {
score += 10 + self.strike_bonus(roll_index);
roll_index += 1;
} else if self.is_spare(roll_index) {
score += 10 + self.spare_bonus(roll_index);
roll_index += 2;
} else {
score += self.rolls[roll_index] + self.rolls[roll_index + 1];
roll_index += 2;
}
}
score
}
fn is_strike(&self, roll_index: usize) -> bool {
self.rolls[roll_index] == 10
}
fn strike_bonus(&self, roll_index: usize) -> u32 {
self.rolls[roll_index + 1] + self.rolls[roll_index + 2]
}
fn is_spare(&self, roll_index: usize) -> bool {
self.rolls[roll_index] + self.rolls[roll_index + 1] == 10
}
fn spare_bonus(&self, roll_index: usize) -> u32 {
self.rolls[roll_index + 2]
}
}
#[cfg(test)]
mod tests {
use bowling_game_kata::Game;
fn roll_many(game: &mut Game, times: u32, pins: u32) {
for _ in 0..times {
game.roll(pins);
}
}
fn roll_spare(game: &mut Game) {
game.roll(5);
game.roll(5);
}
fn roll_strike(game: &mut Game) {
game.roll(10);
}
#[test]
fn gutter_game() {
let mut game = Game::new();
roll_many(&mut game, 20, 0);
assert_eq!(game.score(), 0);
}
#[test]
fn all_ones() {
let mut game = Game::new();
roll_many(&mut game, 20, 1);
assert_eq!(game.score(), 20);
}
#[test]
fn one_spare() {
let mut game = Game::new();
roll_spare(&mut game);
game.roll(3);
roll_many(&mut game, 17, 0);
assert_eq!(game.score(), 16);
}
#[test]
fn one_strike() {
let mut game = Game::new();
roll_strike(&mut game);
game.roll(3);
game.roll(4);
roll_many(&mut game, 16, 0);
assert_eq!(game.score(), 24);
}
}
Perfect game
The last case that should be considered is when a player rolls only the strikes. We need take into account 12
rolls; not 10
. Last two rolls are the bonus for the 10th strike
. The test is:
#[test]
fn perfect_game() {
let mut game = Game::new();
roll_many(&mut game, 12, 10);
assert_eq!(game.score(), 300);
}
Run the tests:
$ cargo test
Compiling code-katas v0.1.0 (file:///Users/alex-diez/Projects/code-katas)
Finished dev [unoptimized + debuginfo] target(s) in 1.27 secs
Running target/debug/deps/code_katas-91959ec1e8c184b3
running 5 tests
test bowling_game_kata::tests::gutter_game ... ok
test bowling_game_kata::tests::all_ones ... ok
test bowling_game_kata::tests::one_spare ... ok
test bowling_game_kata::tests::one_strike ... ok
test bowling_game_kata::tests::perfect_game ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured
Passed? Sometimes you will write tests that pass. It is not bad, and it is not good either; because you would not understand why it had happened. Now we need to find out why it passed. In this particular case when we count the game score is_strike
condition will be true
all the time therefore 10
rolls times 10
pins plus 20
pins as the bonus gives us 300
. So we are done.
Final code
pub struct Game {
rolls: Vec<u32>
}
impl Game {
pub fn new() -> Game {
Game { rolls: Vec::new() }
}
pub fn roll(&mut self, pins: u32) {
self.rolls.push(pins);
}
pub fn score(self) -> u32 {
let mut score = 0;
let mut roll_index = 0;
for _ in 0..10 {
if self.is_strike(roll_index) {
score += 10 + self.strike_bonus(roll_index);
roll_index += 1;
} else if self.is_spare(roll_index) {
score += 10 + self.spare_bonus(roll_index);
roll_index += 2;
} else {
score += self.rolls[roll_index] + self.rolls[roll_index + 1];
roll_index += 2;
}
}
score
}
fn is_strike(&self, roll_index: usize) -> bool {
self.rolls[roll_index] == 10
}
fn strike_bonus(&self, roll_index: usize) -> u32 {
self.rolls[roll_index + 1] + self.rolls[roll_index + 2]
}
fn is_spare(&self, roll_index: usize) -> bool {
self.rolls[roll_index] + self.rolls[roll_index + 1] == 10
}
fn spare_bonus(&self, roll_index: usize) -> u32 {
self.rolls[roll_index + 2]
}
}
#[cfg(test)]
mod tests {
use bowling_game_kata::Game;
fn roll_many(game: &mut Game, times: u32, pins: u32) {
for _ in 0..times {
game.roll(pins);
}
}
fn roll_spare(game: &mut Game) {
game.roll(5);
game.roll(5);
}
fn roll_strike(game: &mut Game) {
game.roll(10);
}
#[test]
fn gutter_game() {
let mut game = Game::new();
roll_many(&mut game, 20, 0);
assert_eq!(game.score(), 0);
}
#[test]
fn all_ones() {
let mut game = Game::new();
roll_many(&mut game, 20, 1);
assert_eq!(game.score(), 20);
}
#[test]
fn one_spare() {
let mut game = Game::new();
roll_spare(&mut game);
game.roll(3);
roll_many(&mut game, 17, 0);
assert_eq!(game.score(), 16);
}
#[test]
fn one_strike() {
let mut game = Game::new();
roll_strike(&mut game);
game.roll(3);
game.roll(4);
roll_many(&mut game, 16, 0);
assert_eq!(game.score(), 24);
}
#[test]
fn perfect_game() {
let mut game = Game::new();
roll_many(&mut game, 12, 10);
assert_eq!(game.score(), 300);
}
}
Post Script
To master any skills it’d better repeat the exercise few times. When I started practicing TDD I did 14 days cycle for each code kata, now I do it in 10 days cycle (you know human psychology, we like rounded numbers). I don’t like lots of files in one directory that’s why my project has the followings layout:
code-katas/
|
+-src/
| |
| +-bowling_game_kata/
| | |
| | +- day_1.rs
| | +- day_2.rs
| | | ...
| | +- day_N.rs
| | +- mod.rs
| |
| +- other_code_kata/
| | |
| | +- ...
| |
| +-lib.rs
|
+-Cargo.toml