Features are detailed in the rockPaperScissors.feature
feature file:
Feature: Rock Paper Scissors Game
Scenario: Player 1 wins with πͺ¨ over βοΈ
Given Player 1 chooses πͺ¨
And Player 2 chooses βοΈ
When they play
Then the result should be Player 1 because rock crushes scissors
Scenario: Player 1 wins with π over πͺ¨
Given Player 1 chooses π
And Player 2 chooses πͺ¨
When they play
Then the result should be Player 1 because paper covers rock
Scenario: Player 2 wins with βοΈ over π
Given Player 1 chooses π
And Player 2 chooses βοΈ
When they play
Then the result should be Player 2 because scissors cuts paper
Scenario: Player 2 wins with πͺ¨ over βοΈ
Given Player 1 chooses βοΈ
And Player 2 chooses πͺ¨
When they play
Then the result should be Player 2 because rock crushes scissors
Scenario Outline: Draw
Given Player 1 chooses <choice>
And Player 2 chooses <choice>
When they play
Then the result should be Draw because same choice
Examples:
| choice |
| πͺ¨ |
| βοΈ |
| π |
Tests are automated with cucumber.js
through step definitions
in the rockPapersScissorsSteps.ts
:
import {Given, Then, When} from "@cucumber/cucumber";
import {Choice, Result, RockPaperScissors} from "../../src/rockPaperScissors";
import * as assert from "assert";
let result: Result;
let player1: Choice;
let player2: Choice;
Given(/^Player (\d+) chooses (.*)$/, function (player, choice) {
if (player === 1) player1 = choice;
else player2 = choice;
});
When(/^they play$/, function () {
result = RockPaperScissors.play(player1, player2)
});
Then(/^the result should be (.*) because (.*)$/, function (expectedWinner, expectedReason) {
assert.deepEqual(result, {winner: expectedWinner, reason: expectedReason});
});
We can continue using Behavior-Driven Development
to implement the new features:
- This file speaks for itself from a business point of view
- Those scenarios have been used to align and understand features
- AND to drive the implementation
Before implementing the features we prepare the code to "welcome" them.
Let's identify how to simplify it:
export class RockPaperScissors {
static play(player1: Choice, player2: Choice): Result {
// Too many branches
// High cyclomatic complexity
if (player1 === player2) return {winner: "Draw", reason: "same choice"};
else if (player1 === "πͺ¨" && player2 === "βοΈ")
return {winner: "Player 1", reason: "rock crushes scissors"};
else if (player1 === "π" && player2 === "πͺ¨")
// Duplication everywhere
return {winner: "Player 1", reason: "paper covers rock"};
else if (player1 === "βοΈ" && player2 === "π")
return {winner: "Player 1", reason: "scissors cuts paper"};
else if (player2 === "πͺ¨" && player1 === "βοΈ")
return {winner: "Player 2", reason: "rock crushes scissors"};
else if (player2 === "π" && player1 === "πͺ¨")
return {winner: "Player 2", reason: "paper covers rock"};
else return {winner: "Player 2", reason: "scissors cuts paper"};
}
}
We may use a map
that declares what beats what
and use it from this function
:
const whatBeatsWhat = new Map<string, string>([
[keyFor("πͺ¨", "βοΈ"), "rock crushes scissors"],
[keyFor("π", "πͺ¨"), "paper covers rock"],
[keyFor("βοΈ", "π"), "scissors cuts paper"]
]);
function keyFor(choice1: Choice, choice2: Choice): string {
return `${choice1}_${choice2}`;
}
export class RockPaperScissors {
static play(player1: Choice, player2: Choice): Result {
if (player1 === player2) return {winner: "Draw", reason: "same choice"};
else if (whatBeatsWhat.has(keyFor(player1, player2)))
return {winner: "Player 1", reason: whatBeatsWhat.get(keyFor(player1, player2))};
else return {winner: "Player 2", reason: whatBeatsWhat.get(keyFor(player2, player1))};
}
}
It will be much easier to add features now.
Let's identify Lizard
and Spock
tests:
- Spock vaporizes rock
- Spock smashes scissors
- Paper disproves Spock
- Lizard poisons Spock
- Scissors decapitates lizard
- Rock crushes lizard
- Lizard eats paper
π΄ We start by adding a new Scenario
Scenario: Player 1 wins with π over πͺ¨
Given Player 1 chooses π
And Player 2 chooses πͺ¨
When they play
Then the result should be Player 1 because spock vaporizes rock
π’ We make it pass as fast as possible
// Add Spock choice
export type Choice = "πͺ¨" | "π" | "βοΈ" | "π";
export type Winner = "Player 1" | "Player 2" | "Draw"
export type Result = {
winner: Winner,
reason: string
};
const whatBeatsWhat = new Map<string, string>([
[keyFor("πͺ¨", "βοΈ"), "rock crushes scissors"],
[keyFor("π", "πͺ¨"), "paper covers rock"],
[keyFor("βοΈ", "π"), "scissors cuts paper"],
// Declare Spock wins over rock
[keyFor("π", "πͺ¨"), "spock vaporizes rock"]
]);
- We update the tests list
β
Spock vaporizes rock
- Spock smashes scissors
- Paper disproves Spock
- Lizard poisons Spock
- Scissors decapitates lizard
- Rock crushes lizard
- Lizard eats paper
π΅ Because we have prepared the code to welcome the new features, we have not that much space for improvement.
We can refactor the Scenarios
to use a single Outline
that will use Examples
as input:
Feature: Rock, Paper, Scissors, Lizard, Spock Game
Scenario: Player 1 wins with πͺ¨ over βοΈ
Given Player 1 chooses πͺ¨
And Player 2 chooses βοΈ
When they play
Then the result should be Player 1 because rock crushes scissors
Scenario: Player 1 wins with π over πͺ¨
Given Player 1 chooses π
And Player 2 chooses πͺ¨
When they play
Then the result should be Player 1 because paper covers rock
Scenario: Player 2 wins with βοΈ over π
Given Player 1 chooses π
And Player 2 chooses βοΈ
When they play
Then the result should be Player 2 because scissors cuts paper
Scenario: Player 2 wins with πͺ¨ over βοΈ
Given Player 1 chooses βοΈ
And Player 2 chooses πͺ¨
When they play
Then the result should be Player 2 because rock crushes scissors
Scenario: Player 1 wins with π over πͺ¨
Given Player 1 chooses π
And Player 2 chooses πͺ¨
When they play
Then the result should be Player 1 because spock vaporizes rock
Scenario Outline: Draw
Given Player 1 chooses <choice>
And Player 2 chooses <choice>
When they play
Then the result should be Draw because same choice
Examples:
| choice |
| πͺ¨ |
| βοΈ |
| π |
Feature
file looks like this now:
Feature: Rock, Paper, Scissors, Lizard, Spock Game
Scenario Outline: Rock, Paper, Scissors, Lizard, Spock Winners
Given Player 1 chooses <player1>
And Player 2 chooses <player2>
When they play
Then the result should be <result> because <reason>
Examples:
| player1 | player2 | result | reason |
| πͺ¨ | βοΈ | Player 1 | rock crushes scissors |
| π | πͺ¨ | Player 1 | paper covers rock |
| π | βοΈ | Player 2 | scissors cuts paper |
| βοΈ | πͺ¨ | Player 2 | rock crushes scissors |
| πͺ¨ | πͺ¨ | Draw | same choice |
| βοΈ | βοΈ | Draw | same choice |
| π | π | Draw | same choice |
| π | πͺ¨ | Player 1 | spock vaporizes rock |
π΄ Spock smashes scissors
Scenario Outline: Rock, Paper, Scissors, Lizard, Spock Winners
Given Player 1 chooses <player1>
And Player 2 chooses <player2>
When they play
Then the result should be <result> because <reason>
Examples:
| player1 | player2 | result | reason |
...
| βοΈ | π | Player 2 | spock smashes scissors |
π’ We add the scoring logic to the Map
const whatBeatsWhat = new Map<string, string>([
[keyFor("πͺ¨", "βοΈ"), "rock crushes scissors"],
[keyFor("π", "πͺ¨"), "paper covers rock"],
[keyFor("βοΈ", "π"), "scissors cuts paper"],
[keyFor("π", "πͺ¨"), "spock vaporizes rock"],
[keyFor("π", "βοΈ"), "spock smashes scissors"]
]);
π΅ Anything to refactor?
We have completed our tests list:
β
Spock vaporizes rock
β
Spock smashes scissors
β
Paper disproves Spock
β
Lizard poisons Spock
β
Scissors decapitates lizard
β
Rock crushes lizard
β
Lizard eats paper
We end-up with this code:
export type Choice = "πͺ¨" | "π" | "βοΈ" | "π" | "π¦";
export type Winner = "Player 1" | "Player 2" | "Draw"
export type Result = {
winner: Winner,
reason: string
};
const whatBeatsWhat = new Map<string, string>([
[keyFor("πͺ¨", "βοΈ"), "rock crushes scissors"],
[keyFor("π", "πͺ¨"), "paper covers rock"],
[keyFor("βοΈ", "π"), "scissors cuts paper"],
[keyFor("π", "πͺ¨"), "spock vaporizes rock"],
[keyFor("π", "βοΈ"), "spock smashes scissors"],
[keyFor("π", "π"), "paper disproves spock"],
[keyFor("π¦", "π"), "lizard poisons spock"],
[keyFor("βοΈ", "π¦"), "scissors decapitates lizard"],
[keyFor("πͺ¨", "π¦"), "rock crushes lizard"],
[keyFor("π¦", "π"), "lizard eats paper"]
]);
function keyFor(choice1: Choice, choice2: Choice): string {
return `${choice1}_${choice2}`;
}
export class RockPaperScissorsLizardSpock {
static play(player1: Choice, player2: Choice): Result {
if (player1 === player2) return {winner: "Draw", reason: "same choice"};
else if (whatBeatsWhat.has(keyFor(player1, player2)))
return {winner: "Player 1", reason: whatBeatsWhat.get(keyFor(player1, player2))};
else return {winner: "Player 2", reason: whatBeatsWhat.get(keyFor(player2, player1))};
}
}
- How can
B.D.D
help you? - What would be
simpler
with this approach? - Conversely, what would be
harder
?