From 1cb03ef70cc5f8b398cc585bf094536149b54327 Mon Sep 17 00:00:00 2001 From: Adrian Wannenmacher Date: Mon, 9 Feb 2026 03:36:18 +0100 Subject: [PATCH] implement model for full games --- models/game.js | 147 +++++++++++++++++++ models/game.test.js | 346 ++++++++++++++++++++++++++++++++++++++++++++ test.html | 1 + 3 files changed, 494 insertions(+) create mode 100644 models/game.js create mode 100644 models/game.test.js diff --git a/models/game.js b/models/game.js new file mode 100644 index 0000000..34be225 --- /dev/null +++ b/models/game.js @@ -0,0 +1,147 @@ +"use strict"; + +import { Round, Team } from "./round.js"; +import RoundResult from "./round_result.js"; + +export default class Game { + /** The finished rounds. + * @type {RoundResult[]} + */ + #rounds = []; + + /** Get the finished rounds. + * + * DO NOT write to the returned object. + */ + get rounds() { + return this.#rounds; + } + + /** How many points a team needs to win. */ + #goal = 11; + + /** Get how many points are needed to win. */ + get goal() { + return this.#goal; + } + + /** The current round. + * @type {?Round} + */ + #currentRound = null; + + /** Get the current round of the game. */ + get currentRound() { + return this.#currentRound; + } + + constructor(value) { + if (value === undefined || typeof value === "number") { + if (typeof value === "number") + this.#goal = value; + + this.#currentRound = new Round(this.#goal, this.#goal); + this.#currentRound.addEventListener( + Round.winEvent, this.#boundRoundFinishedHandler); + } else if (typeof value === "object") { + if (!("goal" in value)) + throw new TypeError("missing goal in deserialization object"); + if (typeof value.goal !== "number") + throw new TypeError("goal in deserialization object must be number"); + this.#goal = value.goal; + + if (!("rounds" in value)) + throw new TypeError("missing rounds in deserialization object"); + if (!Array.isArray(value.rounds)) + throw new TypeError("rounds in deserialization object must be array"); + for (let r of value.rounds) + this.#rounds.push(new RoundResult(r)); + + if (!("currentRound" in value)) + throw new TypeError("missing currentRound in deserialization object"); + if (this.result.winner === null) + this.#currentRound = new Round(value.currentRound); + else if (value.currentRound !== null) + throw new TypeError("currentRound in finished game must be null"); + } else { + throw new TypeError("unknown form of Game constructor"); + } + } + + /** Get the results of the game. */ + get result() { + let ourPoints = 0; + let theirPoints = 0; + let tailor = null; + const tailorGoal = this.#goal - 2; + + for (let r of this.#rounds) { + if (r.winner === Team.We) + ourPoints += r.points; + else if (r.winner === Team.They) + theirPoints += r.points; + + if (tailor === null && ( + (ourPoints >= tailorGoal && theirPoints === 0) + || (theirPoints >= tailorGoal && ourPoints === 0))) + { + tailor = r.winner; + } + } + + let weWon = ourPoints >= this.goal; + let theyWon = theirPoints >= this.goal; + let winner; + + if (!weWon && !theyWon) { + return {winner: null, points: 0, ourPoints, theirPoints}; + } else if (weWon && theyWon) { + throw new Error("game with multiple winners"); + } else if (weWon) { + winner = Team.We; + } else { + winner = Team.They; + } + + let points; + if (tailor !== null && winner !== tailor) { + points = 4; + } else if (tailor !== null && winner === tailor) { + points = 2; + } else { + points = 1; + } + + return {winner, points, ourPoints, theirPoints}; + } + + /** Handle it when the current round is finished. */ + #handleRoundFinished() { + this.#currentRound.removeEventListener( + Round.winEvent, this.#boundRoundFinishedHandler); + this.#rounds.push( + new RoundResult(this.#currentRound.points, this.#currentRound.winner)); + this.#currentRound = null; + + let result = this.result; + + if (result.winner === null) { + this.#currentRound = new Round( + Math.max(this.#goal - result.ourPoints, 2), + Math.max(this.#goal - result.theirPoints, 2)); + this.#currentRound.addEventListener( + Round.winEvent, this.#boundRoundFinishedHandler); + } + } + + #boundRoundFinishedHandler = this.#handleRoundFinished.bind(this); + + /** Export needed data for JSON serialization. */ + toJSON() { + return { + goal: this.#goal, + rounds: this.#rounds, + currentRound: this.#currentRound, + }; + } +} diff --git a/models/game.test.js b/models/game.test.js new file mode 100644 index 0000000..f1a29e6 --- /dev/null +++ b/models/game.test.js @@ -0,0 +1,346 @@ +"use strict"; + +import { Round, Team } from "./round.js"; +import RoundResult from "./round_result.js"; +import Game from "./game.js"; + +QUnit.module("models", function() { + QUnit.module("game", function() { + QUnit.test("default construction", function(assert) { + let game = new Game(); + assert.strictEqual(game.rounds.length, 0, "no past rounds"); + assert.equal(game.goal, 11, "default goal"); + assert.notStrictEqual(game.currentRound, null, "current round there"); + assert.deepEqual( + game.result, + { + winner: null, + points: 0, + ourPoints: 0, + theirPoints: 0 + }, + "initial results", + ); + }); + + QUnit.test("higher goal", function(assert) { + let game = new Game(15); + assert.strictEqual(game.rounds.length, 0, "no past rounds"); + assert.equal(game.goal, 15, "higher goal"); + assert.notStrictEqual(game.currentRound, null, "current round there"); + assert.deepEqual( + game.result, + { + winner: null, + points: 0, + ourPoints: 0, + theirPoints: 0 + }, + "initial results", + ); + }); + + QUnit.test("single round played", function(assert) { + let game = new Game(); + game.currentRound.winner = Team.We; + + assert.equal(game.rounds.length, 1, "one round played"); + assert.deepEqual( + game.rounds[0].toJSON(), + { points: 2, winner: Team.We}, + "first round correct"); + assert.notStrictEqual(game.currentRound, null, "current round there"); + assert.false(game.currentRound.decided, "current round is not decided"); + assert.deepEqual( + game.result, + { + winner: null, + points: 0, + ourPoints: 2, + theirPoints: 0 + }, + "intermediate results", + ); + }); + + QUnit.test("two rounds played", function(assert) { + let game = new Game(); + game.currentRound.winner = Team.We; + game.currentRound.raise(Team.We); + game.currentRound.winner = Team.They; + + assert.equal(game.rounds.length, 2, "two round played"); + assert.deepEqual( + game.rounds[1].toJSON(), + { points: 3, winner: Team.They}, + "second round correct"); + assert.notStrictEqual(game.currentRound, null, "current round there"); + assert.false(game.currentRound.decided, "current round is not decided"); + assert.deepEqual( + game.result, + { + winner: null, + points: 0, + ourPoints: 2, + theirPoints: 3 + }, + "intermediate results", + ); + }); + + QUnit.test("regular victory", function(assert) { + let game = new Game(); + game.currentRound.winner = Team.We; // 2 + game.currentRound.winner = Team.They; // 2 + game.currentRound.winner = Team.We; // 4 + game.currentRound.winner = Team.We; // 6 + game.currentRound.winner = Team.We; // 8 + game.currentRound.winner = Team.We; // 10 + game.currentRound.winner = Team.We; // 12 + + assert.equal(game.rounds.length, 7, "seven rounds played"); + assert.strictEqual(game.currentRound, null, "no further rounds"); + assert.deepEqual( + game.result, + { + winner: Team.We, + points: 1, + ourPoints: 12, + theirPoints: 2, + }, + "final results", + ); + }); + + QUnit.test("tailor victory", function(assert) { + let game = new Game(); + game.currentRound.winner = Team.They; // 2 + game.currentRound.winner = Team.They; // 4 + game.currentRound.winner = Team.They; // 6 + game.currentRound.winner = Team.They; // 8 + game.currentRound.winner = Team.They; // 10 + game.currentRound.winner = Team.They; // 12 + + assert.equal(game.rounds.length, 6, "seven rounds played"); + assert.strictEqual(game.currentRound, null, "no further rounds"); + assert.deepEqual( + game.result, + { + winner: Team.They, + points: 2, + ourPoints: 0, + theirPoints: 12, + }, + "final results", + ); + }); + + QUnit.test("reverse tailor victory", function(assert) { + let game = new Game(); + game.currentRound.winner = Team.We; // 2 + game.currentRound.winner = Team.We; // 4 + game.currentRound.winner = Team.We; // 6 + game.currentRound.winner = Team.We; // 8 + game.currentRound.winner = Team.We; // 10 + game.currentRound.winner = Team.They; // 2 + game.currentRound.winner = Team.They; // 4 + game.currentRound.winner = Team.They; // 6 + game.currentRound.winner = Team.They; // 8 + game.currentRound.winner = Team.They; // 10 + game.currentRound.winner = Team.They; // 12 + + assert.equal(game.rounds.length, 11, "eleven rounds played"); + assert.strictEqual(game.currentRound, null, "no further rounds"); + assert.deepEqual( + game.result, + { + winner: Team.They, + points: 4, + ourPoints: 10, + theirPoints: 12, + }, + "final results", + ); + }); + + QUnit.test("reverse tailor victory with low goal", function(assert) { + let game = new Game(3); + game.currentRound.winner = Team.They; // 2 + game.currentRound.winner = Team.We; // 2 + game.currentRound.winner = Team.We; // 4 + + assert.equal(game.rounds.length, 3, "three rounds played"); + assert.strictEqual(game.currentRound, null, "no further rounds"); + assert.deepEqual( + game.result, + { + winner: Team.We, + points: 4, + ourPoints: 4, + theirPoints: 2, + }, + "final results", + ); + }); + + QUnit.test("serialization - unfinished", function(assert) { + let game = new Game(); + game.currentRound.winner = Team.We; + game.currentRound.raise(Team.They); + game.currentRound.winner = Team.They; + game.currentRound.raise(Team.We); + + let json = game.toJSON(); + json.currentRound = json.currentRound.toJSON(); + for (let i = 0; i < json.rounds.length; i++) + json.rounds[i] = json.rounds[i].toJSON(); + + assert.deepEqual( + json, + { + goal: 11, + rounds: [ + { points: 2, winner: Team.We }, + { points: 3, winner: Team.They }, + ], + currentRound: { + points: 3, + raisedLast: Team.We, + winner: null, + weLimit: 9, + theyLimit: 8, + }, + }, + "serialized data" + ); + }); + + QUnit.test("serialization - finished", function(assert) { + let game = new Game(3); + game.currentRound.winner = Team.We; + game.currentRound.raise(Team.They); + game.currentRound.winner = Team.They; + + let json = game.toJSON(); + for (let i = 0; i < json.rounds.length; i++) + json.rounds[i] = json.rounds[i].toJSON(); + + assert.deepEqual( + json, + { + goal: 3, + rounds: [ + { points: 2, winner: Team.We }, + { points: 3, winner: Team.They }, + ], + currentRound: null, + }, + "serialized data" + ); + }); + + QUnit.test("deserialize - unfinished", function(assert) { + let game = new Game({ + goal: 3, + rounds: [{ winner: Team.We, points: 2 }], + currentRound: { + points: 3, + raisedLast: Team.They, + winner: null, + weLimit: 2, + theyLimit: 3, + }, + }); + + assert.strictEqual(game.goal, 3, "goal"); + assert.strictEqual(game.rounds.length, 1, "one round played"); + assert.deepEqual( + game.rounds[0].toJSON(), + { winner: Team.We, points: 2 }, + "correct past round"); + assert.deepEqual( + game.currentRound.toJSON(), + { + points: 3, + raisedLast: Team.They, + winner: null, + weLimit: 2, + theyLimit: 3, + }, + "correct current round"); + assert.deepEqual( + game.result, + { + winner: null, + points: 0, + ourPoints: 2, + theirPoints: 0, + }, + "intermediate results"); + }); + + QUnit.test("deserialize - finished", function(assert) { + let game = new Game({ + goal: 3, + rounds: [{ winner: Team.They, points: 3 }], + currentRound: null, + }); + + assert.strictEqual(game.goal, 3, "goal"); + assert.strictEqual(game.rounds.length, 1, "one round played"); + assert.deepEqual( + game.rounds[0].toJSON(), + { winner: Team.They, points: 3 }, + "correct past round"); + assert.strictEqual(game.currentRound, null, "no current round"); + assert.deepEqual( + game.result, + { + winner: Team.They, + points: 2, + ourPoints: 0, + theirPoints: 3, + }, + "final results"); + }); + + QUnit.test("deserialize - invalid", function(assert) { + let deso = {}; + assert.throws(function() { new Game(deso); }, "no goal"); + + deso.goal = "5"; + assert.throws(function() { new Game(deso); }, "string goal"); + + deso.goal = 5; + assert.throws(function() { new Game(deso); }, "no rounds"); + + deso.rounds = ["nonono"]; + assert.throws(function() { new Game(deso); }, "string rounds"); + + deso.rounds = []; + assert.throws(function() { new Game(deso); }, "no currentRound"); + + deso.currentRound = null; + assert.throws(function() { new Game(deso); }, "missing currentRound"); + + deso.currentRound = "nonono"; + assert.throws(function() { new Game(deso); }, "broken currentRound"); + + deso.rounds = [{ winner: Team.We, points: 5 }]; + deso.currentRound = { + points: 2, + raisedLast: Team.They, + winner: null, + weLimit: 2, + theyLimit: 5}; + assert.throws(function() { new Game(deso); }, "unneeded currentRound"); + + deso.goal = 11; + new Game(deso); + + deso.goal = 5; + deso.currentRound = null; + new Game(deso); + }); + }); +}); diff --git a/test.html b/test.html index ace4f27..e7811a3 100644 --- a/test.html +++ b/test.html @@ -13,6 +13,7 @@ + \ No newline at end of file