From e1f153feb31be4d2a716734cd17e799396afad27 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Tue, 13 Jun 2017 21:53:12 +0200 Subject: [PATCH 01/22] Add Player value object --- src/Player.ts | 3 +++ src/__tests__/Player.test.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/Player.ts create mode 100644 src/__tests__/Player.test.ts 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__/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); + }); +}); From b5ca8217d8247e32e1dea162d95c3c91a2e31c8c Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Thu, 15 Jun 2017 21:48:54 +0200 Subject: [PATCH 02/22] Use typescript 2.4 RC --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index aba0b6c..93ace63 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.0-rc" } } diff --git a/yarn.lock b/yarn.lock index 29a7eb6..7859c12 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.0-rc: + version "2.4.0" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.0.tgz#aef5a8d404beba36ad339abf079ddddfffba86dd" uglify-js@^2.6: version "2.8.22" From 522f7f500454badc44ea2354ffaf6ffea310ccad Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Thu, 15 Jun 2017 21:49:57 +0200 Subject: [PATCH 03/22] Start implementing Game class --- src/Game.ts | 64 ++++++++++++++++++++++++++++++++++++++ src/__tests__/Game.test.ts | 64 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/Game.ts create mode 100644 src/__tests__/Game.test.ts diff --git a/src/Game.ts b/src/Game.ts new file mode 100644 index 0000000..aff489c --- /dev/null +++ b/src/Game.ts @@ -0,0 +1,64 @@ +import Player from './Player'; + +class Game { + private _players: Player[]; + private _running = false; + private _currentPlayer: Player; + + public constructor() { + this._players = []; + } + + public player(name: string) { + if (this.running) { + throw new Game.GameAlreadyRunningError; + } + const player = new Player(name, 123); + this._players.push(player); + return player; + } + + public start() { + if (this.running) { + throw new Game.GameAlreadyRunningError; + } + if (this.players.length === 0) { + throw new Game.NoPlayersAdded(); + } + + this._currentPlayer = this.players.shift() as Player; + this._running = true; + } + + public get players(): Player[] { + return [...this._players]; + } + + public get running(): boolean { + return this._running; + } + + public get currentPlayer(): Player { + return this._currentPlayer; + } +} + +namespace Game { + + export class GameAlreadyRunningError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, GameAlreadyRunningError.prototype); + } + } + + export class NoPlayersAdded extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, NoPlayersAdded.prototype); + } + } + +} + +export default Game; diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts new file mode 100644 index 0000000..f562f2c --- /dev/null +++ b/src/__tests__/Game.test.ts @@ -0,0 +1,64 @@ +import Game from '../Game'; + +describe('Game', () => { + let game: Game; + + beforeEach(() => { + game = new Game(); + }); + + 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('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.start(); + expect(game.running).toBe(true); + }); + }); + }); + + describe('game is running', () => { + it('forbids adding new players', () => { + game.player('Hauke'); + game.start(); + expect(() => game.player('Karl')).toThrowError(Game.GameAlreadyRunningError); + }); + + it('forbids starting game', () => { + game.player('Hauke'); + 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); + }); + }); +}); From 53c7f7e737e83aa1664cee210435c0abd3f7d880 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sat, 17 Jun 2017 15:01:46 +0200 Subject: [PATCH 04/22] Move dice to method --- src/DiceCup.ts | 10 ++-------- src/__tests__/DiceCup.test.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 16 deletions(-) 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/__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); }); }); From 6ef2bf12f70cd2ac20676be16fc8ef3275f6d7a9 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sat, 17 Jun 2017 15:03:11 +0200 Subject: [PATCH 05/22] Remove artefact --- src/__tests__/HelloWorld.test.js.map | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/__tests__/HelloWorld.test.js.map 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 From 9058085e96a7c89dc007c629c31dcb53f0f63e66 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sat, 17 Jun 2017 15:25:24 +0200 Subject: [PATCH 06/22] Player casts dice --- src/Game.ts | 30 ++++++++++++++++++++++++++++++ src/__tests__/Game.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Game.ts b/src/Game.ts index aff489c..0ec4aa7 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -1,4 +1,6 @@ import Player from './Player'; +import Score from './Score'; +import { ACES, TWOS, THREES } from './categories'; class Game { private _players: Player[]; @@ -28,6 +30,21 @@ class Game { this._currentPlayer = this.players.shift() as Player; this._running = true; + + } + + public cast(): Game.Result { + if (!this.running) { + throw new Game.GameNotRunningError; + } + return { + dice: [1, 2, 3], + scores: [ + new Score(ACES, 1), + new Score(TWOS, 2), + new Score(THREES, 3), + ] + }; } public get players(): Player[] { @@ -45,11 +62,24 @@ class Game { namespace Game { + export type Result = { + dice: number[], + scores: Score[] + }; + 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 NoPlayersAdded extends Error { diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index f562f2c..78ee683 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -1,4 +1,6 @@ import Game from '../Game'; +import Score from '../Score'; +import { ACES, TWOS, THREES } from '../categories'; describe('Game', () => { let game: Game; @@ -60,5 +62,25 @@ describe('Game', () => { 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('it returns possible scores', () => { + game.player('Karsten'); + game.start(); + expect(game.cast()).toEqual({ + dice: [1, 2, 3], + scores: [ + new Score(ACES, 1), + new Score(TWOS, 2), + new Score(THREES, 3), + ] + }); + }); + }); }); }); From 641d95604328afdef86792143ff8037c98b8c24d Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sat, 17 Jun 2017 16:24:39 +0200 Subject: [PATCH 07/22] Use ScoreAnalyzer and DiceCup in Game --- src/Game.ts | 17 +++++++++-------- src/__tests__/Game.test.ts | 39 +++++++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/Game.ts b/src/Game.ts index 0ec4aa7..ff844f1 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -1,13 +1,14 @@ +import DiceCup from './DiceCup'; import Player from './Player'; import Score from './Score'; -import { ACES, TWOS, THREES } from './categories'; +import ScoreAnalyzer from './ScoreAnalyzer'; class Game { private _players: Player[]; private _running = false; private _currentPlayer: Player; - public constructor() { + public constructor(private diceCup: DiceCup, private scoreAnalyzer: ScoreAnalyzer) { this._players = []; } @@ -37,13 +38,13 @@ class Game { if (!this.running) { throw new Game.GameNotRunningError; } + + const dice = this.diceCup.cast(5); + const scores = this.scoreAnalyzer.scores(dice); + return { - dice: [1, 2, 3], - scores: [ - new Score(ACES, 1), - new Score(TWOS, 2), - new Score(THREES, 3), - ] + dice: dice, + scores: scores, }; } diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index 78ee683..168b789 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -1,12 +1,37 @@ +import DiceCup from '../DiceCup'; import Game from '../Game'; import Score from '../Score'; -import { ACES, TWOS, THREES } from '../categories'; +import ScoreAnalyzer from '../ScoreAnalyzer'; + +import { + aces, twos, threes, fours, fives, sixes, + threeOfAKind, fourOfAKind, fullHouse, + smallStraight, largeStraight, chance, yahtzee +} from '../categories'; + +import { + ACES, TWOS, THREES, FOURS, FIVES, + SMALL_STRAIGHT, LARGE_STRAIGHT, CHANCE +} from '../categories'; describe('Game', () => { let game: Game; + let returnedDice: number[]; beforeEach(() => { - game = new Game(); + const DiceCupMock = jest.fn(() => ({ + cast: (numberOfDice: number) => { + return returnedDice; + } + })); + + const scoreanalyzer = new ScoreAnalyzer([ + aces, twos, threes, fours, fives, sixes, + threeOfAKind, fourOfAKind, fullHouse, + smallStraight, largeStraight, chance, yahtzee + ]); + + game = new Game(new DiceCupMock(), scoreanalyzer); }); describe('game management', () => { @@ -72,12 +97,20 @@ describe('Game', () => { it('it returns possible scores', () => { game.player('Karsten'); game.start(); + + returnedDice = [1, 2, 3, 4, 5]; + expect(game.cast()).toEqual({ - dice: [1, 2, 3], + 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), ] }); }); From b6d72d19aeab3cc3b2439f2139b9024e527d4866 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sat, 17 Jun 2017 18:42:11 +0200 Subject: [PATCH 08/22] Player casts dice again --- src/Game.ts | 13 ++++++++---- src/__tests__/Game.test.ts | 43 +++++++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/Game.ts b/src/Game.ts index ff844f1..22d68e7 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -34,16 +34,21 @@ class Game { } - public cast(): Game.Result { + public cast(dice: number[] = []): Game.Result { if (!this.running) { throw new Game.GameNotRunningError; } - const dice = this.diceCup.cast(5); - const scores = this.scoreAnalyzer.scores(dice); + let numberOfDice = dice.length; + if (numberOfDice === 0) { + numberOfDice = 5; + } + + const diceCast = this.diceCup.cast(numberOfDice); + const scores = this.scoreAnalyzer.scores(diceCast); return { - dice: dice, + dice: diceCast, scores: scores, }; } diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index 168b789..7a51282 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -10,28 +10,29 @@ import { } from '../categories'; import { - ACES, TWOS, THREES, FOURS, FIVES, + ACES, TWOS, THREES, FOURS, FIVES, THREE_OF_A_KIND, SMALL_STRAIGHT, LARGE_STRAIGHT, CHANCE } from '../categories'; describe('Game', () => { + let diceCup: DiceCup; let game: Game; let returnedDice: number[]; beforeEach(() => { const DiceCupMock = jest.fn(() => ({ - cast: (numberOfDice: number) => { - return returnedDice; - } + 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(new DiceCupMock(), scoreanalyzer); + game = new Game(diceCup, scoreanalyzer); }); describe('game management', () => { @@ -94,6 +95,14 @@ describe('Game', () => { expect(() => game.cast()).toThrowError(Game.GameNotRunningError); }); + it('casts the dice with 5 by default', () => { + game.player('Karsten'); + game.start(); + returnedDice = [1, 2, 3, 4, 5]; + game.cast(); + expect(diceCup.cast).toHaveBeenCalledWith(5); + }); + it('it returns possible scores', () => { game.player('Karsten'); game.start(); @@ -115,5 +124,29 @@ describe('Game', () => { }); }); }); + + describe('player casts selected dice again', () => { + it('it returns possible scores', () => { + 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); + }); + }); }); }); From d84f8b7b776a515b03179591b7316bb9a19ed124 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sat, 17 Jun 2017 21:38:25 +0200 Subject: [PATCH 09/22] Allow only 3 dice casts --- src/Game.ts | 16 +++++++++++++++- src/__tests__/Game.test.ts | 13 ++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Game.ts b/src/Game.ts index 22d68e7..393b812 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -7,6 +7,7 @@ class Game { private _players: Player[]; private _running = false; private _currentPlayer: Player; + private numberOfCasts: number; public constructor(private diceCup: DiceCup, private scoreAnalyzer: ScoreAnalyzer) { this._players = []; @@ -31,7 +32,7 @@ class Game { this._currentPlayer = this.players.shift() as Player; this._running = true; - + this.numberOfCasts = 0; } public cast(dice: number[] = []): Game.Result { @@ -39,6 +40,10 @@ class Game { throw new Game.GameNotRunningError; } + if (this.numberOfCasts === 3) { + throw new Game.DiceCastingExceededError(); + } + let numberOfDice = dice.length; if (numberOfDice === 0) { numberOfDice = 5; @@ -47,6 +52,8 @@ class Game { const diceCast = this.diceCup.cast(numberOfDice); const scores = this.scoreAnalyzer.scores(diceCast); + this.numberOfCasts++; + return { dice: diceCast, scores: scores, @@ -88,6 +95,13 @@ namespace Game { } } + export class DiceCastingExceededError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, DiceCastingExceededError.prototype); + } + } + export class NoPlayersAdded extends Error { constructor() { super(); diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index 7a51282..20ef5dc 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -126,7 +126,7 @@ describe('Game', () => { }); describe('player casts selected dice again', () => { - it('it returns possible scores', () => { + it('returns possible scores', () => { game.player('Karsten'); game.start(); @@ -148,5 +148,16 @@ describe('Game', () => { expect(diceCup.cast).toHaveBeenLastCalledWith(2); }); }); + + describe('player casts dice more than three times', () => { + it('throws error', () => { + game.player('Karsten'); + game.start(); + game.cast(); + game.cast(); + game.cast(); + expect(() => game.cast()).toThrowError(Game.DiceCastingExceededError); + }); + }); }); }); From 5f51007427da2681dc5767dc174e2f573a401efd Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sun, 18 Jun 2017 11:15:11 +0200 Subject: [PATCH 10/22] Needs at least to players --- src/Game.ts | 10 ++++++++++ src/__tests__/Game.test.ts | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Game.ts b/src/Game.ts index 393b812..949fac8 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -29,6 +29,9 @@ class Game { if (this.players.length === 0) { throw new Game.NoPlayersAdded(); } + if (this.players.length < 2) { + throw new Game.MinimumPlayerRequirementsError(); + } this._currentPlayer = this.players.shift() as Player; this._running = true; @@ -109,6 +112,13 @@ namespace Game { } } + export class MinimumPlayerRequirementsError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, MinimumPlayerRequirementsError.prototype); + } + } + } export default Game; diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index 20ef5dc..02f0630 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -60,21 +60,31 @@ describe('Game', () => { 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); }); @@ -97,6 +107,7 @@ describe('Game', () => { it('casts the dice with 5 by default', () => { game.player('Karsten'); + game.player('Sven'); game.start(); returnedDice = [1, 2, 3, 4, 5]; game.cast(); @@ -105,6 +116,7 @@ describe('Game', () => { it('it returns possible scores', () => { game.player('Karsten'); + game.player('Ole'); game.start(); returnedDice = [1, 2, 3, 4, 5]; @@ -127,6 +139,7 @@ describe('Game', () => { describe('player casts selected dice again', () => { it('returns possible scores', () => { + game.player('Lydia'); game.player('Karsten'); game.start(); @@ -151,6 +164,7 @@ describe('Game', () => { describe('player casts dice more than three times', () => { it('throws error', () => { + game.player('Horst'); game.player('Karsten'); game.start(); game.cast(); From 6fdd7d80cc63115af9a2f21c4d41c0eaa15a9080 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sun, 18 Jun 2017 11:28:23 +0200 Subject: [PATCH 11/22] Add score to scorecard --- src/Game.ts | 11 +++++++++++ src/__tests__/Game.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/Game.ts b/src/Game.ts index 949fac8..f5e3496 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -2,15 +2,18 @@ import DiceCup from './DiceCup'; import Player from './Player'; import Score from './Score'; import ScoreAnalyzer from './ScoreAnalyzer'; +import Scorecard from './Scorecard'; class Game { private _players: Player[]; private _running = false; private _currentPlayer: Player; private numberOfCasts: number; + private scorecard: Scorecard; public constructor(private diceCup: DiceCup, private scoreAnalyzer: ScoreAnalyzer) { this._players = []; + this.scorecard = new Scorecard(); } public player(name: string) { @@ -63,6 +66,10 @@ class Game { }; } + public score(score: Score) { + this.scorecard.add(score); + } + public get players(): Player[] { return [...this._players]; } @@ -74,6 +81,10 @@ class Game { public get currentPlayer(): Player { return this._currentPlayer; } + + public get scores(): number { + return this.scorecard.score; + } } namespace Game { diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index 02f0630..5acbdb8 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -173,5 +173,30 @@ describe('Game', () => { expect(() => game.cast()).toThrowError(Game.DiceCastingExceededError); }); }); + + describe('player selects score', () => { + it('add 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]); + expect(game.scores).toBe(6); + }); + }); }); }); From 2ed0277e71ab34068d1482002447ea6258a7b036 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sun, 18 Jun 2017 11:34:18 +0200 Subject: [PATCH 12/22] Use same category twice forbidden --- src/__tests__/Game.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index 5acbdb8..d806b73 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -2,6 +2,7 @@ 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, @@ -197,6 +198,19 @@ describe('Game', () => { game.score(result.scores[1]); expect(game.scores).toBe(6); }); + + it('forbids to add score 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]); + + expect(() => game.score(result.scores[1])).toThrow(Scorecard.CategoryAlreadyUsedError); + }); }); }); }); From a55726e583204a08edaaa9bce1d5f9f63c047591 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sun, 18 Jun 2017 12:14:39 +0200 Subject: [PATCH 13/22] Scoring changes to next player --- src/Game.ts | 31 ++++++++++++++++++++++--------- src/__tests__/Game.test.ts | 13 +++++++++++++ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/Game.ts b/src/Game.ts index f5e3496..d1f7aef 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -1,18 +1,19 @@ import DiceCup from './DiceCup'; -import Player from './Player'; import Score from './Score'; import ScoreAnalyzer from './ScoreAnalyzer'; import Scorecard from './Scorecard'; class Game { - private _players: Player[]; + private _players: Game.Players; private _running = false; - private _currentPlayer: Player; + private _currentPlayer: Game.Player; private numberOfCasts: number; private scorecard: Scorecard; + private playerId: number; public constructor(private diceCup: DiceCup, private scoreAnalyzer: ScoreAnalyzer) { this._players = []; + this.playerId = 1; this.scorecard = new Scorecard(); } @@ -20,7 +21,8 @@ class Game { if (this.running) { throw new Game.GameAlreadyRunningError; } - const player = new Player(name, 123); + const player = { id: this.playerId, name }; + this.playerId++; this._players.push(player); return player; } @@ -36,7 +38,7 @@ class Game { throw new Game.MinimumPlayerRequirementsError(); } - this._currentPlayer = this.players.shift() as Player; + this._currentPlayer = this.players.shift() as Game.Player; this._running = true; this.numberOfCasts = 0; } @@ -68,9 +70,13 @@ class Game { public score(score: Score) { this.scorecard.add(score); + const nextPlayer = this._players.filter((player) => { + return player.id === (this.currentPlayer.id + 1); + }); + this._currentPlayer = nextPlayer.pop() as Game.Player; } - public get players(): Player[] { + public get players(): Game.Player[] { return [...this._players]; } @@ -78,7 +84,7 @@ class Game { return this._running; } - public get currentPlayer(): Player { + public get currentPlayer(): Game.Player { return this._currentPlayer; } @@ -90,10 +96,17 @@ class Game { namespace Game { export type Result = { - dice: number[], - scores: Score[] + dice: number[]; + scores: Score[]; }; + export type Player = { + id: number; + name: string; + }; + + export type Players = Player[]; + export class GameAlreadyRunningError extends Error { constructor() { super(); diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index d806b73..dbfaaf0 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -211,6 +211,19 @@ describe('Game', () => { expect(() => game.score(result.scores[1])).toThrow(Scorecard.CategoryAlreadyUsedError); }); + + 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); + }); }); }); }); From 146a6306b3a2cde041a29b4c012c158b43246e41 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sun, 18 Jun 2017 12:27:25 +0200 Subject: [PATCH 14/22] Forbids adding same player twice --- src/Game.ts | 24 ++++++++++++++++++++---- src/__tests__/Game.test.ts | 5 +++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Game.ts b/src/Game.ts index d1f7aef..c00f996 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -21,10 +21,7 @@ class Game { if (this.running) { throw new Game.GameAlreadyRunningError; } - const player = { id: this.playerId, name }; - this.playerId++; - this._players.push(player); - return player; + return this.newPlayer(name); } public start() { @@ -76,6 +73,18 @@ class Game { 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); + return player; + } + public get players(): Game.Player[] { return [...this._players]; } @@ -136,6 +145,13 @@ namespace Game { } } + export class PlayerNameAlreadyExistsError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, PlayerNameAlreadyExistsError.prototype); + } + } + export class MinimumPlayerRequirementsError extends Error { constructor() { super(); diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index dbfaaf0..27fcd73 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -44,6 +44,11 @@ describe('Game', () => { 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'); From 37ee150dba8bd307d20b7e3c8e5ba8e89cf7610e Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sun, 18 Jun 2017 12:27:58 +0200 Subject: [PATCH 15/22] Enable all strict checking options --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" }, From c454fb4359bc283c310624fc3f7097d3b73641ed Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Sun, 18 Jun 2017 13:42:39 +0200 Subject: [PATCH 16/22] Forbids recast of nonexistent dice --- src/Game.ts | 33 ++++++++++++++++++++++++++++++++- src/__tests__/Game.test.ts | 13 +++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Game.ts b/src/Game.ts index c00f996..32cedb3 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -10,11 +10,13 @@ class Game { 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 = new Scorecard(); + this.lastDiceCast = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; } public player(name: string) { @@ -54,9 +56,27 @@ class Game { 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); - const scores = this.scoreAnalyzer.scores(diceCast); + diceCast.forEach((pip) => { + this.lastDiceCast[pip]++; + }); + const scores = this.scoreAnalyzer.scores(diceCast); this.numberOfCasts++; return { @@ -116,6 +136,10 @@ namespace Game { export type Players = Player[]; + export type DiceCast = { + [n: number]: number; + }; + export class GameAlreadyRunningError extends Error { constructor() { super(); @@ -159,6 +183,13 @@ namespace Game { } } + export class NonAvailableDiceError extends Error { + constructor() { + super(); + Object.setPrototypeOf(this, NonAvailableDiceError.prototype); + } + } + } export default Game; diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index 27fcd73..49ef6de 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -166,6 +166,19 @@ describe('Game', () => { }); 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', () => { From e7a61603371800ffb6b2b39c3b042b47e607d4bf Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Wed, 21 Jun 2017 21:19:55 +0200 Subject: [PATCH 17/22] Fix missing return type --- src/Game.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Game.ts b/src/Game.ts index 32cedb3..70262b1 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -19,7 +19,7 @@ class Game { this.lastDiceCast = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; } - public player(name: string) { + public player(name: string): Game.Player { if (this.running) { throw new Game.GameAlreadyRunningError; } From 3c220021fea4f3341c0cabf8e6ac05fb7cad59c1 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Thu, 13 Jul 2017 12:06:28 +0200 Subject: [PATCH 18/22] Update Typescript to v2.4.1 --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 93ace63..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.4.0-rc" + "typescript": "2.4.1" } } diff --git a/yarn.lock b/yarn.lock index 7859c12..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.4.0-rc: - version "2.4.0" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.0.tgz#aef5a8d404beba36ad339abf079ddddfffba86dd" +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" From 41034d2b2bea195a40b270666239d9025b540614 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Thu, 13 Jul 2017 12:28:30 +0200 Subject: [PATCH 19/22] Fixed test description --- src/__tests__/Game.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index 49ef6de..b404bec 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -217,7 +217,7 @@ describe('Game', () => { expect(game.scores).toBe(6); }); - it('forbids to add score used category', () => { + it('forbids to add score to used category', () => { game.player('Horst'); game.player('Harald'); game.start(); From 08ddbcc1c94358a8b692988c1caf9500ba97214c Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Thu, 13 Jul 2017 13:38:38 +0200 Subject: [PATCH 20/22] Game returns categories --- src/Game.ts | 26 ++++++++++++++++++++++++++ src/__tests__/Game.test.ts | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Game.ts b/src/Game.ts index 70262b1..ea44519 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -3,6 +3,14 @@ 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; @@ -120,6 +128,24 @@ class Game { public get scores(): number { return this.scorecard.score; } + + public get usedCategories(): Category[] { + return this.scorecard.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 { diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index b404bec..de4222f 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -11,8 +11,9 @@ import { } from '../categories'; import { - ACES, TWOS, THREES, FOURS, FIVES, THREE_OF_A_KIND, - SMALL_STRAIGHT, LARGE_STRAIGHT, CHANCE + 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', () => { @@ -230,6 +231,36 @@ describe('Game', () => { 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]); + + 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]); + + 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'); From 8eec2bb5e386dbfbdb67b65b5918ab713f8b7b4d Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Thu, 13 Jul 2017 13:42:54 +0200 Subject: [PATCH 21/22] Fix test description --- src/__tests__/Game.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index de4222f..fe4d87a 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -195,7 +195,7 @@ describe('Game', () => { }); describe('player selects score', () => { - it('add score to scorecard', () => { + it('adds score to scorecard', () => { game.player('Horst'); game.player('Harald'); game.start(); From 9bc2497d875a36695ac90f3cd1f3bc0fcb21adb6 Mon Sep 17 00:00:00 2001 From: Hauke Stange Date: Thu, 13 Jul 2017 15:44:02 +0200 Subject: [PATCH 22/22] Manage scorecards for players --- src/Game.ts | 20 ++++++++++++++------ src/__tests__/Game.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Game.ts b/src/Game.ts index ea44519..e434a74 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -16,14 +16,14 @@ class Game { private _running = false; private _currentPlayer: Game.Player; private numberOfCasts: number; - private scorecard: Scorecard; + 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 = new Scorecard(); + this.scorecard = []; this.lastDiceCast = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }; } @@ -94,10 +94,17 @@ class Game { } public score(score: Score) { - this.scorecard.add(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 === (this.currentPlayer.id + 1); + return player.id === nextPlayerId; }); + this._currentPlayer = nextPlayer.pop() as Game.Player; } @@ -110,6 +117,7 @@ class Game { const player = { id: this.playerId, name }; this.playerId++; this._players.push(player); + this.scorecard[player.id] = new Scorecard(); return player; } @@ -126,11 +134,11 @@ class Game { } public get scores(): number { - return this.scorecard.score; + return this.scorecard[this.currentPlayer.id].score; } public get usedCategories(): Category[] { - return this.scorecard.categories.map((score) => score.category); + return this.scorecard[this.currentPlayer.id].categories.map((score) => score.category); } public get unusedCategories(): Category[] { diff --git a/src/__tests__/Game.test.ts b/src/__tests__/Game.test.ts index fe4d87a..9c63fda 100644 --- a/src/__tests__/Game.test.ts +++ b/src/__tests__/Game.test.ts @@ -215,6 +215,8 @@ describe('Game', () => { // } game.score(result.scores[1]); + game.score(result.scores[1]); + expect(game.scores).toBe(6); }); @@ -226,6 +228,7 @@ describe('Game', () => { 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); @@ -239,6 +242,7 @@ describe('Game', () => { 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]); @@ -252,6 +256,7 @@ describe('Game', () => { returnedDice = [1, 2, 3, 2, 2]; const result = game.cast(); + game.score(result.scores[1]); game.score(result.scores[1]); expect(game.unusedCategories).toEqual([ @@ -274,5 +279,24 @@ describe('Game', () => { 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); + }); + }); }); });