diff --git a/models/game.js b/models/game.js index 7708958..14a4359 100644 --- a/models/game.js +++ b/models/game.js @@ -1,5 +1,6 @@ "use strict"; +import GameRules, { RaisingRule } from "/models/game_rules.js"; import { Round, Team } from "/models/round.js"; import RoundResult from "/models/round_result.js"; @@ -31,12 +32,17 @@ export default class Game extends EventTarget { return this.#rounds; } - /** How many points a team needs to win. */ - #goal = 11; + /** The rules of this game. */ + #rules = new GameRules(); - /** Get how many points are needed to win. */ - get goal() { - return this.#goal; + /** Get the rules of this game. + * + * Note that this actually returns a copy of the game rules. They cannot be + * changed, as changing the rules during a game would a) be unfair and b) + * rather difficult to correctly implement. + */ + get rules() { + return new GameRules(this.#rules); } /** The current round. @@ -51,14 +57,12 @@ export default class Game extends EventTarget { constructor(value) { super(); - if (value === undefined || typeof value === "number") { - if (typeof value === "number") - this.#goal = value; + if (value === undefined || value instanceof GameRules) { + if (value instanceof GameRules) + this.#rules = value; - if (this.#goal < 1) - throw new RangeError("goal must be at least 1"); - - this.#currentRound = new Round(this.#goal, this.#goal); + this.#currentRound = new Round( + this.#rules.raisingLimit(0), this.#rules.raisingLimit(0)); this.#currentRound.addEventListener( Round.EVENT_CHANGE, this.#boundHandleRoundChange); } else if (typeof value === "object") { @@ -78,7 +82,7 @@ export default class Game extends EventTarget { let ourPoints = 0; let theirPoints = 0; let tailor = null; - const tailorGoal = this.#goal - 2; + const tailorGoal = this.#rules.goal - 2; for (let r of this.#rounds) { if (r.winner === Team.We) @@ -94,8 +98,8 @@ export default class Game extends EventTarget { } } - let weWon = ourPoints >= this.goal; - let theyWon = theirPoints >= this.goal; + let weWon = ourPoints >= this.#rules.goal; + let theyWon = theirPoints >= this.#rules.goal; let winner; if (!weWon && !theyWon) { @@ -137,8 +141,8 @@ export default class Game extends EventTarget { if (result.winner === null) { this.#currentRound = new Round( - Math.max(this.#goal - result.ourPoints, 2), - Math.max(this.#goal - result.theirPoints, 2)); + this.#rules.raisingLimit(result.ourPoints), + this.#rules.raisingLimit(result.theirPoints)); this.#currentRound.addEventListener( Round.EVENT_CHANGE, this.#boundHandleRoundChange); } @@ -165,7 +169,7 @@ export default class Game extends EventTarget { */ toStruct() { return { - goal: this.#goal, + rules: this.#rules.toStruct(), rounds: this.#rounds.map((r) => r.toStruct()), currentRound: this.#currentRound !== null ? this.#currentRound.toStruct() : null, @@ -177,11 +181,21 @@ export default class Game extends EventTarget { if (typeof value !== "object") throw new TypeError("struct must be an object"); - if (typeof value.goal !== "number") - throw new TypeError("struct must contain goal as number"); - if (!Number.isInteger(value.goal) || value.goal < 1) - throw new RangeError("struct must contain goal >= 1 as integer"); - this.#goal = value.goal; + if ("goal" in value && "rules" in value) + throw new TypeError("struct cannot contain both rules and goal"); + else if ("goal" in value) { + if (typeof value.goal !== "number") + throw new TypeError("if struct contains goal, it must be a number"); + if (!Number.isInteger(value.goal) || value.goal < 1) + throw new RangeError("if struct contains goal, must be integer >= 1"); + this.#rules.goal = value.goal; + this.#rules.raising = RaisingRule.UntilEnough; + } else if ("rules" in value) { + if (typeof value.rules !== "object") + throw new TypeError("if struct contains rules, they must be an object"); + this.#rules = new GameRules(value.rules); + } else + throw new TypeError("struct must contain either rules or goal"); if (!("rounds" in value)) throw new TypeError("struct must contain rounds"); diff --git a/models/game.test.js b/models/game.test.js index 0436175..f85a4a3 100644 --- a/models/game.test.js +++ b/models/game.test.js @@ -3,13 +3,16 @@ import { Round, Team } from "/models/round.js"; import RoundResult from "/models/round_result.js"; import Game from "/models/game.js"; +import GameRules, { RaisingRule } from "./game_rules.js"; export default 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.strictEqual(game.rules.goal, 11, "default goal"); + assert.strictEqual( + game.rules.raising, RaisingRule.UnlessStricken, "default raising rule"); assert.notStrictEqual(game.currentRound, null, "current round there"); assert.deepEqual( game.result, @@ -29,17 +32,14 @@ export default function() { new TypeError("unknown form of Game constructor")); }); - QUnit.test("low goal", function(assert) { - assert.throws( - function() { new Game(0); }, - new RangeError("goal must be at least 1"), - "goal must be 1 or higher"); - }); - - QUnit.test("higher goal", function(assert) { - let game = new Game(15); + QUnit.test("with non-default rules", function(assert) { + let rules = new GameRules(); + rules.goal = 15; + rules.raising = RaisingRule.UntilEnough; + let game = new Game(rules); assert.strictEqual(game.rounds.length, 0, "no past rounds"); - assert.equal(game.goal, 15, "higher goal"); + assert.equal(game.rules.goal, 15, "higher goal"); + assert.equal(game.rules.raising, RaisingRule.UntilEnough, "raising rule"); assert.notStrictEqual(game.currentRound, null, "current round there"); assert.deepEqual( game.result, @@ -201,7 +201,9 @@ export default function() { }); QUnit.test("reverse tailor victory with low goal", function(assert) { - let game = new Game(3); + let rules = new GameRules(); + rules.goal = 3; + let game = new Game(rules); game.currentRound.winner = Team.They; // 2 game.currentRound.winner = Team.We; // 2 game.currentRound.winner = Team.We; // 4 @@ -220,8 +222,39 @@ export default function() { ); }); + QUnit.test("raising rules effect - unless stricken", function(assert) { + let rules = new GameRules(); + rules.goal = 3; + rules.raising = RaisingRule.UnlessStricken; + + let game = new Game(rules); + game.currentRound.raise(Team.We); + game.currentRound.raise(Team.They); + assert.notStrictEqual(game.currentRound, null, "round still going"); + + game = new Game(rules); + game.currentRound.winner = Team.We; + game.currentRound.raise(Team.They); + assert.notStrictEqual(game.currentRound, null, "round still going"); + game.currentRound.raise(Team.We); + assert.strictEqual(game.currentRound, null, "round ended"); + }); + + QUnit.test("raising rules effect - until enough", function(assert) { + let rules = new GameRules(); + rules.goal = 3; + rules.raising = RaisingRule.UntilEnough; + + let game = new Game(rules); + game.currentRound.raise(Team.We); + game.currentRound.raise(Team.They); + assert.strictEqual(game.currentRound, null, "round ended"); + }); + QUnit.test("round change triggers event", function(assert) { - let game = new Game(3); + let rules = new GameRules(); + rules.goal = 3; + let game = new Game(); game.addEventListener(Game.EVENT_CHANGE, function() { assert.step("event"); }); @@ -239,36 +272,26 @@ export default function() { let struct = game.toStruct(); let expected = { - goal: 11, - rounds: [ - { points: 2, winner: Team.We }, - { points: 3, winner: Team.They }, - ], - currentRound: { - points: 3, - raisedLast: Team.We, - winner: null, - ourLimit: 9, - theirLimit: 8 - }, + rules: game.rules.toStruct(), + rounds: game.rounds.map((r) => r.toStruct()), + currentRound: game.currentRound.toStruct(), }; assert.deepEqual(struct, expected, "successfull structurizing"); }); QUnit.test("toStruct - finished", function(assert) { - let game = new Game(3); + let rules = new GameRules(); + rules.goal = 3; + let game = new Game(rules); game.currentRound.winner = Team.We; game.currentRound.raise(Team.They); game.currentRound.winner = Team.They; let struct = game.toStruct(); let expected = { - goal: 3, - rounds: [ - { points: 2, winner: Team.We }, - { points: 3, winner: Team.They }, - ], + rules: game.rules.toStruct(), + rounds: game.rounds.map((r) => r.toStruct()), currentRound: null, }; @@ -276,11 +299,13 @@ export default function() { }); QUnit.test("fromStruct - current", function(assert) { - let orig = new Game(4); + let rules = new GameRules(); + rules.goal = 4; + let orig = new Game(rules); orig.currentRound.raise(Team.We); let copy = new Game(orig.toStruct()); - assert.strictEqual(copy.goal, orig.goal, "goals match"); + assert.deepEqual(copy.rules, orig.rules, "rules match"); assert.strictEqual( copy.currentRound.points, orig.currentRound.points, @@ -306,19 +331,34 @@ export default function() { assert.throws(function() { new Game(struct); }, error, message); } - doIt("no goal", new TypeError("struct must contain goal as number")); + doIt( + "no goal", new TypeError("struct must contain either rules or goal")); struct.goal = "3"; - doIt("string goal", new TypeError("struct must contain goal as number")); + doIt( + "string goal", + new TypeError("if struct contains goal, it must be a number")); struct.goal = Math.PI; doIt( "non-int goal", - new RangeError("struct must contain goal >= 1 as integer")); + new RangeError("if struct contains goal, must be integer >= 1")); struct.goal = 0; doIt( "small goal", - new RangeError("struct must contain goal >= 1 as integer")); + new RangeError("if struct contains goal, must be integer >= 1")); struct.goal = 3; + struct.rules = ""; + doIt( + "rules and goal", + new TypeError("struct cannot contain both rules and goal")); + delete struct.goal; + doIt( + "string rules", + new TypeError("if struct contains rules, they must be an object")); + let rules = new GameRules(); + rules.goal = 3; + struct.rules = rules; + doIt("no rounds", new TypeError("struct must contain rounds")); struct.rounds = "nope"; doIt( @@ -352,59 +392,62 @@ export default function() { new Game(struct); }); - // Data Import Tests - // ================= - // - // The tests named "fromStruct - vXX - XXXXX" are there to ensure that - // future versions of the `Game` class still can correctly read in the - // structural data exported by earlier versions. This is needed to ensure - // that the data remains usable. - // - // These tests work by importing an old structural object, and then - // exporting a new one. The new one should match with how the current - // implementation would represent the same state. - // - // Therefore you should not modify the `struct` variables. Instead adjust - // the `expected` variable, to make sure the reexported data matches what - // is now correct. + // data for the version import tests + let round1 = new RoundResult(2, Team.We); + let round2 = new RoundResult(3, Team.They); + let current = new Round(2, 3); + let rules = new GameRules(); + rules.goal = 3; + rules.raising = RaisingRule.UntilEnough; - QUnit.test("fromStruct - v1 - unfinished", function(assert) { - let past = new RoundResult(2, Team.We); - let current = new Round(2, 3); - current.raise(Team.They); + QUnit.test.each( + "fromStruct - unfinished", + { + v1: { + goal: 3, + rounds: [ round1.toStruct() ], + currentRound: current.toStruct(), + }, + v2: { + rules: rules.toStruct(), + rounds: [ round1.toStruct() ], + currentRound: current.toStruct(), + }, + }, + function(assert, input) { + let game = new Game(input); + let expeted = { + rules: rules.toStruct(), + rounds: [ round1.toStruct() ], + currentRound: current.toStruct(), + }; + assert.deepEqual(game.toStruct(), expeted, "reexport matches"); + } + ); - let struct = { - goal: 3, - rounds: [ past.toStruct() ], - currentRound: current.toStruct(), - }; - let game = new Game(struct); - - let expected = { - goal: 3, - rounds: [ past.toStruct() ], - currentRound: current.toStruct(), - }; - assert.deepEqual(game.toStruct(), expected, "reexport matches"); - }); - - QUnit.test("fromStruct - v1 - finished", function(assert) { - let round1 = new RoundResult(2, Team.We); - let round2 = new RoundResult(3, Team.They); - - let struct = { - goal: 3, - rounds: [ round1.toStruct(), round2.toStruct() ], - currentRound: null, - }; - let game = new Game(struct); - - let expected = { - goal: 3, - rounds: [ round1.toStruct(), round2.toStruct() ], - currentRound: null, - }; - assert.deepEqual(game.toStruct(), expected, "reexport matches"); - }); + QUnit.test.each( + "fromStruct - finished", + { + v1: { + goal: 3, + rounds: [ round1.toStruct(), round2.toStruct() ], + currentRound: null, + }, + v2: { + rules: rules.toStruct(), + rounds: [ round1.toStruct(), round2.toStruct() ], + currentRound: null, + }, + }, + function(assert, input) { + let game = new Game(input); + let expeted = { + rules: rules.toStruct(), + rounds: [ round1.toStruct(), round2.toStruct() ], + currentRound: null, + }; + assert.deepEqual(game.toStruct(), expeted, "reexport matches"); + } + ); }); } diff --git a/models/session.js b/models/session.js index fadd7ba..3a07a88 100644 --- a/models/session.js +++ b/models/session.js @@ -1,5 +1,6 @@ "use strict"; +import GameRules, { RaisingRule } from "/models/game_rules.js"; import Game from "/models/game.js"; import { Team } from "/models/round.js"; @@ -59,30 +60,20 @@ export default class Session extends EventTarget { * * Triggers the `Session.EVENT_CHANGE` event and sets the update time. */ - #changed() { + #changed = () => { this.#updated = new Date(); this.dispatchEvent(new CustomEvent(Session.EVENT_CHANGE)); } - /** The amout of points at which individual games are won. + /** The rules of the next game. */ + #rules = new GameRules(); + + /** Get the rules of the next game. * - * Only applies to new games. + * Note that the returned object can be manipulated. */ - #goal = 11; - - /** Get the goal for new games. */ - get goal() { - return this.#goal; - } - - /** Set the goal for new games. */ - set goal(value) { - if (typeof value !== "number") - throw new TypeError("goal must be a number"); - if (!Number.isInteger(value) || value < 1) - throw new RangeError("goal must be integer >= 1"); - this.#goal = value; - this.#changed(); + get rules() { + return this.#rules; } /** The name or members of the "we" team. */ @@ -139,7 +130,7 @@ export default class Session extends EventTarget { /** Add another round if there is no current one. */ anotherGame() { if (this.#currentGame === null) { - this.#currentGame = new Game(this.goal); + this.#currentGame = new Game(this.rules); this.#currentGame.addEventListener( Game.EVENT_CHANGE, this.#boundHandleGameChange); this.#changed(); @@ -183,6 +174,7 @@ export default class Session extends EventTarget { constructor(value) { super(); if (value === undefined) { + this.#rules.addEventListener(GameRules.EVENT_CHANGE, this.#changed); } else if (typeof value === "object") { this.#fromStruct(value); } else { @@ -205,7 +197,7 @@ export default class Session extends EventTarget { */ toStruct() { let res = { - goal: this.#goal, + rules: this.#rules.toStruct(), ourTeam: this.#ourTeam, theirTeam: this.#theirTeam, games: this.#games.map((g) => g.toStruct()), @@ -235,11 +227,22 @@ export default class Session extends EventTarget { this.#id = value.id; } - if (typeof value.goal !== "number") - throw new TypeError("struct must contain goal as number"); - if (!Number.isInteger(value.goal) || value.goal < 1) - throw new RangeError("struct must contain goal >= 1 as integer"); - this.#goal = value.goal; + if ("goal" in value && "rules" in value) + throw new TypeError("struct cannot contain both rules and goal"); + else if ("goal" in value) { + if (typeof value.goal !== "number") + throw new TypeError("if struct contains goal, it must be a number"); + if (!Number.isInteger(value.goal) || value.goal < 1) + throw new RangeError("if struct contains goal, must be integer >= 1"); + this.#rules.goal = value.goal; + this.#rules.raising = RaisingRule.UntilEnough; + } else if ("rules" in value) { + if (typeof value.rules !== "object") + throw new TypeError("if struct contains rules, they must be an object"); + this.#rules = new GameRules(value.rules); + this.#rules.addEventListener(GameRules.EVENT_CHANGE, this.#changed); + } else + throw new TypeError("struct must contain either rules or goal"); if (typeof value.ourTeam !== "string") throw new TypeError("struct must contain ourTeam as string"); diff --git a/models/session.test.js b/models/session.test.js index 089d107..b727450 100644 --- a/models/session.test.js +++ b/models/session.test.js @@ -3,13 +3,16 @@ import { Team } from "/models/round.js"; import Game from "/models/game.js"; import Session from "/models/session.js"; +import GameRules, { RaisingRule } from "/models/game_rules.js"; export default function() { QUnit.module("session", function() { QUnit.test("initial state", function(assert) { let now = new Date(); let session = new Session(); - assert.strictEqual(session.goal, 11, "initial goal"); + assert.strictEqual(session.rules.goal, 11, "initial goal"); + assert.strictEqual( + session.rules.raising, RaisingRule.UnlessStricken, "initial raising"); assert.strictEqual(session.games.length, 0, "no finished games"); assert.strictEqual(session.currentGame, null, "no game in progress"); assert.deepEqual( @@ -29,30 +32,25 @@ export default function() { new TypeError("unknown form of Session constructor")); }); - QUnit.test("set goal", function(assert) { + QUnit.test("changing rules triggers change event", function(assert) { let session = new Session(); - assert.strictEqual(session.goal, 11, "initial goal"); session.addEventListener(Session.EVENT_CHANGE, function() { assert.step("event"); }); - session.goal = 3; - assert.strictEqual(session.goal, 3, "changed goal"); + assert.notStrictEqual(session.rules.goal, 3, "not already new goal"); + session.rules.goal = 3; + assert.strictEqual(session.rules.goal, 3, "new goal"); - assert.throws( - function() { session.goal = "0"; }, - new TypeError("goal must be a number"), - "string goal"); - assert.throws( - function() { session.goal = 0.5; }, - new RangeError("goal must be integer >= 1"), - "float goal"); - assert.throws( - function() { session.goal = 0; }, - new RangeError("goal must be integer >= 1"), - "small goal"); + assert.notStrictEqual( + session.rules.raising, + RaisingRule.UntilEnough, + "not already new raising rule"); + session.rules.raising = RaisingRule.UntilEnough; + assert.strictEqual( + session.rules.raising, RaisingRule.UntilEnough, "new raising rule"); - assert.verifySteps(["event"], "event happened once"); + assert.verifySteps(["event", "event"], "event happened twice"); assert.true(session.updated >= session.created, "was updated"); }); @@ -66,7 +64,7 @@ export default function() { let session = new Session(); session.anotherGame(); session.currentGame.currentRound.winner = Team.We; - for (let i = 0; i < session.goal; i += 2) + for (let i = 0; i < session.rules.goal; i += 2) session.currentGame.currentRound.winner = Team.They; assert.strictEqual(session.games.length, 1, "single game"); @@ -89,10 +87,10 @@ export default function() { let session = new Session(); session.anotherGame(); session.currentGame.currentRound.winner = Team.We; - for (let i = 0; i < session.goal; i += 2) + for (let i = 0; i < session.rules.goal; i += 2) session.currentGame.currentRound.winner = Team.They; session.anotherGame(); - for (let i = 0; i < session.goal; i += 2) + for (let i = 0; i < session.rules.goal; i += 2) session.currentGame.currentRound.winner = Team.We; assert.strictEqual(session.games.length, 2, "two games") @@ -194,7 +192,7 @@ export default function() { let struct = session.toStruct(); let expected = { - goal: 11, + rules: session.rules.toStruct(), ourTeam: "", theirTeam: "", games: [], @@ -219,18 +217,13 @@ export default function() { session.theirTeam = "This is them!"; let struct = session.toStruct(); - let finished = new Game(3); - finished.currentRound.raise(Team.We); - finished.currentRound.winner = Team.They; - let unfinished = new Game(3); - unfinished.currentRound.winner = Team.We; let expected = { id: 15, - goal: 3, + rules: session.rules.toStruct(), ourTeam: "This is us!", theirTeam: "This is them!", - games: [ finished.toStruct() ], - currentGame: unfinished.toStruct(), + games: session.games.map(g => g.toStruct()), + currentGame: session.currentGame.toStruct(), created: session.created, updated: session.updated, }; @@ -240,14 +233,17 @@ export default function() { QUnit.test("fromStruct - current", function(assert) { let orig = new Session(); - orig.goal = 3; + orig.rules.goal = 3; + orig.rules.raising = RaisingRule.UntilEnough; orig.ourTeam = "This is us!"; orig.theirTeam = "This is them!"; let copy = new Session(orig.toStruct()); assert.strictEqual(copy.id, orig.id, "IDs match"); assert.strictEqual(copy.id, null, "copy ID is null"); - assert.strictEqual(copy.goal, orig.goal, "goals match"); + assert.strictEqual(copy.rules.goal, orig.rules.goal, "goals match"); + assert.strictEqual( + copy.rules.raising, orig.rules.raising, "raising rule matches"); assert.strictEqual(copy.ourTeam, orig.ourTeam, "our teams match"); assert.strictEqual(copy.theirTeam, orig.theirTeam, "their teams match"); assert.strictEqual( @@ -288,9 +284,11 @@ export default function() { assert.throws(function() { new Session(struct); }, error, message); } - let unfinished = new Game(3); + let rules = new GameRules(); + rules.goal = 3; + let unfinished = new Game(rules); unfinished.currentRound.winner = Team.We; - let finished = new Game(3); + let finished = new Game(rules); finished.currentRound.raise(Team.We); finished.currentRound.winner = Team.They; @@ -308,19 +306,32 @@ export default function() { new TypeError("if struct contains id, then it must be a number")); delete struct.id; - doIt("no goal", new TypeError("struct must contain goal as number")); + doIt( + "no goal", new TypeError("struct must contain either rules or goal")); struct.goal = "3"; - doIt("string goal", new TypeError("struct must contain goal as number")); + doIt( + "string goal", + new TypeError("if struct contains goal, it must be a number")); struct.goal = Math.PI; doIt( "non-int goal", - new RangeError("struct must contain goal >= 1 as integer")); + new RangeError("if struct contains goal, must be integer >= 1")); struct.goal = 0; doIt( "small goal", - new RangeError("struct must contain goal >= 1 as integer")); + new RangeError("if struct contains goal, must be integer >= 1")); struct.goal = 3; + struct.rules = ""; + doIt( + "rules and goal", + new TypeError("struct cannot contain both rules and goal")); + delete struct.goal; + doIt( + "string rules", + new TypeError("if struct contains rules, they must be an object")); + struct.rules = rules; + doIt( "no ourTeam", new TypeError("struct must contain ourTeam as string")); struct.ourTeam = 5; @@ -382,182 +393,134 @@ export default function() { new Session(struct); }); - // Data Import Tests - // ================= - // - // The tests named "fromStruct - vXX - XXXXX" are there to ensure that - // future versions of the `Session` class still can correctly read in the - // structural data exported by earlier versions. This is needed to ensure - // that the data remains usable. - // - // These tests work by importing an old structural object, and then - // exporting a new one. The new one should match with how the current - // implementation would represent the same state. - // - // Therefore you should not modify the `struct` variables. Instead adjust - // the `expected` variable, to make sure the reexported data matches what - // is now correct. + // data for the version import tests + let rules = new GameRules(); + rules.goal = 3; + rules.raising = RaisingRule.UntilEnough; + let finished = new Game(rules); + finished.currentRound.raise(Team.We); + finished.currentRound.winner = Team.They; + let unfinished = new Game(rules); + unfinished.currentRound.winner = Team.We; - QUnit.test("fromStruct - v1 - new session", function(assert) { - let struct = { - goal: 3, - ourTeam: "", - theirTeam: "", - games: [], - currentGame: null, - }; - let session = new Session(struct); + QUnit.test.each( + "fromStruct - new session", + { + v1: { + goal: 3, + ourTeam: "", + theirTeam: "", + games: [], + currentGame: null, + }, + v2: { + id: 23, + goal: 3, + ourTeam: "", + theirTeam: "", + games: [], + currentGame: null, + }, + v3: { + id: 23, + goal: 3, + ourTeam: "", + theirTeam: "", + games: [], + currentGame: null, + created: new Date("2026-02-26T20:05:00"), + updated: new Date("2026-02-26T20:05:00"), + }, + v4: { + id: 23, + rules: rules.toStruct(), + ourTeam: "", + theirTeam: "", + games: [], + currentGame: null, + created: new Date("2026-02-26T20:05:00"), + updated: new Date("2026-02-26T20:05:00"), + }, + }, + function(assert, input) { + let session = new Session(input); + let expected = { + rules: rules.toStruct(), + ourTeam: "", + theirTeam: "", + games: [], + currentGame: null, + created: new Date("2026-02-26T22:00:00"), + updated: new Date("2026-02-26T22:00:00"), + }; + if ("id" in input) + expected.id = input.id; + if ("created" in input) + expected.created = new Date(input.created); + if ("updated" in input) + expected.updated = new Date(input.updated); + assert.deepEqual(session.toStruct(), expected, "reexport matches"); + } + ); - let expected = { - goal: 3, - ourTeam: "", - theirTeam: "", - games: [], - currentGame: null, - created: new Date("2026-02-26T22:00:00"), - updated: new Date("2026-02-26T22:00:00"), - }; - assert.deepEqual(session.toStruct(), expected, "reexport matches"); - }); - - QUnit.test("fromStruct - v1 - finished & unfinished", function(assert) { - let finished = new Game(3); - finished.currentRound.raise(Team.We); - finished.currentRound.winner = Team.They; - let unfinished = new Game(3); - unfinished.currentRound.winner = Team.We; - - let struct = { - goal: 3, - ourTeam: "This is us!", - theirTeam: "This is them!", - games: [ finished.toStruct() ], - currentGame: unfinished.toStruct(), - }; - let session = new Session(struct); - - let expected = { - goal: 3, - ourTeam: "This is us!", - theirTeam: "This is them!", - games: [ finished.toStruct() ], - currentGame: unfinished.toStruct(), - created: new Date("2026-02-26T22:00:00"), - updated: new Date("2026-02-26T22:00:00"), - }; - assert.deepEqual(session.toStruct(), expected, "reexport matches"); - }); - - QUnit.test("fromStruct - v2 - new session", function(assert) { - let struct = { - id: 23, - goal: 3, - ourTeam: "", - theirTeam: "", - games: [], - currentGame: null, - }; - let session = new Session(struct); - - let expected = { - id: 23, - goal: 3, - ourTeam: "", - theirTeam: "", - games: [], - currentGame: null, - created: new Date("2026-02-26T22:00:00"), - updated: new Date("2026-02-26T22:00:00"), - }; - assert.deepEqual(session.toStruct(), expected, "reexport matches"); - }); - - QUnit.test("fromStruct - v2 - finished & unfinished", function(assert) { - let finished = new Game(3); - finished.currentRound.raise(Team.We); - finished.currentRound.winner = Team.They; - let unfinished = new Game(3); - unfinished.currentRound.winner = Team.We; - - let struct = { - id: 17, - goal: 3, - ourTeam: "This is us!", - theirTeam: "This is them!", - games: [ finished.toStruct() ], - currentGame: unfinished.toStruct(), - }; - let session = new Session(struct); - - let expected = { - id: 17, - goal: 3, - ourTeam: "This is us!", - theirTeam: "This is them!", - games: [ finished.toStruct() ], - currentGame: unfinished.toStruct(), - created: new Date("2026-02-26T22:00:00"), - updated: new Date("2026-02-26T22:00:00"), - }; - assert.deepEqual(session.toStruct(), expected, "reexport matches"); - }); - - QUnit.test("fromStruct - v3 - new session", function(assert) { - let struct = { - id: 23, - goal: 3, - ourTeam: "", - theirTeam: "", - games: [], - currentGame: null, - created: new Date("2026-02-26T20:05:00"), - updated: new Date("2026-02-26T20:05:00"), - }; - let session = new Session(struct); - - let expected = { - id: 23, - goal: 3, - ourTeam: "", - theirTeam: "", - games: [], - currentGame: null, - created: new Date("2026-02-26T20:05:00"), - updated: new Date("2026-02-26T20:05:00"), - }; - assert.deepEqual(session.toStruct(), expected, "reexport matches"); - }); - - QUnit.test("fromStruct - v3 - finished & unfinished", function(assert) { - let finished = new Game(3); - finished.currentRound.raise(Team.We); - finished.currentRound.winner = Team.They; - let unfinished = new Game(3); - unfinished.currentRound.winner = Team.We; - - let struct = { - id: 17, - goal: 3, - ourTeam: "This is us!", - theirTeam: "This is them!", - games: [ finished.toStruct() ], - currentGame: unfinished.toStruct(), - created: new Date("2026-02-26T20:05:00"), - updated: new Date("2026-02-26T20:05:00"), - }; - let session = new Session(struct); - - let expected = { - id: 17, - goal: 3, - ourTeam: "This is us!", - theirTeam: "This is them!", - games: [ finished.toStruct() ], - currentGame: unfinished.toStruct(), - created: new Date("2026-02-26T20:05:00"), - updated: new Date("2026-02-26T20:05:00"), - }; - assert.deepEqual(session.toStruct(), expected, "reexport matches"); - }); + QUnit.test.each( + "fromStruct - finished & unfinished", + { + v1: { + goal: 3, + ourTeam: "This is us!", + theirTeam: "This is them!", + games: [ finished.toStruct() ], + currentGame: unfinished.toStruct(), + }, + v2: { + id: 17, + goal: 3, + ourTeam: "This is us!", + theirTeam: "This is them!", + games: [ finished.toStruct() ], + currentGame: unfinished.toStruct(), + }, + v3: { + id: 17, + goal: 3, + ourTeam: "This is us!", + theirTeam: "This is them!", + games: [ finished.toStruct() ], + currentGame: unfinished.toStruct(), + created: new Date("2026-02-26T20:05:00"), + updated: new Date("2026-02-26T20:05:00"), + }, + v4: { + id: 17, + rules: rules.toStruct(), + ourTeam: "This is us!", + theirTeam: "This is them!", + games: [ finished.toStruct() ], + currentGame: unfinished.toStruct(), + created: new Date("2026-02-26T20:05:00"), + updated: new Date("2026-02-26T20:05:00"), + }, + }, + function(assert, input) { + let session = new Session(input); + let expected = { + rules: rules.toStruct(), + ourTeam: "This is us!", + theirTeam: "This is them!", + games: [ finished.toStruct() ], + currentGame: unfinished.toStruct(), + created: new Date("2026-02-26T22:00:00"), + updated: new Date("2026-02-26T22:00:00"), + }; + if ("id" in input) + expected.id = input.id; + if ("created" in input) + expected.created = new Date(input.created); + if ("updated" in input) + expected.updated = new Date(input.updated); + assert.deepEqual(session.toStruct(), expected, "reexport matches"); + } + ); }); }