diff --git a/package.json b/package.json index aba0b6c..13a2452 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,6 @@ "rimraf": "^2.6.1", "ts-jest": "^20.0.4", "tslint": "^5.3.2", - "typescript": "^2.3.3" + "typescript": "2.4.1" } } diff --git a/src/DiceCup.ts b/src/DiceCup.ts index c95c069..9bc9ae1 100644 --- a/src/DiceCup.ts +++ b/src/DiceCup.ts @@ -1,13 +1,7 @@ export default class DiceCup { - private numberOfDices: number; - - public constructor(dices: number) { - this.numberOfDices = dices; - } - - public cast(): Array { + public cast(numberOfDice: number): Array { const pips: Array = []; - for (let i = 0; i < this.numberOfDices; i++) { + for (let i = 0; i < numberOfDice; i++) { pips.push(Math.floor(Math.random() * 6) + 1); } return pips; diff --git a/src/Game.ts b/src/Game.ts new file mode 100644 index 0000000..e434a74 --- /dev/null +++ b/src/Game.ts @@ -0,0 +1,229 @@ +import DiceCup from './DiceCup'; +import Score from './Score'; +import ScoreAnalyzer from './ScoreAnalyzer'; +import Scorecard from './Scorecard'; + +import { Category } from './categories'; + +import { + ACES, TWOS, THREES, FOURS, FIVES, SIXES, + THREE_OF_A_KIND, FOUR_OF_A_KIND, FULL_HOUSE, + SMALL_STRAIGHT, LARGE_STRAIGHT, YAHTZEE, CHANCE +} from './categories'; + +class Game { + private _players: Game.Players; + private _running = false; + private _currentPlayer: Game.Player; + private numberOfCasts: number; + private scorecard: Scorecard[]; + private playerId: number; + private lastDiceCast: Game.DiceCast; + + public constructor(private diceCup: DiceCup, private scoreAnalyzer: ScoreAnalyzer) { + this._players = []; + this.playerId = 1; + this.scorecard = []; + this.lastDiceCast = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; + } + + public player(name: string): Game.Player { + if (this.running) { + throw new Game.GameAlreadyRunningError; + } + return this.newPlayer(name); + } + + public start() { + if (this.running) { + throw new Game.GameAlreadyRunningError; + } + if (this.players.length === 0) { + throw new Game.NoPlayersAdded(); + } + if (this.players.length < 2) { + throw new Game.MinimumPlayerRequirementsError(); + } + + this._currentPlayer = this.players.shift() as Game.Player; + this._running = true; + this.numberOfCasts = 0; + } + + public cast(dice: number[] = []): Game.Result { + if (!this.running) { + throw new Game.GameNotRunningError; + } + + if (this.numberOfCasts === 3) { + throw new Game.DiceCastingExceededError(); + } + + let numberOfDice = dice.length; + if (numberOfDice === 0) { + numberOfDice = 5; + } + + let diceRecastMap: Game.DiceCast = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; + dice.forEach((pip) => { + diceRecastMap[pip]++; + }); + + if (dice.length > 0) { + for (let recastDice in diceRecastMap) { + if ((diceRecastMap[recastDice] !== 0 && this.lastDiceCast[recastDice] === 0) + || this.lastDiceCast[recastDice] < diceRecastMap[recastDice]) { + throw new Game.NonAvailableDiceError(); + } + } + } + + this.lastDiceCast = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; + const diceCast = this.diceCup.cast(numberOfDice); + diceCast.forEach((pip) => { + this.lastDiceCast[pip]++; + }); + + const scores = this.scoreAnalyzer.scores(diceCast); + this.numberOfCasts++; + + return { + dice: diceCast, + scores: scores, + }; + } + + public score(score: Score) { + this.scorecard[this.currentPlayer.id].add(score); + + let nextPlayerId = this.currentPlayer.id + 1; + if (nextPlayerId > this._players.length) { + nextPlayerId = 1; + } + + const nextPlayer = this._players.filter((player) => { + return player.id === nextPlayerId; + }); + + this._currentPlayer = nextPlayer.pop() as Game.Player; + } + + private newPlayer(name: string): Game.Player { + this._players.filter((player) => { + if (player.name === name) { + throw new Game.PlayerNameAlreadyExistsError(); + } + }); + const player = { id: this.playerId, name }; + this.playerId++; + this._players.push(player); + this.scorecard[player.id] = new Scorecard(); + return player; + } + + public get players(): Game.Player[] { + return [...this._players]; + } + + public get running(): boolean { + return this._running; + } + + public get currentPlayer(): Game.Player { + return this._currentPlayer; + } + + public get scores(): number { + return this.scorecard[this.currentPlayer.id].score; + } + + public get usedCategories(): Category[] { + return this.scorecard[this.currentPlayer.id].categories.map((score) => score.category); + } + + public get unusedCategories(): Category[] { + const unusedCategories: Category[] = [ + ACES, TWOS, THREES, FOURS, FIVES, SIXES, + THREE_OF_A_KIND, FOUR_OF_A_KIND, FULL_HOUSE, + SMALL_STRAIGHT, LARGE_STRAIGHT, YAHTZEE, CHANCE + ]; + + this.usedCategories.forEach((category) => unusedCategories.splice( + unusedCategories.indexOf(category), 1 + )); + + return unusedCategories; + } +} + +namespace Game { + + export type Result = { + dice: number[]; + scores: Score[]; + }; + + export type Player = { + id: number; + name: string; + }; + + export type Players = Player[]; + + export type DiceCast = { + [n: number]: number; + }; + + export class GameAlreadyRunningError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, GameAlreadyRunningError.prototype); + } + + } + + export class GameNotRunningError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, GameNotRunningError.prototype); + } + } + + export class DiceCastingExceededError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, DiceCastingExceededError.prototype); + } + } + + export class NoPlayersAdded extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, NoPlayersAdded.prototype); + } + } + + export class PlayerNameAlreadyExistsError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, PlayerNameAlreadyExistsError.prototype); + } + } + + export class MinimumPlayerRequirementsError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, MinimumPlayerRequirementsError.prototype); + } + } + + export class NonAvailableDiceError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, NonAvailableDiceError.prototype); + } + } + +} + +export default Game; diff --git a/src/Player.ts b/src/Player.ts new file mode 100644 index 0000000..c05f45f --- /dev/null +++ b/src/Player.ts @@ -0,0 +1,3 @@ +export default class Player { + public constructor(public readonly name: string, public readonly id: number) { } +} diff --git a/src/__tests__/DiceCup.test.ts b/src/__tests__/DiceCup.test.ts index 41ddf52..5092506 100644 --- a/src/__tests__/DiceCup.test.ts +++ b/src/__tests__/DiceCup.test.ts @@ -2,20 +2,20 @@ import DiceCup from '../DiceCup'; describe('DiceCup', () => { it('casts five numbers between 1 and 6', () => { - const numberOfDices = 5; - const cup = new DiceCup(numberOfDices); - const cast = cup.cast(); + const numberOfDice = 5; + const cup = new DiceCup(); + const cast = cup.cast(numberOfDice); - expect(cast).toHaveLength(numberOfDices); + expect(cast).toHaveLength(numberOfDice); cast.forEach((pip) => { expect(pip).toBeGreaterThanOrEqual(1); expect(pip).toBeLessThanOrEqual(6); }); }); - it('casts exactly as many dices as are in the cup', () => { - const numberOfDices = 10; - const cup = new DiceCup(numberOfDices); - expect(cup.cast()).toHaveLength(numberOfDices); + it('casts exactly as many dice as are in the cup', () => { + const numberOfDice = 10; + const cup = new DiceCup(); + expect(cup.cast(numberOfDice)).toHaveLength(numberOfDice); }); }); diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts new file mode 100644 index 0000000..9c63fda --- /dev/null +++ b/src/__tests__/Game.test.ts @@ -0,0 +1,302 @@ +import DiceCup from '../DiceCup'; +import Game from '../Game'; +import Score from '../Score'; +import ScoreAnalyzer from '../ScoreAnalyzer'; +import Scorecard from '../Scorecard'; + +import { + aces, twos, threes, fours, fives, sixes, + threeOfAKind, fourOfAKind, fullHouse, + smallStraight, largeStraight, chance, yahtzee +} from '../categories'; + +import { + ACES, TWOS, THREES, FOURS, FIVES, SIXES, + THREE_OF_A_KIND, FOUR_OF_A_KIND, FULL_HOUSE, + SMALL_STRAIGHT, LARGE_STRAIGHT, YAHTZEE, CHANCE +} from '../categories'; + +describe('Game', () => { + let diceCup: DiceCup; + let game: Game; + let returnedDice: number[]; + + beforeEach(() => { + const DiceCupMock = jest.fn(() => ({ + cast: jest.fn(() => returnedDice) + })); + + diceCup = new DiceCupMock(); + + const scoreanalyzer = new ScoreAnalyzer([ + aces, twos, threes, fours, fives, sixes, + threeOfAKind, fourOfAKind, fullHouse, + smallStraight, largeStraight, chance, yahtzee + ]); + + game = new Game(diceCup, scoreanalyzer); + }); + + describe('game management', () => { + it('adds players', () => { + const player1 = game.player('Hauke'); + const player2 = game.player('Katharina'); + expect(game.players).toContain(player1); + expect(game.players).toContain(player2); + }); + + it('forbids adding same player name twice', () => { + game.player('Hauke'); + expect(() => game.player('Hauke')).toThrowError(Game.PlayerNameAlreadyExistsError); + }); + + it('returns copy of player list', () => { + game.player('Hauke'); + game.player('Katharina'); + expect(game.players.length).toBe(2); + game.players.pop(); + expect(game.players.length).toBe(2); + }); + + describe('no players added', () => { + it('throws errror', () => { + expect(() => game.start()).toThrowError(Game.NoPlayersAdded); + }); + }); + + describe('players added', () => { + it('starts the game', () => { + game.player('Hauke'); + game.player('Klaus'); + game.start(); + expect(game.running).toBe(true); + }); + }); + + describe('start the game', () => { + it('needs at least two players', () => { + game.player('Otto'); + expect(() => game.start()).toThrowError(Game.MinimumPlayerRequirementsError); + }); + }); + }); + + describe('game is running', () => { + it('forbids adding new players', () => { + game.player('Hauke'); + game.player('Klaas'); + game.start(); + expect(() => game.player('Karl')).toThrowError(Game.GameAlreadyRunningError); + }); + + it('forbids starting game', () => { + game.player('Hauke'); + game.player('Philip'); + game.start(); + expect(() => game.start()).toThrowError(Game.GameAlreadyRunningError); + }); + + it('starts with first player', () => { + const player1 = game.player('Hauke'); + const player2 = game.player('Katharina'); + + game.start(); + + expect(game.currentPlayer).toBe(player1); + expect(game.currentPlayer).not.toBe(player2); + }); + + describe('player casts dice', () => { + it('forbids to cast the dice before game has started', () => { + game.player('Karsten'); + expect(() => game.cast()).toThrowError(Game.GameNotRunningError); + }); + + it('casts the dice with 5 by default', () => { + game.player('Karsten'); + game.player('Sven'); + game.start(); + returnedDice = [1, 2, 3, 4, 5]; + game.cast(); + expect(diceCup.cast).toHaveBeenCalledWith(5); + }); + + it('it returns possible scores', () => { + game.player('Karsten'); + game.player('Ole'); + game.start(); + + returnedDice = [1, 2, 3, 4, 5]; + + expect(game.cast()).toEqual({ + dice: [1, 2, 3, 4, 5], + scores: [ + new Score(ACES, 1), + new Score(TWOS, 2), + new Score(THREES, 3), + new Score(FOURS, 4), + new Score(FIVES, 5), + new Score(SMALL_STRAIGHT, 30), + new Score(LARGE_STRAIGHT, 40), + new Score(CHANCE, 15), + ] + }); + }); + }); + + describe('player casts selected dice again', () => { + it('returns possible scores', () => { + game.player('Lydia'); + game.player('Karsten'); + game.start(); + + returnedDice = [1, 2, 3, 4, 5]; + game.cast(); + expect(diceCup.cast).toHaveBeenCalledWith(5); + + returnedDice = [1, 2, 3, 2, 2]; + expect(game.cast([4, 5])).toEqual({ + dice: [1, 2, 3, 2, 2], + scores: [ + new Score(ACES, 1), + new Score(TWOS, 6), + new Score(THREES, 3), + new Score(THREE_OF_A_KIND, 10), + new Score(CHANCE, 10), + ] + }); + expect(diceCup.cast).toHaveBeenLastCalledWith(2); + }); + + describe('player casts non existent dice', () => { + it('throws error', () => { + game.player('Lydia'); + game.player('Karsten'); + game.start(); + + returnedDice = [1, 2, 3, 4, 5]; + game.cast(); + expect(() => game.cast([6])).toThrowError(Game.NonAvailableDiceError); + expect(() => game.cast([2, 2])).toThrowError(Game.NonAvailableDiceError); + }); + }); + }); + + describe('player casts dice more than three times', () => { + it('throws error', () => { + game.player('Horst'); + game.player('Karsten'); + game.start(); + game.cast(); + game.cast(); + game.cast(); + expect(() => game.cast()).toThrowError(Game.DiceCastingExceededError); + }); + }); + + describe('player selects score', () => { + it('adds score to scorecard', () => { + game.player('Horst'); + game.player('Harald'); + game.start(); + + returnedDice = [1, 2, 3, 2, 2]; + const result = game.cast(); + + // { + // dice: [1, 2, 3, 2, 2], + // scores: [ + // new Score(ACES, 1), + // new Score(TWOS, 6), + // new Score(THREES, 3), + // new Score(THREE_OF_A_KIND, 10), + // new Score(CHANCE, 10), + // ] + // } + + game.score(result.scores[1]); + game.score(result.scores[1]); + + expect(game.scores).toBe(6); + }); + + it('forbids to add score to used category', () => { + game.player('Horst'); + game.player('Harald'); + game.start(); + + returnedDice = [1, 2, 3, 2, 2]; + const result = game.cast(); + + game.score(result.scores[1]); + game.score(result.scores[1]); + + expect(() => game.score(result.scores[1])).toThrow(Scorecard.CategoryAlreadyUsedError); + }); + + it('returns used categories', () => { + game.player('Horst'); + game.player('Harald'); + game.start(); + + returnedDice = [1, 2, 3, 2, 2]; + const result = game.cast(); + + game.score(result.scores[1]); + game.score(result.scores[1]); + + expect(game.usedCategories).toEqual([TWOS]); + }); + + it('returns unused categories', () => { + game.player('Horst'); + game.player('Harald'); + game.start(); + + returnedDice = [1, 2, 3, 2, 2]; + const result = game.cast(); + + game.score(result.scores[1]); + game.score(result.scores[1]); + + expect(game.unusedCategories).toEqual([ + ACES, THREES, FOURS, FIVES, SIXES, + THREE_OF_A_KIND, FOUR_OF_A_KIND, FULL_HOUSE, + SMALL_STRAIGHT, LARGE_STRAIGHT, YAHTZEE, CHANCE + ]); + }); + + it('changes to next player', () => { + const player1 = game.player('Horst'); + const player2 = game.player('Harald'); + game.start(); + + expect(game.currentPlayer).toBe(player1); + + returnedDice = [1, 2, 3, 2, 2]; + game.score(game.cast().scores[1]); + + expect(game.currentPlayer).toBe(player2); + }); + }); + + describe('scoring', () => { + it('manages scores for each player', () => { + game.player('Horst'); + game.player('Harald'); + game.start(); + + returnedDice = [1, 2, 3, 2, 2]; + game.score(game.cast().scores[1]); + + expect(game.scores).toBe(0); + + returnedDice = [6, 6, 6, 6, 6]; + game.score(game.cast().scores[0]); + game.score(game.cast().scores[0]); + + expect(game.scores).toBe(30); + }); + }); + }); +}); diff --git a/src/__tests__/HelloWorld.test.js.map b/src/__tests__/HelloWorld.test.js.map deleted file mode 100644 index bc93f9a..0000000 --- a/src/__tests__/HelloWorld.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"HelloWorld.test.js","sourceRoot":"","sources":["HelloWorld.test.ts"],"names":[],"mappings":";;AAAA,4CAAuC;AAEvC,QAAQ,CAAC,YAAY,EAAE;IACnB,EAAE,CAAC,kBAAkB,EAAE;QACnB,IAAM,KAAK,GAAG,IAAI,oBAAU,EAAE,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/src/__tests__/Player.test.ts b/src/__tests__/Player.test.ts new file mode 100644 index 0000000..8a6da08 --- /dev/null +++ b/src/__tests__/Player.test.ts @@ -0,0 +1,13 @@ +import Player from '../Player'; + +describe('Player', () => { + it('has a name', () => { + const player = new Player('Hauke', 123); + expect(player.name).toEqual('Hauke'); + }); + + it('has a id', () => { + const player = new Player('Hauke', 123); + expect(player.id).toEqual(123); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index c5986ed..896b4e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "outDir": "build/dist", "rootDir": "src", "sourceMap": true, - "strictNullChecks": true, + "strict": true, "suppressImplicitAnyIndexErrors": true, "target": "es5" }, diff --git a/yarn.lock b/yarn.lock index 29a7eb6..669fc42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2109,9 +2109,9 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -typescript@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.3.tgz#9639f3c3b40148e8ca97fe08a51dd1891bb6be22" +typescript@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc" uglify-js@^2.6: version "2.8.22"