From 220a9b40e4efb96d62b35d1b9ccbfec42efcdc5c Mon Sep 17 00:00:00 2001 From: Adrian Wannenmacher Date: Mon, 9 Feb 2026 23:16:23 +0100 Subject: [PATCH] implement model for sessions of multiple games --- models/session.js | 138 +++++++++++++++++++++++ models/session.test.js | 242 +++++++++++++++++++++++++++++++++++++++++ test.html | 1 + 3 files changed, 381 insertions(+) create mode 100644 models/session.js create mode 100644 models/session.test.js diff --git a/models/session.js b/models/session.js new file mode 100644 index 0000000..04814ae --- /dev/null +++ b/models/session.js @@ -0,0 +1,138 @@ +"use strict"; + +import Game from "./game.js"; +import { Team } from "./round.js"; + +export default class Session { + /** The amout of points at which individual games are won. + * + * Only applies to new games. + */ + goal = 11; + + /** The name or members of the "we" team. */ + ourTeam = ""; + + /** The name or members of the "they" team. */ + theirTeam = ""; + + /** The finished games. + * @type {Game[]} + */ + #games = []; + + /** Get the finished games. + * + * DO NOT write to the returned object. + */ + get games() { + return this.#games; + } + + /** The currently played game. + * @type {?Game} + */ + #currentGame = null; + + /** Get the currently played game. */ + get currentGame() { + return this.#currentGame; + } + + /** Add another round if there is no current one. */ + anotherGame() { + if (this.#currentGame === null) { + this.#currentGame = new Game(this.goal); + this.#currentGame.addEventListener( + Game.finishedEvent, this.#boundGameFinishedHandler); + } + } + + /** Get the current amouts of points. + * + * Note that on this level points are a punishment. + */ + get result() { + let ourPoints = 0; + let theirPoints = 0; + + for (let g of this.#games) { + let r = g.result; + if (r.winner === Team.We) { + theirPoints += r.points; + } else if (r.winner === Team.They) { + ourPoints += r.points; + } + } + + return { ourPoints, theirPoints }; + } + + /** Handle it when the current game is finished. */ + #gameFinishedHandler() { + this.#currentGame.removeEventListener( + Game.finishedEvent, this.#boundGameFinishedHandler); + this.#games.push(this.#currentGame); + this.#currentGame = null; + } + + #boundGameFinishedHandler = this.#gameFinishedHandler.bind(this); + + constructor(value) { + if (value === undefined) { + this.anotherGame(); + } 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 (!("ourTeam" in value)) + throw new TypeError("missing ourTeam in deserialization object"); + if (typeof value.ourTeam !== "string") + throw new TypeError( + "ourTeam in deserialization object must be string"); + this.ourTeam = value.ourTeam; + + if (!("theirTeam" in value)) + throw new TypeError("missing theirTeam in deserialization object"); + if (typeof value.theirTeam !== "string") + throw new TypeError( + "theirTeam in deserialization object must be string"); + this.theirTeam = value.theirTeam; + + if (!("games" in value)) + throw new TypeError("missing games in deserialization object"); + if (!Array.isArray(value.games)) + throw new TypeError("games in deserialization object must be array"); + for (let g of value.games) { + let game = new Game (g); + if (game.result.winner === null) + throw new TypeError("past game cannot be unfinished"); + this.#games.push(game); + } + + if (!("currentGame" in value)) + throw new TypeError("missing currentGame in deserialization object"); + if (value.currentGame !== null) { + this.#currentGame = new Game(value.currentGame); + if (this.#currentGame.result.winner !== null) + throw new Error("currentGame cannot be finished"); + } + } else { + throw new TypeError("unknown form of Session constructor"); + } + } + + /** Export needed data for JSON serialization. */ + toJSON() { + return { + goal: this.goal, + ourTeam: this.ourTeam, + theirTeam: this.theirTeam, + games: this.#games, + currentGame: this.#currentGame, + } + } +} diff --git a/models/session.test.js b/models/session.test.js new file mode 100644 index 0000000..5d7c45c --- /dev/null +++ b/models/session.test.js @@ -0,0 +1,242 @@ +"use strict"; + +import { Round, Team } from "./round.js"; +import Game from "./game.js"; +import Session from "./session.js"; + +QUnit.module("models", function() { + QUnit.module("session", function() { + QUnit.test("initial state", function(assert) { + let session = new Session(); + assert.strictEqual(session.goal, 11, "initial goal"); + assert.strictEqual(session.games.length, 0, "no finished games"); + assert.notStrictEqual(session.currentGame, null, "game in progress"); + assert.deepEqual( + session.result, + { ourPoints: 0, theirPoints: 0 }, + "initially no points"); + assert.strictEqual(session.ourTeam, "", "our team name"); + assert.strictEqual(session.theirTeam, "", "their team name"); + }); + + QUnit.test("single game finished", function(assert) { + let session = new Session(); + session.currentGame.currentRound.winner = Team.We; + for (let i = 0; i < session.goal; i += 2) + session.currentGame.currentRound.winner = Team.They; + + assert.strictEqual(session.games.length, 1, "single game"); + assert.deepEqual( + session.games[0].result, + { + winner: Team.They, + points: 1, + ourPoints: 2, + theirPoints: 12, + }); + assert.strictEqual(session.currentGame, null, "no game in progress"); + assert.deepEqual( + session.result, + { ourPoints: 1, theirPoints: 0 }, + "one point for losing team"); + }); + + QUnit.test("two games finished", function(assert) { + let session = new Session(); + session.currentGame.currentRound.winner = Team.We; + for (let i = 0; i < session.goal; i += 2) + session.currentGame.currentRound.winner = Team.They; + session.anotherGame(); + for (let i = 0; i < session.goal; i += 2) + session.currentGame.currentRound.winner = Team.We; + + assert.strictEqual(session.games.length, 2, "two games") + assert.deepEqual( + session.games[1].result, + { + winner: Team.We, + points: 2, + ourPoints: 12, + theirPoints: 0, + }); + assert.strictEqual(session.currentGame, null, "no game in progress"); + assert.deepEqual( + session.result, + { ourPoints: 1, theirPoints: 2 }, + "one point for losing team"); + }); + + QUnit.test("new game doesn't overwrite existing", function(assert) { + let session = new Session(); + session.currentGame.currentRound.winner = Team.We; + assert.notStrictEqual(session.currentGame, null, "ongoing game"); + + session.anotherGame(); + assert.deepEqual( + session.currentGame.result, + { + winner: null, + points: 0, + ourPoints: 2, + theirPoints: 0, + }, + "initial game still current"); + }); + + QUnit.test("serialization - new session", function(assert) { + let session = new Session(); + let json = session.toJSON(); + json.currentGame = session.currentGame.toJSON(); + + assert.deepEqual( + json, + { + goal: 11, + ourTeam: "", + theirTeam: "", + games: [], + currentGame: session.currentGame.toJSON() + }, + "correct serialization"); + }); + + QUnit.test("serialization - finished & unfinished game", function(assert) { + let session = new Session(); + session.currentGame.currentRound.winner = Team.We; + for ( + let i = 0; + session.currentGame !== null && i < session.currentGame.goal; + i += 2 + ) + session.currentGame.currentRound.winner = Team.They; + + session.goal = 15; + session.anotherGame(); + session.currentGame.currentRound.winner = Team.They; + for ( + let i = 0; + session.currentGame !== null && i < session.currentGame.goal - 2; + i += 2 + ) + session.currentGame.currentRound.winner = Team.We; + + session.goal = 5; + session.ourTeam = "This is us!"; + session.theirTeam = "This is them!"; + + let json = session.toJSON(); + json.games = []; + for (let i = 0; i < session.games.length; i++) + json.games.push(session.games[i].toJSON()); + json.currentGame = session.currentGame.toJSON(); + + assert.deepEqual( + json, + { + goal: 5, + ourTeam: "This is us!", + theirTeam: "This is them!", + games: [ + session.games[0].toJSON(), + ], + currentGame: session.currentGame.toJSON(), + }, + "correct serialization"); + assert.strictEqual(json.games[0].goal, 11, "first goal"); + assert.strictEqual(json.currentGame.goal, 15, "second goal"); + }); + + QUnit.test("deserialization - new session", function(assert) { + let game = new Game(); + let json = { + goal: 11, + ourTeam: "", + theirTeam: "", + games: [], + currentGame: game.toJSON(), + }; + json.currentGame.currentRound = game.currentRound.toJSON(); + + let session = new Session(json); + assert.strictEqual(session.goal, 11, "goal"); + assert.strictEqual(session.ourTeam, "", "our team name"); + assert.strictEqual(session.theirTeam, "", "their team name"); + assert.strictEqual(session.games.length, 0, "no past games"); + assert.deepEqual(session.currentGame.toJSON(), game.toJSON()); + }); + + QUnit.test("deserialization - un- and finished games", function(assert) { + let finished = new Game(2); + finished.currentRound.winner = Team.We; + + let unfinished = new Game(3); + unfinished.currentRound.winner = Team.They; + + let json = { + goal: 4, + ourTeam: "This is us!", + theirTeam: "This is them!", + games: [finished], + currentGame: unfinished, + }; + let deso = JSON.parse(JSON.stringify(json)); + let session = new Session(deso); + + assert.strictEqual(session.goal, 4, "goal"); + assert.strictEqual(session.ourTeam, "This is us!", "our team name"); + assert.strictEqual(session.theirTeam, "This is them!", "their team"); + assert.strictEqual(session.games.length, 1, "one past game"); + assert.deepEqual( + session.games[0].toJSON(), finished.toJSON(), "finished game"); + assert.notStrictEqual(session.currentGame, null, "unfinished game here"); + assert.deepEqual( + session.currentGame.toJSON(), unfinished.toJSON(), "unfinished game"); + }); + + QUnit.test("deserialization - invalid", function(assert) { + let deso = {}; + assert.throws(function() { new Session(deso); }, "no goal"); + + deso.goal = "11"; + assert.throws(function() { new Session(deso); }, "string goal"); + + deso.goal = 11; + assert.throws(function() { new Session(deso); }, "no ourTeam"); + + deso.ourTeam = 11; + assert.throws(function() { new Session(deso); }, "number ourTeam"); + + deso.ourTeam = ""; + assert.throws(function() { new Session(deso); }, "no theirTeam"); + + deso.theirTeam = 11; + assert.throws(function() { new Session(deso); }, "number theirTeam"); + + deso.theirTeam = ""; + assert.throws(function() { new Session(deso); }, "no games"); + + deso.games = null; + assert.throws(function() { new Session(deso); }, "null games"); + + deso.games = []; + assert.throws(function() { new Session(deso); }, "no currentGame"); + + deso.currentGame = { + goal: 3, + rounds: [{ winner: Team.They, points: 3 }], + currentRound: null, + }; + assert.throws(function() { new Session(deso); }, "finished currentGame"); + + deso.currentGame = null; + new Session(deso); + + deso.games = [{ + goal: 3, + rounds: [{ winner: Team.They, points: 2}], + currentRound: (new Round(3, 2)).toJSON(), + }]; + assert.throws(function() { new Session(deso); }, "unfinished past"); + }); + }); +}); diff --git a/test.html b/test.html index e7811a3..e0ca443 100644 --- a/test.html +++ b/test.html @@ -14,6 +14,7 @@ + \ No newline at end of file