Skip to content

Latest commit

Β 

History

History
345 lines (282 loc) Β· 10.7 KB

step-by-step.md

File metadata and controls

345 lines (282 loc) Β· 10.7 KB

Day 18: Add features πŸ¦ŽπŸ––.

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

Behavior-Driven Development

Refactor to facilitate 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.

Tests list

Let's identify Lizard and Spock tests:

Rock Paper Scissors Lizard Spock

- Spock vaporizes rock
- Spock smashes scissors
- Paper disproves Spock
- Lizard poisons Spock
- Scissors decapitates lizard
- Rock crushes lizard
- Lizard eats paper

Add a first scenario

πŸ”΄ 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

first failing test

🟒 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

πŸ”΄ 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?

Fast-forward

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))};
    }
}

Reflect

  • How can B.D.D help you?
  • What would be simpler with this approach?
  • Conversely, what would be harder?