In this kata, we will build out and implement a program for an interactive bowling scoring program using Functional Responsive Programming, with RxJS and CycleJS technologies. Using Test-First disciplines, we will build these applications from scratch, starting with an empty file. The Kata will be built with a testing suite that displays its results in the browser console.
I hope these katas may inspire you to adopt these terrific agile practices to change the world with your own examples and craftsmanship.
[](http://wizardwerdna.github.io)Click here for a running version of the FRP Bowling application.
Click here for the source code and tests on Github
The purpose is to demonstrate how to use Test First Development to execute the Classical Bowling Kata. This Kata varies from he Classical Kata, in that the scoring program can handle partial games and will properly score partial strikes and spares.
This application's development was recorded in a series of screencasts following the TDD development of the application. They take forever to load, and I will update the list as I get them out, the introduction and three principal modules are presently laid out. None have been carefully edited -- this round is primarily or comments and discussion. All mistakes are mine. A written outline of the development will also be posted here as it is completed. Please remember, these are all very rough drafts.
name | description |
---|---|
01 - FRP Bowling - Introduction | Introduction to the application, overview of its modules, overview of TDD, and the testing framework. |
02 - FRP Bowling - Scorer$ | TDD development of the scorer$, which transforms a stream of rolls into a stream of frame scores. |
03 - FRP Bowling - Displayer$ | TDD development of the displayer$, which transforms a stream of rolls into a singleton stream of a string representing the display of the stream of rolls. |
04 - FRP Bowling - Enabler | TDD development of the enabler, which, given an array of scores from the scorer$ and string from the displayer$, determines which roll buttons are to be disableto be disabled. |
05 - CycleJS Static DOM Display | TDD development of the view for a bowling line component of a CycleJS Application |
06 - MVI Component Feature | Strategy for TDD development of a CycleJ Application Component using MVI |
07 - BowlingLine Feature | TDD development of a BowlingLine Component |
08 - Mainline Feature Using Component List | Development of an Application Providing a List of BowlingLine Components |
09 - Deletion Feature | Linking Component and Mainline to implement Deletion Feature |
The key functional parts built with corresponding unit tests are:
name | description |
---|---|
scorer$ | transforms a stream of rolls into a stream of frame scores |
displayer$ | transforms a stream of rolls into a string (as a singleton stream), in which gutter balls are shown as "-", spares are shown as "/" and strikes are shown as " X" (or just "X" for the tenth frame). |
enabled | a function that returns a value representing the maximum button to be enabled |
A Kata is a stylized solution to a small-ish and well-defined problem, meant to illustrate key development techniques. I will perform each kata for you in a screencast and in written materials summarizing the development. Then it is your turn. Based on what you read and saw, perform the kata yourself until you have assimilated the techniques for its solution.
Correct performance has nothing to do with mimicking what I show you, but by finding your own chops and improving on the craft. We repeat katas to better comprehend the practices and to build mental "muscle memory" so that skills flow quickly from your mind, as though you were touch typing the ideas themselves. They should take your breath away, as can a Mozart aria.
My name is Andrew Greenberg, and I would be grateful for your comments and suggestions as you work through these forms. You can find me by e-mail at wizardwerdna@gmail.com, or at @wizardwerdna on twitter. Now, let's get this kata started.
To install and run the application, clone the repository, go to the directory and install dependencies and start the webpack-dev-server.
git clone git@github.com:wizardwerdna/FRPBowlingKata.git
cd FRPBowlingKata
npm run setup
npm start
Then open your browser to http://localhost:8080/
If you care to follow the development in the Screencasts, you might want to start with the current version of the browser-base testing framework at my Seed site, and follow along the kata development to perform it yourself.
git clone git@github.com:wizardwerdna/RxJS.git <yourdesiredfolder>
cd <yourdesiredfolder>
npm run setup
npm start
The seed development system includes a homegrown test system that displays test results in the console of a Browser.
test('string identifying test or tests', function_containing_assertions, display)
A test
function posts a group to the browser console, bearing the string
as its identifier, and then runs the function. The third parameter display
indicates whether the group for the test is initially opened (when true, the default), or closed. Tests may be nested, where they will post groups internal to parent tests. Statistics of tests run,
passed and errors are maintained and will be posted after the outermost
nested function is completed.
expect(actual)
expect(actual).not
Expectations retain the value of actual
for use with subsequent testing
functions as shown below. If followed with a .not
, it will change the
meaning of the testing function, passing when the function would fail, and vice-versa. Thus, expect(1).toBe(1)
passes and expect(1).not.toBe(1)
fails. Likewise, expect(1).not.toBe(false)
passes and expect(1).toBe(false)
fails.
expect(actual).toBe(expected)
This test is passed, when actual === expected
, in the javascript
sense of '==='. Thus,
expect(actual).toEqual(expected)
This test is passed, when JSON.stringify(actual) === JSON.stringify(expected)
.
expect(actual$).toMarble(expected_marble_diagram)
This test is passed, when str2mbl(actual$) === expected_marble_diagram
.
expect(actual$).toBeStreamOf(expected)
For each element ele
of actual$, this expectation runs expect(ele).toBe(expected)
, as separate expectations. Thus, no tests are run when actual$ is the empty stream.
mbl2str$(marble_diagram)
This returns a stream corresponding to the ASCII marble diagram. The stream is treated synchronously, giving no meaning to each '-', except as a separator of
the elements presented. Thus mbl2str$('--a-b--c--|')
is the same stream as mbl2str$('-a-b-c|')
;
- Build the
scorer$
,displayer$
andenabler
modules. - Building the
run
function. - Building the
main
function using the reducer pattern.- Building the "dumb" DOM Return
- Connect DOM.sources to actions$
- Connect actions$ to build a state model$
- Connect the "dumb" DOM Return with the model data
- Implement Undo/Redo functionality
- Making and using a BowlingLine Component
- Building a UI to create a multi-line application.
- Building the Delete line functionality.
- In component
- add key to vtree from props.id
- add delete buttons to each component
- add DELETE actions
- return a Delete stream
- In top level
- in model, pull a merged stream of individual Delete streams
- in index, create a proxy to receive individual pulls
- after model, push the erged stream to proxy
- in intent, respond to received Delete ctions with DELETEPLAYER
- add DELETEPLAYER actions and reducer to model, deleting lines
- In component
test('empty', testScore('-|', '-|'));
function testScore(rolls, expected) {
return function () {
const rolls$ = mbl2str$(rolls);
expect(scorer$(rolls$)).toMarble(expected);
};
}
and we are red! So we can at last write some code. The test requires the function to return an empty Observable, so the easiest way to accomplish this is simply to return that constant. We call this use of a constant in testing a "slime." The advantage is that it makes no commitment how that value is computed, its just the value itself. Of course more tests will force us to take this specific constraint and generalize it. As "Uncle Bob" Martin wrote
As the tests get more specific, the code gets more generic.
So we write the most specific code we can imagine, and replace the return statement with:
return Observable.empty();
and we are green!
test('gutter', testScore('-0|', '-0|'));
and we are red! Examining these two test side-by-side, we notice that each test responds with its input alone. Thus, this test can be satisfied simply by returning its input.
return roll$
and we are green!
test('open', testScore('-0-0|', '-0|'));
and we are red! This test does not allow our last solution to work any longer, because it returns two, and not one 0. But RxJS has an operation that will help us by simply selecting the first value of the input stream, or the empty stream in the case of an empty stream.
Ok, bear with me. I realize this is a stupid, dumb-as-dirt, way to solve
the two gutter rolls from single gutter rolls. But our tests say the result
is the same for both of them. So our new test fails by copying the [0,0].
It turns out there is a five-character way to do this, so we do this, using
the take Operator
which simple takes the first element of the two cases, or empty, if there are no
elements.
return roll$.take(1)
and we are green!
So, in response to our solution, the test writing person writes a test that trivially avoids our gambit, focusing on the last element, and not the first:
test('open', testScore('-0-1|', '-1|'));
and we are red. That said, there is a
trivial solution that solves the problem. Yes, I know we could write deeper
code, but this is the simplest thing that works for our tests. We simply replace
the take
operator with one that focuses on the last element:
Rxjs
has a
takeLast Operator
and we code:
return roll$.takeLast(1)
and we are green, then we refactor!
OK, finally, our test-writer self changes the test slightly in a way that makes trivial solutions far more difficult to write.
test('open', testScore('-1-1|', '-2|'));
and we are red! Clearly, the solution is to
simply add up the numbers. RxJS has an operator for that: the
reduce Operator
.
This operator will be familiar with anyone who has used the corresponding reduce
function with Javascript arrays. We code:
return roll$.reduce((acc, curr) => acc + curr);
and we are green, then we refactor!
const sum = (acc, curr) => acc + curr;
return roll$.reduce(sum);
For our next test, we write code handling more than one frame:
test('two open', testScore('-0-0-1-1|', '-0-2|'));
which fails because our existing code only returns a single number. We are red!
This is a bit trickier than the earlier task, because we want to break up the open frames into separate groups before we reduce it with a sum. Happily, Rx has operators for that, one of which is
This operator will take a stream, and break it up into substreams each of
which has two elements, so that we can operate on each substream separately.
If there are an even number of elements, an additional empty stream
appears at the end, and if there are an odd number of elements,
then the last element of the stream is added as a singleton stream with the last
element. What we have is a resulting stream of streams, each of which we can reduce
.
The problem is that we want to have a flattened stream of the resulting
reductions, and there is an operator for that too:
mergeMap Operator
.
Folks familiar with other functional languages may recognize this as a function
called flatMap
;
What mergemap
does to an Observable of Observables is to first apply the map
function to each supplied inner Observable, and then merge the results together.
For those in the know, this is equivalent to a map
, followed by a mergeAll
.
We now write:
const frameScorer = frame => frame.reduce(sum);
return roll$
.windowCount(2)
.mergeMap(frameScorer);
and we are green, then we refactor!
But our code still isn't satisfactory, because it doesn't return a running sum of frame scores (because the previous example had a first frame score of zero). We work around this with the following test, to force a generalization.
test('two open', testScore('-1-1-2-2|', '-2-6|'));
and we are red!
We now add a new operator that handles this. reduce
would not suffice,
because it returns a single result, the sum of all the elements in the
stream. Rxjs has yet another useful result: the scan Operator
.
Scan performs the acccumulation as does reduce, but emits the result of the process with an element for each time. So we just add a scan to the end
return roll$
.windowCount(2)
.mergeMap(frameScorer);
.scan(sum);
and we are green! And so its time to move on to a different subject: the spare!
In American ten pins, a spare scores a spare frame as 10 pins plus the pins for the next roll. Thus:
test('spare', testScore('-5-5-5|', '-15-20|'));
and we are red! Se we solve this by generalizing the frameScorer:
const frameScorer = frame =>
frame.reduce((acc, curr) =>
acc + curr === 10 ?
acc + curr + 5 :
acc + curr
);
and we are green. Now, yes, I know we used the 5 bonus as a slime, but this solution is helpful because it gives us the shape of our next generalization, which will be forced by our next test.
test('spare', testScore('-5-5-9|', '-19-28|'));
and we are red! Here is our problem, the
windowCount solution does not let us look at rolls beyond the two roll frame.
So we need to gather and use that information somehow. RxJS has a solution for
that, the bufferCount
.
We have a few steps to solve this puzzle. The first is to figure out
how to "look ahead" to the next frame, and then to parse that frame
for the result. Again, we exploit a special Rxjs operator:
bufferCount Operator
.
This reduces a stream of values into a stream of arrays, of the length passed in
as the first parameter. If a second parameter is given, then the next array starts
that many items after the last one. Thus for the example stream in our test:
--5--5--9--|
, bufferCount(3,1)
results in the stream: --[5,5,9]--[5,9]--[9]--
. Then, we break that stream
up into the observable of observables, the first one having the first two
elements --[5,5,9]--[5,9]--
, and the second one having the remainder --[9]--
Then, to score a substream (we call them frames), we pick off the first triplet of that substream and check to see if its a spare. If so, we add all three elements, otherwise we add the first two, as we would for any open frame.
...
const frameScorer = frame =>
frame.reduce((acc, curr) =>
curr[0] + curr[1] ?
curr.reduce(sum) :
curr[0] + (curr[1] || 0)
);
...
return roll$
.bufferCount(3, 1);
.windowCount(2)
.mergeMap(frameScorer);
.scan(sum);
and we are green! Time to solve the strike!
A strike is scored differently from the spare, in that the strike frame gets ten pins plus the next two rolls, instead of just one. We write:
test('strike', testScore('-10-1-2|', '-13-16|'));
and we are red! This fails in two regards, getting both scores wrong. The first frame is scored as an 11, and the second frame adds two, and not 3 pins. We solve the first issue first, which requires only a minor adjustment to the framescorer to add up the three pins for strikes as well as spares,
const frameScorer = frame =>
frame.
take(1).
map(rolls =>
rolls.pins[0] === 10 || rolls.pins[0] + rolls.pins[1] === 10 ?
rolls.pins.reduce(sumReducer) :
rolls.pins[0] + (rolls.pins[1] || 0)
);
which solves part of the problem, not outputting 13 for the first frame, but doesn't solve the miscounting of the second one.
The problem is that while we are still breaking up every frame into two
rolls using windowCount(2)
, but strike frames take only one roll. Thus,
our program took the strike frame from the rolls 10 and 1, and then the
second frame with the roll 2. This requires a structural change, braking
frames into 1 or two rolls, depending on whether they are strikes or
non-strikes. The windowCount
operator doesn't cut it here.
But Rx has another operator that will suit us fine, after a bit of preprocessing. Consider the
groupBy
operator.
groupBy
, like windowCount
breaks the stream up to a stream of substreams,
but runs a "key" function to determine how to break up the frames. If we somehow
preprocess the stream of triplets into a stream of objects of the form:
{frame: pins: }
we can use groupBy(x => x.frame)
will break the stream into substreams by frame.
We begin by restructuring the sequence
.bufferCount(3,1)
.windowCount(2)
as
.bufferCount(3,1)
.scan(frameReducer, {frame: 0, isLastRoll: true}
.groupBy(roll => roll.frame, roll => roll.pins)
and then write a frameReducer to break up frames the same manner as windowCount. The second parameter removes the scaffolding to return to the original roll triplets.
...
const isStrike = (curr) => curr[0] === 10;
const frameMarker = (acc, curr) => (
acc.lastRoll && isSrike(curr) ?
{ pins: curr, frame: acc.frame + 1, lastRoll: true }
{
pins: curr,
frame: acc.lastRoll ? acc.frame + 1 : acc.frame,
lastRoll: !acc.lastRoll
}
)
...
return roll$
.bufferCount(3, 1)
.scan(frameMarker)
.groupBy(m => m.frame, m => m.pins)
.mergeMap(frameScorer);
.scan(sum);
and we are green, then we refactor!
We are not scoring every type of frame correctly. In particular, when a frame has a mark like a strike or a spare, by convention, we do not score the frame until the bowler has rolled all of the bonus pins. We write a test to prove this:
test('partial spare', testScore('-5-5|', '-|'));
and we are red! This happens because bufferCount
does not always produce arrays having three elements, and this makes reduces
err quietly. To fix this, we map all the responses from bufferCount to have
three elements, expanding short arrays with NaN
elements, so that the sum
reduce results in NaN for the frame score. Then we filter out the NaNs.
return roll$
.bufferCount(3, 1)
.map(trip => trip.concat(NaN, NaN).slice(0, 3))
.scan(frameMarker)
.groupBy(m => m.frame, m => m.pins)
.mergeMap(frameScorer);
.scan(sum)
.filter(score => !isNaN(score))
and we are green, then we refactor!
There is one last problem to solve before we can move on to the displayer: the spare game scores the last dangling 5 as a partial open and eleventh frame, as shown by this test:
test('spare game', testScore(
'-5-5-5-5-5-5-5-5-5-5-5-5-5-5-5-5-5-5-5-5-5|',
'-15-30-45-60-75-90-105-120-135-150|'
));
and we are red! We solve this simply
by limiting the number of frames we return, using the take
operator.
return roll$
...
.take(10);
and we are green, then we refactor! This yields a rather pretty piece of code that looks like it was designed by first and coded top-down. Agreed it is pretty. What do you think?
roll$
.let(makeFrames)
.let(scoreFrames)
.let(cleanPartials);
function makeFrames(roll$) {
return roll$
.bufferCount(3, 1)
.map(trip => trip.concat(NaN, NaN).slice(0, 3))
.scan(frameMarker)
.groupBy(m => m.frame, m => m.pins)
}
function scoreFrames(frame$) {
return frame$
.mergeMap(frameScorer);
.scan(sum)
}
function cleanPartials(score$) {
return score$
.filter(score => !isNaN(score))
.take(10)
}
The displayer$
takes the same stream of rolls as the scorer$
and returns
a singleton stream containing a string that would be displayed in the rolls
display line of the game display. Gutter balls are displayed as '-', not a
0. Strikes are display as a space followed by an X, except for the tenth
frame, and all other rolls are displayed either as the number of pins if an
open frame or first roll, and as a '/' if its a successful spare attempt.
We begin, as before, with a test for the empty game, which yields an empty string.
test('empty', testDisplay('-|', ''));
function testDisplay(rolls, expected) {
return function() {
const roll$ = mbl2str$(rolls);
expect(displayer$(roll$)).toBeStreamOf(expected);
};
}
and we are nred!
function displayer$(roll$) {
return Observer.of('');
}
and we are green!
test('gutter', testDisplay('-0|', '-'));
and we are red! We need to return different results
depending upon whether or not roll$ is empty. It turns out there is an operator
that can handle this case. Rxjs
has a
defaultIfEmpty Operator
return roll$
.mapTo('-')
.defaultIfEmpty('')
and we are green!
test('strike', testDisplay('-10|', ' X'));
and we are red!
roll$
.map(pins =>
pins === 10 ?
' X' :
'-'
)
.defaultIfEmpty('');
and we are green!
test('other', testDisplay('-1|', '1'));
and we are red! So we resolve this by adding another case, and then defaulting non-empty streams to '1', which is a slime.
roll$
.map(pins =>
pins === 10 ?
' X' :
pins === 0 ?
'-' :
'1'
)
.defaultIfEmpty('');
and we are green! We add another test forcing us to generalize the slime.
test('other', testDisplay('-9|', '9'));
and we are red! Of course, we could add a pins === 1 ?
case, and on and on for the rest of the pincounts, but there is a much shorter
generalization that works, and that is precisely when we do generalize -- when it is
more painful to do things case-by-case. As tests get more specific, the code gets
more generic.
roll$
.map(pins =>
pins === 10 ?
' X' :
pins === 0 ?
'-' :
pins.toString();
)
.defaultIfEmpty('');
and we are green, then we refactor!
const displayOneRoll = (pins) =>
pins === 10 ?
' X' :
pins === 0 ?
'-' :
pins.toString();
roll$
.map(displayOneRoll)
.defaultIfEmpty('');
test('two rolls', testDisplay('-0-9|', '-9'));
and we are red! So we must generalize in the single roll case, and reduce screams out as the alternative.
const displayOneRoll = (pins) =>
pins === 10 ?
' X' :
pins === 0 ?
'-' :
pins.toString();
roll$
.reduce((display, roll) => display + displayOneRoll(roll), '')
.defaultIfEmpty('');
and we are green, then we refactor! Because
the reduce handles the empty case effetively, we cand delete the call to defaultIfEmpty
.
const isStrike = pins => pins === 10;
const isGutter = pins => pins === 0;
const displayOneRoll = (pins) =>
isStrike(pins) ?
' X' :
isGutter(pins) ?
'-' :
pins.toString();
const addPinToDisplay = (display, pins) => display + displayOneRoll(pins);
roll$.display(addPinToDisplay, '');
test('spare', testDisplay('-0-10|', '-/'));
test('spare', testDisplay('-5-5|', '5/'));
and we are red! To determine whether a roll yield
a strike or a spare attempt, we will need the information from display in the
displayOneRoll code. Basically, its a spare if its a spare attempt and the
roll plus the pins from the last roll add to 10. We begin with the obvious
efforts, noting that the case -0-10|
should result in -/
:
...
const displayOneRoll = (display, pins) =>
display.length > 0 &&
pins + (parseInt(display[display.length - 1]) || 0) === 10 ?
'/' :
isStrike(pins) ?
' X' :
isGutter(pins) ?
'-' :
pins.toString();
const addPinToDisplay = (display, pins) => display + displayOneRoll(display, pins);
...
and we are green, then we refactor!
...
const lastRoll = display => parseInt(display[display.length - 1]) || 0;
const isSpareAttempt = (display, pins) => display.length > 0
const isSpare = (display, pins) =>
isSpareAttempt(display, pins) && pins + lastRoll(display, pins) === 10;
const displayOneRoll = (display, pins) =>
isSpare(display, pins) ?
'/' :
isStrike(pins) ?
' X' :
isGutter(pins) ?
'-' :
pins.toString();
const addPinToDisplay = (display, pins) => display + displayOneRoll(display, pins);
...
Now this is the structure we will end up with. Really, the rest of our tests
provide details for isSpareAttempt and how to handle isStrike in the tenth frame.
I will leave the roll-by-roll development as an exercise, I will provide the tests
and we end up with the following refactored version. Note in particular the
development of isSpareAttempt
and the introduction of spaceUnlessTenth
.
test('two spares', testDisplay('-5-10-0-5|', '5/-/'))
test('tenth frame', function() {
const preRolls = '-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0';
const preDisplay = '------------------';
test('spare', testDisplay(preRolls + '-0-10|', preDisplay + '-/'));
test('spare/non', testDisplay(preRolls + '-0-10-0|', preDisplay + '-/-'));
test('spare/strike', testDisplay(preRolls + '-0-10-10|', preDisplay + '-/X'));
test('strike', testDisplay(preRolls + '-10|', preDisplay + 'X'));
test('strike/non', testDisplay(preRolls + '-10-0|', preDisplay + 'X-'));
test('strike/open', testDisplay(preRolls + '-10-0-0|', preDisplay + 'X--'));
test('strike/spare', testDisplay(preRolls + '-10-0-10|', preDisplay + 'X-/'));
test('double', testDisplay(preRolls + '-10-10|', preDisplay + 'XX'));
test('turkey', testDisplay(preRolls + '-10-10-10|', preDisplay + 'XXX'));
});
and we are red!
function displayer$(pins$) {
const lastRoll = (display, pins) => parseInt(display[display.length - 1]) || 0;
const isDoubleAttempt = (display, pins) =>
display.length === 19 && display[18] === 'X';
const isStrikeSpareAttempt = (display, pins) =>
display.length === 20 && display[18] === 'X' && display[19] !== 'X';
const isOddRoll = (display, pins) => display.length % 2 === 1;
const isSpareAttempt = (display, pins) =>
(isOddRoll(display, pins) && !isDoubleAttempt(display, pins)) ||
isStrikeSpareAttempt(display, pins);
const isSpare = (display, pins) =>
isSpareAttempt(display, pins) && pins + lastRoll(display, pins) === 10;
const isGutter = (display, pins) => pins === 0;
const isStrike = (display, pins) => pins === 10;
const spaceUnlessTenth = (display) => display.length >= 18 ? '' : ' ';
const displayOneRoll = (display, pins) =>
isSpare(display, pins) ?
'/' :
isGutter(display, pins) ?
'-' :
isStrike(display, pins) ?
spaceUnlessTenth(display) + 'X' :
pins.toString();
const addPinsToDisplay = (display, pins) =>
display + displayOneRoll(display, pins);
return pins$.reduce(addPinsToDisplay);
}
and we are green!
TO BE DONE
test('empty', testEnable('', [], 10));
function testEnable(display, scores, expected) {
return () => expect(enabler(display, scores)).toBe(expected);
}
}
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('gutter', testEnable('-', [0], 10));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('other', testEnable('4', [4], 6));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('open', testEnable('44', [8], 10));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('spare', testEnable('4/', [], 10));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
``javascript test('spare/other', testEnable('4/4', [], 6));
and we are <span style="color: red">red</span>!
```typescript
SOLUTION GOES HERE
and we are green, then we refactor!
test('tenth frame', function() {
const preDisplay = '------------------';
const preScores = [0, 0, 0, 0, 0, 0, 0, 0, 0];
test('gutter', testEnable(preDisplay + '-', preScores.concat(0), 10));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('other', testEnable(preDisplay + '3', preScores.concat(3), 7));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('open', testEnable(preDisplay + '34', preScores.concat(7), -1));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('partial spare', testEnable(preDisplay + '3/', preScores, 10));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('complete spare', testEnable(
preDisplay + '3/2', preScores.concat(12), -1));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('strike', testEnable(preDisplay + 'X', preScores, 10));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('strike/gutter', testEnable(preDisplay + 'X-', preScores, 10));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('strike/other', testEnable(preDisplay + 'X3', preScores, 7));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('strike/open', testEnable(preDisplay + 'X34', preScores.concat(13), -1));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('strike/spare', testEnable(preDisplay + 'X3/', preScores.concat(20), -1));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('double', testEnable(preDisplay + 'XX', preScores, 10));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('double/gutter', testEnable(
preDisplay + 'XX-', preScores.concat(20), -1));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('double/other', testEnable(preDisplay + 'XX3', preScores.concat(23), -1));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!
test('turkey', testEnable(preDisplay + 'XXX', preScores.concat(30), -1));
and we are red!
SOLUTION GOES HERE
and we are green, then we refactor!