Skip to content
Permalink
Browse files

port exercise bowling from JS track - fixes #332 (#355)

  • Loading branch information...
trvrfrd authored and matthewmorgan committed Oct 22, 2017
1 parent c7e048a commit 5ca33a6b2d7880c0040d0870617c65e3f9f05453
Showing with 393 additions and 0 deletions.
  1. +16 −0 config.json
  2. +80 −0 exercises/bowling/README.md
  3. +159 −0 exercises/bowling/bowling.spec.js
  4. +67 −0 exercises/bowling/example.js
  5. +71 −0 exercises/bowling/package.json
@@ -994,6 +994,22 @@
"Exception handling"
]
},
{
"core": false,
"difficulty": 8,
"slug": "bowling",
"topics": [
"arrays",
"control-flow-(conditionals)",
"control-flow-(loops)",
"exception-handling",
"games",
"parsing",
"text-formatting"
],
"unlocked_by": "grade-school",
"uuid": "dbf26ef1-62ff-4cb1-8ac7-09b022df3b2f"
},
{
"uuid": "6a1eee0e-f8d4-446d-9c52-f31c3700af1b",
"slug": "diamond",
@@ -0,0 +1,80 @@
# Bowling

Score a bowling game.

Bowling is game where players roll a heavy ball to knock down pins
arranged in a triangle. Write code to keep track of the score
of a game of bowling.

## Scoring Bowling

A game consists of 10 frames. A frame is composed of one or two balls thrown, with 10 pins standing at the start of the frame. There are three cases for the tabulation of a frame:

* An open frame is where a score of less than 10 is recorded for the frame. In this case the score for the frame is the number of pins knocked down after the second throw.

* A spare is where all ten pins are knocked down after the second throw. The total value of a spare is 10 plus the number of pins knocked down in the next throw.

* A strike is where all ten pins are knocked down after the first throw. The total value of a strike is 10 plus the number of pins knocked down in the next two throws. If a strike is immediately followed by another strike, then the first strike's value cannot be totalled until the next throw.

Here is a three frame example:

| Frame 1 | Frame 2 | Frame 3 |
| :-------------: |:-------------:| :---------------------:|
| X (strike) | 5/ (spare) | 9 0 (open frame) |

Frame 1 is (10 + 5 + 5) = 20

Frame 2 is (5 + 5 + 9) = 19

Frame 3 is (9 + 0) = 9

This means the current running total is 48.

The tenth frame in the game is a special case. If the player throws a strike or a spare, then they get one extra throw called a fill ball. Fill balls exist to calculate the total of the 10th frame. Scoring a strike or spare on the fill ball does not give the player more fill balls. The total value of the 10th frame is the total number of pins knocked down.

For a tenth frame of X1/ (strike and a spare), the total value is 20.

For a tenth frame of XXX (three strikes), the total value is 30.

## Setup

Go through the setup instructions for ECMAScript to
install the necessary dependencies:

http://exercism.io/languages/ecmascript

## Requirements

Install assignment dependencies:

```bash
$ npm install
```

Write code to keep track of the score of a game of bowling. It should
support two operations:

* `roll(pins : int)` is called each time the player rolls a ball. The
argument is the number of pins knocked down.
* `score() : int` is called only at the very end of the game. It
returns the total score for that game.

## Making the test suite pass

Execute the tests with:

```bash
$ npm test
```

In the test suites all tests but the first have been skipped.

Once you get a test passing, you can enable the next one by
changing `xtest` to `test`.

## Source

The Bowling Game Kata at but UncleBob [http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata](http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata)

## Submitting Incomplete Solutions
It's possible to submit an incomplete solution so you can see how others have completed the exercise.
@@ -0,0 +1,159 @@
import Bowling from './bowling';

describe('Bowling', () => {
describe('Check game can be scored correctly.', () => {
test('should be able to score a game with all gutterballs', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(new Bowling(rolls).score()).toEqual(0);
});

xtest('should be able to score a game with all open frames', () => {
const rolls = [3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6];
expect(new Bowling(rolls).score()).toEqual(90);
});

xtest('a spare followed by zeros is worth ten points', () => {
const rolls = [6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(new Bowling(rolls).score()).toEqual(10);
});

xtest('points scored in the roll after a spare are counted twice', () => {
const rolls = [6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(new Bowling(rolls).score()).toEqual(16);
});

xtest('consecutive spares each get a one roll bonus', () => {
const rolls = [5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(new Bowling(rolls).score()).toEqual(31);
});

xtest('should allow fill ball when the last frame is a spare', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7];
expect(new Bowling(rolls).score()).toEqual(17);
});

xtest('a strike earns ten points in a frame with a single roll', () => {
const rolls = [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(new Bowling(rolls).score()).toEqual(10);
});

xtest('points scored in the two rolls after a strike are counted twice as a bonus', () => {
const rolls = [10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(new Bowling(rolls).score()).toEqual(26);
});

xtest('should be able to score multiple strikes in a row', () => {
const rolls = [10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(new Bowling(rolls).score()).toEqual(81);
});

xtest('should allow fill balls when the last frame is a strike', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 1];
expect(new Bowling(rolls).score()).toEqual(18);
});

xtest('rolling a spare with the two roll bonus does not get a bonus roll', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 3];
expect(new Bowling(rolls).score()).toEqual(20);
});

xtest('strikes with the two roll bonus do not get bonus rolls', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10];
expect(new Bowling(rolls).score()).toEqual(30);
});

xtest('a strike with the one roll bonus after a spare in the last frame does not get a bonus', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 10];
expect(new Bowling(rolls).score()).toEqual(20);
});

xtest('should be able to score a perfect game', () => {
const rolls = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10];
expect(new Bowling(rolls).score()).toEqual(300);
});
});

describe('Check game rules.', () => {
xtest('rolls can not score negative points', () => {
const rolls = [-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Pins must have a value from 0 to 10'),
);
});

xtest('a roll can not score more than 10 points', () => {
const rolls = [11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Pins must have a value from 0 to 10'),
);
});

xtest('two rolls in a frame can not score more than 10 points', () => {
const rolls = [5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Pin count exceeds pins on the lane'),
);
});

xtest('two bonus rolls after a strike in the last frame can not score more than 10 points', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 5, 6];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Pin count exceeds pins on the lane'),
);
});

xtest('two bonus rolls after a strike in the last frame can score more than 10 points if one is a strike', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 6];
expect(new Bowling(rolls).score()).toEqual(26);
});

xtest('the second bonus rolls after a strike in the last frame can not be a strike if the first one is not a strike', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 6, 10];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Pin count exceeds pins on the lane'),
);
});

xtest('an unstarted game can not be scored', () => {
const rolls = [];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Score cannot be taken until the end of the game'),
);
});

xtest('an incomplete game can not be scored', () => {
const rolls = [0, 0];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Score cannot be taken until the end of the game'),
);
});

xtest('a game with more than ten frames and no last frame spare or strike can not be scored', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Should not be able to roll after game is over'),
);
});

xtest('bonus rolls for a strike in the last frame must be rolled before score can be calculated', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Score cannot be taken until the end of the game'),
);
});

xtest('both bonus rolls for a strike in the last frame must be rolled before score can be calculated', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Score cannot be taken until the end of the game'),
);
});

xtest('bonus roll for a spare in the last frame must be rolled before score can be calculated', () => {
const rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3];
expect(() => { new Bowling(rolls).score(); }).toThrow(
new Error('Score cannot be taken until the end of the game'),
);
});
});
});
@@ -0,0 +1,67 @@
export default class Bowling {
constructor(rolls) {
this.rolls = rolls;
}

score() {
const initialState = {
frameNumber: 1,
rollNumber: 1,
pinsRemaining: 10,
spareLastFrame: false,
strikeLastFrame: false,
twoStrikesInARow: false,
fillBall: false,
score: 0,
};

const finalState = this.rolls.reduce((state, roll) => {
if (roll < 0 || roll > 10) {
throw new Error('Pins must have a value from 0 to 10');
}

if (roll > state.pinsRemaining) {
throw new Error('Pin count exceeds pins on the lane');
}

if (state.frameNumber > 10) {
throw new Error('Should not be able to roll after game is over');
}

const finalFrame = state.frameNumber === 10;
const strike = state.rollNumber === 1 && roll === 10;
const spare = state.rollNumber === 2 && roll === state.pinsRemaining;
const frameOver = finalFrame
? (!state.fillBall && !spare && state.rollNumber === 2) || state.rollNumber === 3
: strike || spare || state.rollNumber === 2;

let score = state.score + roll;

if (state.strikeLastFrame && state.rollNumber < 3) { score += roll; }
if (state.spareLastFrame && state.rollNumber === 1) { score += roll; }
if (state.twoStrikesInARow && state.rollNumber === 1) { score += roll; }

const next = {};

next.frameNumber = frameOver ? state.frameNumber + 1 : state.frameNumber;
next.rollNumber = frameOver ? 1 : state.rollNumber + 1;
next.pinsRemaining = finalFrame
? ((strike || spare) ? 10 : state.pinsRemaining - roll)
: (frameOver ? 10 : state.pinsRemaining - roll);
next.spareLastFrame = frameOver ? spare : state.spareLastFrame;
next.strikeLastFrame = frameOver ? strike : state.strikeLastFrame;
next.twoStrikesInARow = frameOver ? strike && state.strikeLastFrame : state.twoStrikesInARow;
next.fillBall = next.fillBall || (finalFrame && (strike || spare));
next.score = score;

return next;
}, initialState);

if (finalState.frameNumber !== 11) {
throw new Error('Score cannot be taken until the end of the game');
}

return finalState.score;
}
}

Oops, something went wrong.

0 comments on commit 5ca33a6

Please sign in to comment.
You can’t perform that action at this time.