Weekend Coding Kata Challenge – Yatzy Refactor in Typescript

I had done the Yatzy refactoring kata (https://github.com/emilybache/Yatzy-Refactoring-Kata) a few times before. I think once it was in Kotlin, and once in Java. However, due to time constraint, I never managed to finish the kata. I think I was pair-programming with one of my work colleagues, and we only had around 90 minutes to do the kata.

I wanted to try again the other week, so I decided to re-do the kata but this time in Typescript stack. It managed to complete the whole thing, but it did took me longer than 90 minutes. Here is the final product after a single afternoon of coding

https://github.com/suryast/yatzy-ts

import assert from 'assert';
import Yatzy from '../src/Yatzy';

describe('Chance', () => {
  it('scores sum of all dice', () => {
    assert.strictEqual(15, new Yatzy(2, 3, 4, 5, 1).chance());
    assert.strictEqual(16, new Yatzy(3, 3, 4, 5, 1).chance());
  });
});

describe('Ones', () => {
  it('score the sum of 1s', () => {
    assert.strictEqual(1, new Yatzy(1, 2, 3, 4, 5).ones());
    assert.strictEqual(2, new Yatzy(1, 2, 1, 4, 5).ones());
    assert.strictEqual(0, new Yatzy(6, 2, 2, 4, 5).ones());
    assert.strictEqual(4, new Yatzy(1, 2, 1, 1, 1).ones());
  });
});

describe('Twos', () => {
  it('score the sum of 2s', () => {
    assert.strictEqual(4, new Yatzy(1, 2, 3, 2, 6).twos());
    assert.strictEqual(10, new Yatzy(2, 2, 2, 2, 2).twos());
  });
});

describe('Threes', () => {
  it('score the sum of 3s', () => {
    assert.strictEqual(6, new Yatzy(1, 2, 3, 2, 3).threes());
    assert.strictEqual(12, new Yatzy(2, 3, 3, 3, 3).threes());
  });
});

describe('Fours', () => {
  it('score the sum of 4s', () => {
    assert.strictEqual(12, new Yatzy(4, 4, 4, 5, 5).fours());
    assert.strictEqual(8, new Yatzy(4, 4, 5, 5, 5).fours());
    assert.strictEqual(4, new Yatzy(4, 5, 5, 5, 5).fours());
  });
});

describe('Fives', () => {
  it('score the sum of fives', () => {
    assert.strictEqual(10, new Yatzy(4, 4, 4, 5, 5).fives());
    assert.strictEqual(15, new Yatzy(4, 4, 5, 5, 5).fives());
    assert.strictEqual(20, new Yatzy(4, 5, 5, 5, 5).fives());
  });
});

describe('Sixes', () => {
  it('score the sum of sixes', () => {
    assert.strictEqual(0, new Yatzy(4, 4, 4, 5, 5).sixes());
    assert.strictEqual(6, new Yatzy(4, 4, 6, 5, 5).sixes());
    assert.strictEqual(18, new Yatzy(6, 5, 6, 6, 5).sixes());
  });
});

describe('Yatzy', () => {
  it('scores 50', () => {
    assert.strictEqual(50, new Yatzy(4, 4, 4, 4, 4).yatzy());
    assert.strictEqual(50, new Yatzy(6, 6, 6, 6, 6).yatzy());
    assert.strictEqual(0, new Yatzy(6, 6, 6, 6, 3).yatzy());
  });
});

describe('Score a pair', () => {
  it('scores the sum of the highest pair', () => {
    assert.strictEqual(6, new Yatzy(3, 4, 3, 5, 6).onePair());
    assert.strictEqual(10, new Yatzy(5, 3, 3, 3, 5).onePair());
    assert.strictEqual(12, new Yatzy(5, 3, 6, 6, 5).onePair());
  });
});

describe('Two pair', () => {
  it('scores the sum of the two pairs', () => {
    assert.strictEqual(16, new Yatzy(3, 3, 5, 4, 5).twoPairs());
    assert.strictEqual(16, new Yatzy(3, 3, 5, 5, 5).twoPairs());
  });
});

describe('Three of a kind', () => {
  it('scores the sum of the three of the kind', () => {
    assert.strictEqual(9, new Yatzy(3, 3, 3, 4, 5).threeOfAKind());
    assert.strictEqual(15, new Yatzy(5, 3, 5, 4, 5).threeOfAKind());
    assert.strictEqual(9, new Yatzy(3, 3, 3, 3, 5).threeOfAKind());
  });
});

describe('Four of a kind', () => {
  it('scores the sum of the four of the kind', () => {
    assert.strictEqual(12, new Yatzy(3, 3, 3, 3, 5).fourOfAKind());
    assert.strictEqual(20, new Yatzy(5, 5, 5, 4, 5).fourOfAKind());
    assert.strictEqual(12, new Yatzy(3, 3, 3, 3, 3).fourOfAKind());
  });
});

describe('Small straight', () => {
  it('scores 15', () => {
    assert.strictEqual(15, new Yatzy(1, 2, 3, 4, 5).smallStraight());
    assert.strictEqual(15, new Yatzy(2, 3, 4, 5, 1).smallStraight());
    assert.strictEqual(0, new Yatzy(1, 2, 2, 4, 5).smallStraight());
  });
});

describe('Large straight', () => {
  it('scores 20', () => {
    assert.strictEqual(20, new Yatzy(6, 2, 3, 4, 5).largeStraight());
    assert.strictEqual(20, new Yatzy(2, 3, 4, 5, 6).largeStraight());
    assert.strictEqual(0, new Yatzy(1, 2, 2, 4, 5).largeStraight());
  });
});

describe('Full house', () => {
  it('scores the sum of the full house', () => {
    assert.strictEqual(18, new Yatzy(6, 2, 2, 2, 6).fullHouse());
    assert.strictEqual(0, new Yatzy(2, 3, 4, 5, 6).fullHouse());
  });
});
export default class Yatzy {

  private readonly dice: number[];

  constructor(d1: number, d2: number, d3: number, d4: number, d5: number) {
    this.dice = [d1, d2, d3, d4, d5];
  }

  chance(): number {
    return this.dice.reduce((prev, next) => prev + next);
  }

  ones(): number {
    return this.singles(1);
  }

  twos(): number {
    return this.singles(2);
  }

  threes(): number {
    return this.singles(3);
  }


  fours(): number {
    return this.singles(4);
  }

  fives(): number {
    return this.singles(5);
  }

  sixes(): number {
    return this.singles(6);
  }

  yatzy(): number {
    return (new Set(this.dice).size === 1) ? 50 : 0;
  }

  onePair(): number {
    const keysArr = this.createDieKeysArr();
    const max = Math.max.apply(Math, keysArr);

    return this.hasPairs() ? max * 2 : 0;
  }

  twoPairs(): number {
    const keysArr = this.createDieKeysArr();
    const sumOfTwoPairs = keysArr.map(item => item * 2)
        .reduce((firstPair, secondPair) => firstPair + secondPair)

    return this.hasPairs() ? sumOfTwoPairs : 0;
  }

  threeOfAKind(): number {
    let result = this.createDiceObject();

    const threeOfAKindDie = this.getDieIndex(result, 3);

    return this.hasPairs() ? threeOfAKindDie * 3 : 0;
  }

  fourOfAKind(): number {
    let result = this.createDiceObject();

    const fourOfAKindDie = this.getDieIndex(result, 4);

    return this.hasPairs() ? fourOfAKindDie * 4 : 0;
  }

  smallStraight(): number {
    const sizeOfDiceSet = new Set(this.dice).size;
    const min = Math.min.apply(Math, this.dice);

    return (sizeOfDiceSet === 5 && min === 1)? 15:0;
  }

  largeStraight(): number {
    const sizeOfDiceSet = new Set(this.dice).size;
    const max = Math.max.apply(Math, this.dice);

    return (sizeOfDiceSet === 5 && max === 6)? 20:0;
  }

  fullHouse(): number {
    const sum = this.threeOfAKind() + this.onePair()
    return this.hasPairs() ? sum : 0
  }

  private singles(input: number): number {
    return this.dice.filter(die => die === input)
        .reduce((prev, next) => prev + next, 0);
  }

  private hasPairs() {
    return this.dice.length > new Set(this.dice).size;
  }

  private getDieIndex(result: {}, dieIndex: number) {
    // @ts-ignore
    const fourOfAKindDie: number = Object.keys(result).find((key) =>
        // @ts-ignore
        (result[key] >= dieIndex) ? +key! : 0
    );
    return fourOfAKindDie;
  }

  private createDiceObject() {
    let diceObject = {};

    this.dice.forEach(function (die) { // @ts-ignore
      diceObject[die] = (diceObject[die] || 0) + 1;
    });

    return diceObject;
  }

  private createDieKeysArr() {
    const result = this.createDiceObject()

    // @ts-ignore
    let keysArr: number[] = []
    Object.keys(result).find((key) => {
      // @ts-ignore
      (result[key] === 2 || result[key] === 3) ? keysArr.push(+key!) : keysArr.push(null);
    });

    return keysArr;
  }
}

Having the chance to redo this kata again allows me to discover some interesting new features of Typescript. Looking at some of what I wrote a few weeks ago, I no longer remember some of the syntaxes and why I had written things in a certain way. The names of some of the helper names make no sense :P. I guess I will revisit this kata again some other time.

Working with Mocha for doing the unit tests was not bad. Next time, I would prefer to use Jest.