diff --git a/src/game/Game.test.ts b/src/game/Game.test.ts index dcdd322..8a0c7fb 100644 --- a/src/game/Game.test.ts +++ b/src/game/Game.test.ts @@ -1,5 +1,5 @@ import "jest" -import {Game, SelectionMode} from "./Game" +import {EndState, Game, SelectionMode} from "./Game" describe("GameState", () => { const threeByThreeMatrix = [ @@ -8,28 +8,30 @@ describe("GameState", () => { "02", "12", "22", ] + const unrestrictedBuffer = 999 + const unlimitedTime = 999 * 1000 + test("getting size works", () => { - expect((new Game([])).size).toEqual(0) - expect((new Game(["00"])).size).toEqual(1) + expect((new Game(["00"], [["AA"]], unrestrictedBuffer, unlimitedTime)).size).toEqual(1) expect((new Game([ "00", "01", "10", "11", - ])).size).toEqual(2) + ], [], unrestrictedBuffer, unlimitedTime)).size).toEqual(2) }); test("getting cell works", () => { - const game = new Game(threeByThreeMatrix); + const game = new Game(threeByThreeMatrix, [["AA"]], unrestrictedBuffer, unlimitedTime); expect(game.getCell(0, 2)).toEqual("02") }); - + describe("picking", () => { test("starts with free pick", () => { - const game = new Game([]); + const game = new Game([], [["AA"]], 1, unlimitedTime); expect(game.state).toEqual({selectionMode: SelectionMode.FreePick}) }); - + test("picking cells works", () => { - const game = new Game(threeByThreeMatrix); + const game = new Game(threeByThreeMatrix, [["AA"]], unrestrictedBuffer, unlimitedTime); game.pick(0, 0); expect(game.state).toEqual({selectionMode: SelectionMode.RowPick, column: 0}); expect(() => game.pick(0, 2)).toThrow(); @@ -38,11 +40,115 @@ describe("GameState", () => { }); test("picking outside of range fails", () => { - const game = new Game(threeByThreeMatrix); + const game = new Game(threeByThreeMatrix, [], unrestrictedBuffer, unlimitedTime); expect(() => game.pick(-1, 0)).toThrow(); expect(() => game.pick(0, -1)).toThrow(); expect(() => game.pick(3, 0)).toThrow(); expect(() => game.pick(0, 3)).toThrow(); }); + + test("picking fills buffer, fulfills sequence", () => { + const simpleSequence = ["00", "10", "20"] + const game = new Game(threeByThreeMatrix, [simpleSequence], unrestrictedBuffer, unlimitedTime); + expect(game.getSequences()).toEqual([{sequence: simpleSequence, numberOfFulfilled: 0}]) + game.pick(0, 0); + expect(game.buffer).toEqual(["00"]); + expect(game.getSequences()).toEqual([{sequence: simpleSequence, numberOfFulfilled: 1}]) + }); + + test("picking fulfills second sequence occurence", () => { + const sequence = ["AA", "BB", "CC"] + const game = new Game([ + "AA", "AA", "BB", + "BB", "CC", "AA", + "CC", "CC", "CC", + ], [sequence], unrestrictedBuffer, unlimitedTime); + expect(game.getSequences()).toEqual([{sequence: sequence, numberOfFulfilled: 0}]) + game.pick(0, 0); + expect(game.getSequences()).toEqual([{sequence: sequence, numberOfFulfilled: 1}]) + game.pick(2, 0); + expect(game.getSequences()).toEqual([{sequence: sequence, numberOfFulfilled: 2}]) + game.pick(2, 1); + expect(game.getSequences()).toEqual([{sequence: sequence, numberOfFulfilled: 1}]) + game.pick(0, 1); + expect(game.getSequences()).toEqual([{sequence: sequence, numberOfFulfilled: 2}]) + game.pick(0, 2); + expect(game.getSequences()).toEqual([{sequence: sequence, numberOfFulfilled: 3}]) + }); }); + + describe("game end conditions", () => { + test("game won", () => { + const game = new Game([ + "AA", "AA", "BB", + "BB", "CC", "AA", + "CC", "CC", "CC", + ], [["AA", "BB", "CC"]], 3, unlimitedTime) + game.pick(0, 0) + game.pick(2, 0) + game.pick(2, 2) + expect(game.state).toEqual(EndState.Won) + expect(() => game.pick(0, 2)).toThrow(); + }) + + test("game loose", () => { + const game = new Game([ + "AA", "AA", "BB", + "BB", "CC", "AA", + "CC", "CC", "CC", + ], [["AA", "BB", "CC"]], 3, unlimitedTime) + game.pick(0, 0) + game.pick(1, 0) + game.pick(1, 2) + expect(game.state).toEqual(EndState.Lost) + expect(() => game.pick(2, 2)).toThrow(); + }) + }); + + describe("time management", () => { + let currentTimeProgress: number + + beforeEach(() => { + jest.useFakeTimers() + currentTimeProgress = 0 + Date.now = jest.fn(() => { + return currentTimeProgress + }) + }) + + function fakeTimeProgress(ms: number) { + currentTimeProgress += ms + jest.advanceTimersByTime(ms) + } + + test("loosing through timeout works", () => { + const game = new Game([ + "AA", "AA", "BB", + "BB", "CC", "AA", + "CC", "CC", "CC", + ], [["AA"]], 3, 10_000) + fakeTimeProgress(1_000) + expect(game.remainingMilliseconds).toEqual(9_000) + fakeTimeProgress(1_000) + expect(game.remainingMilliseconds).toEqual(8_000) + fakeTimeProgress(8_000) + expect(game.state).toEqual(EndState.Lost) + expect(game.remainingMilliseconds).toEqual(0) + fakeTimeProgress(1_000) + expect(game.remainingMilliseconds).toEqual(0) + }) + + test("clock stops when game is won", () => { + const game = new Game([ + "AA", "AA", "BB", + "BB", "CC", "AA", + "CC", "CC", "CC", + ], [["AA"]], 3, 10_000) + fakeTimeProgress(1_000) + game.pick(0, 0) + expect(game.remainingMilliseconds).toEqual(9_000) + fakeTimeProgress(1_000) + expect(game.remainingMilliseconds).toEqual(9_000) + }) + }) }); diff --git a/src/game/Game.ts b/src/game/Game.ts index 7c62e3b..f6fda32 100644 --- a/src/game/Game.ts +++ b/src/game/Game.ts @@ -71,18 +71,86 @@ class IllegalMoveError extends Error { } } +interface Sequence { + sequence: string[] + numberOfFulfilled: number +} + export class Game { state: State = {selectionMode: SelectionMode.FreePick} public readonly size: number + public readonly buffer: string[] = [] + private readonly timeoutInterval: ReturnType + private readonly startTimeTimeStamp: number + private endTimestamp: number | null = null - constructor(public readonly matrix: string[]) { + constructor( + public readonly matrix: string[], + private readonly sequences: string[][], + public readonly maxBufferLength: number, + public readonly timeout: number, + ) { this.size = Math.sqrt(matrix.length) + this.timeoutInterval = setTimeout(() => { + this.state = EndState.Lost + this.stopClock() + }, + this.timeout) + this.startTimeTimeStamp = Date.now() } - getCell(row: number, column: number) { + get remainingMilliseconds(): number { + if (this.endTimestamp) { + return this.timeout - (this.endTimestamp - this.startTimeTimeStamp) + } + return this.timeout - (Date.now() - this.startTimeTimeStamp) + } + + getCell(row: number, column: number): string { return this.matrix[row + column * this.size] } + getSequences(): Sequence[] { + return this.sequences.map(sequence => { + let longestPrefixLength = 0 + for (let i = 0; i < this.buffer.length; ++i) { + let prefixLength = 0; + for (let j = 0; j < Math.min(sequence.length, this.buffer.length - i); ++j) { + if (this.buffer[i + j] != sequence[j]) { + // abort sequence + prefixLength = 0 + break; + } + ++prefixLength; + } + longestPrefixLength = Math.max(longestPrefixLength, prefixLength); + } + + return { + sequence: sequence, + numberOfFulfilled: longestPrefixLength, + } + }) + } + + private stopClock(): void { + clearTimeout(this.timeoutInterval) + this.endTimestamp = Date.now() + } + + private checkEndGame(): void { + const isSequenceFulfilled = (sequence: Sequence) => sequence.sequence.length === sequence.numberOfFulfilled + if (this.getSequences().every(isSequenceFulfilled)) { + this.stopClock() + this.state = EndState.Won + } else { + this.stopClock() + if (this.buffer.length >= this.maxBufferLength) { + this.state = EndState.Lost + } + } + } + pick(row: number, column: number): void { if (row < 0 || column < 0 || row >= this.size || column >= this.size) { throw new IllegalMoveError() @@ -91,7 +159,9 @@ export class Game { matchState({ Won: () => {throw new IllegalMoveError()}, Lost: () => {throw new IllegalMoveError()}, - InProgress: (selectionMode) => + InProgress: (selectionMode) => { + this.buffer.push(this.getCell(row, column)) + matchSelectionState({ Free: () => { this.state = { @@ -119,7 +189,10 @@ export class Game { throw new IllegalMoveError() } }, - })(selectionMode), + })(selectionMode) + } })(this.state) + + this.checkEndGame(); } }