1
0

switch session and game models to game rules

This is a more flexible system than the previous goal variable. It makes
it possible to conform to the association rules, without sacrificing
backwards compatibility. Also, it makes it easier to add other
changeable rules later on.
This commit is contained in:
Adrian Wannenmacher 2026-03-02 02:52:19 +01:00
parent 14aad4a73d
commit bd79aa568a
Signed by: tfld
GPG Key ID: 19D986ECB1E492D5
4 changed files with 373 additions and 350 deletions

View File

@ -1,5 +1,6 @@
"use strict"; "use strict";
import GameRules, { RaisingRule } from "/models/game_rules.js";
import { Round, Team } from "/models/round.js"; import { Round, Team } from "/models/round.js";
import RoundResult from "/models/round_result.js"; import RoundResult from "/models/round_result.js";
@ -31,12 +32,17 @@ export default class Game extends EventTarget {
return this.#rounds; return this.#rounds;
} }
/** How many points a team needs to win. */ /** The rules of this game. */
#goal = 11; #rules = new GameRules();
/** Get how many points are needed to win. */ /** Get the rules of this game.
get goal() { *
return this.#goal; * 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. /** The current round.
@ -51,14 +57,12 @@ export default class Game extends EventTarget {
constructor(value) { constructor(value) {
super(); super();
if (value === undefined || typeof value === "number") { if (value === undefined || value instanceof GameRules) {
if (typeof value === "number") if (value instanceof GameRules)
this.#goal = value; this.#rules = value;
if (this.#goal < 1) this.#currentRound = new Round(
throw new RangeError("goal must be at least 1"); this.#rules.raisingLimit(0), this.#rules.raisingLimit(0));
this.#currentRound = new Round(this.#goal, this.#goal);
this.#currentRound.addEventListener( this.#currentRound.addEventListener(
Round.EVENT_CHANGE, this.#boundHandleRoundChange); Round.EVENT_CHANGE, this.#boundHandleRoundChange);
} else if (typeof value === "object") { } else if (typeof value === "object") {
@ -78,7 +82,7 @@ export default class Game extends EventTarget {
let ourPoints = 0; let ourPoints = 0;
let theirPoints = 0; let theirPoints = 0;
let tailor = null; let tailor = null;
const tailorGoal = this.#goal - 2; const tailorGoal = this.#rules.goal - 2;
for (let r of this.#rounds) { for (let r of this.#rounds) {
if (r.winner === Team.We) if (r.winner === Team.We)
@ -94,8 +98,8 @@ export default class Game extends EventTarget {
} }
} }
let weWon = ourPoints >= this.goal; let weWon = ourPoints >= this.#rules.goal;
let theyWon = theirPoints >= this.goal; let theyWon = theirPoints >= this.#rules.goal;
let winner; let winner;
if (!weWon && !theyWon) { if (!weWon && !theyWon) {
@ -137,8 +141,8 @@ export default class Game extends EventTarget {
if (result.winner === null) { if (result.winner === null) {
this.#currentRound = new Round( this.#currentRound = new Round(
Math.max(this.#goal - result.ourPoints, 2), this.#rules.raisingLimit(result.ourPoints),
Math.max(this.#goal - result.theirPoints, 2)); this.#rules.raisingLimit(result.theirPoints));
this.#currentRound.addEventListener( this.#currentRound.addEventListener(
Round.EVENT_CHANGE, this.#boundHandleRoundChange); Round.EVENT_CHANGE, this.#boundHandleRoundChange);
} }
@ -165,7 +169,7 @@ export default class Game extends EventTarget {
*/ */
toStruct() { toStruct() {
return { return {
goal: this.#goal, rules: this.#rules.toStruct(),
rounds: this.#rounds.map((r) => r.toStruct()), rounds: this.#rounds.map((r) => r.toStruct()),
currentRound: currentRound:
this.#currentRound !== null ? this.#currentRound.toStruct() : null, this.#currentRound !== null ? this.#currentRound.toStruct() : null,
@ -177,11 +181,21 @@ export default class Game extends EventTarget {
if (typeof value !== "object") if (typeof value !== "object")
throw new TypeError("struct must be an object"); throw new TypeError("struct must be an object");
if (typeof value.goal !== "number") if ("goal" in value && "rules" in value)
throw new TypeError("struct must contain goal as number"); throw new TypeError("struct cannot contain both rules and goal");
if (!Number.isInteger(value.goal) || value.goal < 1) else if ("goal" in value) {
throw new RangeError("struct must contain goal >= 1 as integer"); if (typeof value.goal !== "number")
this.#goal = value.goal; 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)) if (!("rounds" in value))
throw new TypeError("struct must contain rounds"); throw new TypeError("struct must contain rounds");

View File

@ -3,13 +3,16 @@
import { Round, Team } from "/models/round.js"; import { Round, Team } from "/models/round.js";
import RoundResult from "/models/round_result.js"; import RoundResult from "/models/round_result.js";
import Game from "/models/game.js"; import Game from "/models/game.js";
import GameRules, { RaisingRule } from "./game_rules.js";
export default function() { export default function() {
QUnit.module("game", function() { QUnit.module("game", function() {
QUnit.test("default construction", function(assert) { QUnit.test("default construction", function(assert) {
let game = new Game(); let game = new Game();
assert.strictEqual(game.rounds.length, 0, "no past rounds"); 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.notStrictEqual(game.currentRound, null, "current round there");
assert.deepEqual( assert.deepEqual(
game.result, game.result,
@ -29,17 +32,14 @@ export default function() {
new TypeError("unknown form of Game constructor")); new TypeError("unknown form of Game constructor"));
}); });
QUnit.test("low goal", function(assert) { QUnit.test("with non-default rules", function(assert) {
assert.throws( let rules = new GameRules();
function() { new Game(0); }, rules.goal = 15;
new RangeError("goal must be at least 1"), rules.raising = RaisingRule.UntilEnough;
"goal must be 1 or higher"); let game = new Game(rules);
});
QUnit.test("higher goal", function(assert) {
let game = new Game(15);
assert.strictEqual(game.rounds.length, 0, "no past rounds"); 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.notStrictEqual(game.currentRound, null, "current round there");
assert.deepEqual( assert.deepEqual(
game.result, game.result,
@ -201,7 +201,9 @@ export default function() {
}); });
QUnit.test("reverse tailor victory with low goal", function(assert) { 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.They; // 2
game.currentRound.winner = Team.We; // 2 game.currentRound.winner = Team.We; // 2
game.currentRound.winner = Team.We; // 4 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) { 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() { game.addEventListener(Game.EVENT_CHANGE, function() {
assert.step("event"); assert.step("event");
}); });
@ -239,36 +272,26 @@ export default function() {
let struct = game.toStruct(); let struct = game.toStruct();
let expected = { let expected = {
goal: 11, rules: game.rules.toStruct(),
rounds: [ rounds: game.rounds.map((r) => r.toStruct()),
{ points: 2, winner: Team.We }, currentRound: game.currentRound.toStruct(),
{ points: 3, winner: Team.They },
],
currentRound: {
points: 3,
raisedLast: Team.We,
winner: null,
ourLimit: 9,
theirLimit: 8
},
}; };
assert.deepEqual(struct, expected, "successfull structurizing"); assert.deepEqual(struct, expected, "successfull structurizing");
}); });
QUnit.test("toStruct - finished", function(assert) { 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.winner = Team.We;
game.currentRound.raise(Team.They); game.currentRound.raise(Team.They);
game.currentRound.winner = Team.They; game.currentRound.winner = Team.They;
let struct = game.toStruct(); let struct = game.toStruct();
let expected = { let expected = {
goal: 3, rules: game.rules.toStruct(),
rounds: [ rounds: game.rounds.map((r) => r.toStruct()),
{ points: 2, winner: Team.We },
{ points: 3, winner: Team.They },
],
currentRound: null, currentRound: null,
}; };
@ -276,11 +299,13 @@ export default function() {
}); });
QUnit.test("fromStruct - current", function(assert) { 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); orig.currentRound.raise(Team.We);
let copy = new Game(orig.toStruct()); let copy = new Game(orig.toStruct());
assert.strictEqual(copy.goal, orig.goal, "goals match"); assert.deepEqual(copy.rules, orig.rules, "rules match");
assert.strictEqual( assert.strictEqual(
copy.currentRound.points, copy.currentRound.points,
orig.currentRound.points, orig.currentRound.points,
@ -306,19 +331,34 @@ export default function() {
assert.throws(function() { new Game(struct); }, error, message); 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"; 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; struct.goal = Math.PI;
doIt( doIt(
"non-int goal", "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; struct.goal = 0;
doIt( doIt(
"small goal", "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.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")); doIt("no rounds", new TypeError("struct must contain rounds"));
struct.rounds = "nope"; struct.rounds = "nope";
doIt( doIt(
@ -352,59 +392,62 @@ export default function() {
new Game(struct); new Game(struct);
}); });
// Data Import Tests // data for the version import tests
// ================= let round1 = new RoundResult(2, Team.We);
// let round2 = new RoundResult(3, Team.They);
// The tests named "fromStruct - vXX - XXXXX" are there to ensure that let current = new Round(2, 3);
// future versions of the `Game` class still can correctly read in the let rules = new GameRules();
// structural data exported by earlier versions. This is needed to ensure rules.goal = 3;
// that the data remains usable. rules.raising = RaisingRule.UntilEnough;
//
// 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.
QUnit.test("fromStruct - v1 - unfinished", function(assert) { QUnit.test.each(
let past = new RoundResult(2, Team.We); "fromStruct - unfinished",
let current = new Round(2, 3); {
current.raise(Team.They); 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 = { QUnit.test.each(
goal: 3, "fromStruct - finished",
rounds: [ past.toStruct() ], {
currentRound: current.toStruct(), v1: {
}; goal: 3,
let game = new Game(struct); rounds: [ round1.toStruct(), round2.toStruct() ],
currentRound: null,
let expected = { },
goal: 3, v2: {
rounds: [ past.toStruct() ], rules: rules.toStruct(),
currentRound: current.toStruct(), rounds: [ round1.toStruct(), round2.toStruct() ],
}; currentRound: null,
assert.deepEqual(game.toStruct(), expected, "reexport matches"); },
}); },
function(assert, input) {
QUnit.test("fromStruct - v1 - finished", function(assert) { let game = new Game(input);
let round1 = new RoundResult(2, Team.We); let expeted = {
let round2 = new RoundResult(3, Team.They); rules: rules.toStruct(),
rounds: [ round1.toStruct(), round2.toStruct() ],
let struct = { currentRound: null,
goal: 3, };
rounds: [ round1.toStruct(), round2.toStruct() ], assert.deepEqual(game.toStruct(), expeted, "reexport matches");
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");
});
}); });
} }

View File

@ -1,5 +1,6 @@
"use strict"; "use strict";
import GameRules, { RaisingRule } from "/models/game_rules.js";
import Game from "/models/game.js"; import Game from "/models/game.js";
import { Team } from "/models/round.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. * Triggers the `Session.EVENT_CHANGE` event and sets the update time.
*/ */
#changed() { #changed = () => {
this.#updated = new Date(); this.#updated = new Date();
this.dispatchEvent(new CustomEvent(Session.EVENT_CHANGE)); 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 rules() {
return this.#rules;
/** 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();
} }
/** The name or members of the "we" team. */ /** 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. */ /** Add another round if there is no current one. */
anotherGame() { anotherGame() {
if (this.#currentGame === null) { if (this.#currentGame === null) {
this.#currentGame = new Game(this.goal); this.#currentGame = new Game(this.rules);
this.#currentGame.addEventListener( this.#currentGame.addEventListener(
Game.EVENT_CHANGE, this.#boundHandleGameChange); Game.EVENT_CHANGE, this.#boundHandleGameChange);
this.#changed(); this.#changed();
@ -183,6 +174,7 @@ export default class Session extends EventTarget {
constructor(value) { constructor(value) {
super(); super();
if (value === undefined) { if (value === undefined) {
this.#rules.addEventListener(GameRules.EVENT_CHANGE, this.#changed);
} else if (typeof value === "object") { } else if (typeof value === "object") {
this.#fromStruct(value); this.#fromStruct(value);
} else { } else {
@ -205,7 +197,7 @@ export default class Session extends EventTarget {
*/ */
toStruct() { toStruct() {
let res = { let res = {
goal: this.#goal, rules: this.#rules.toStruct(),
ourTeam: this.#ourTeam, ourTeam: this.#ourTeam,
theirTeam: this.#theirTeam, theirTeam: this.#theirTeam,
games: this.#games.map((g) => g.toStruct()), games: this.#games.map((g) => g.toStruct()),
@ -235,11 +227,22 @@ export default class Session extends EventTarget {
this.#id = value.id; this.#id = value.id;
} }
if (typeof value.goal !== "number") if ("goal" in value && "rules" in value)
throw new TypeError("struct must contain goal as number"); throw new TypeError("struct cannot contain both rules and goal");
if (!Number.isInteger(value.goal) || value.goal < 1) else if ("goal" in value) {
throw new RangeError("struct must contain goal >= 1 as integer"); if (typeof value.goal !== "number")
this.#goal = value.goal; 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") if (typeof value.ourTeam !== "string")
throw new TypeError("struct must contain ourTeam as string"); throw new TypeError("struct must contain ourTeam as string");

View File

@ -3,13 +3,16 @@
import { Team } from "/models/round.js"; import { Team } from "/models/round.js";
import Game from "/models/game.js"; import Game from "/models/game.js";
import Session from "/models/session.js"; import Session from "/models/session.js";
import GameRules, { RaisingRule } from "/models/game_rules.js";
export default function() { export default function() {
QUnit.module("session", function() { QUnit.module("session", function() {
QUnit.test("initial state", function(assert) { QUnit.test("initial state", function(assert) {
let now = new Date(); let now = new Date();
let session = new Session(); 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.games.length, 0, "no finished games");
assert.strictEqual(session.currentGame, null, "no game in progress"); assert.strictEqual(session.currentGame, null, "no game in progress");
assert.deepEqual( assert.deepEqual(
@ -29,30 +32,25 @@ export default function() {
new TypeError("unknown form of Session constructor")); 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(); let session = new Session();
assert.strictEqual(session.goal, 11, "initial goal");
session.addEventListener(Session.EVENT_CHANGE, function() { session.addEventListener(Session.EVENT_CHANGE, function() {
assert.step("event"); assert.step("event");
}); });
session.goal = 3; assert.notStrictEqual(session.rules.goal, 3, "not already new goal");
assert.strictEqual(session.goal, 3, "changed goal"); session.rules.goal = 3;
assert.strictEqual(session.rules.goal, 3, "new goal");
assert.throws( assert.notStrictEqual(
function() { session.goal = "0"; }, session.rules.raising,
new TypeError("goal must be a number"), RaisingRule.UntilEnough,
"string goal"); "not already new raising rule");
assert.throws( session.rules.raising = RaisingRule.UntilEnough;
function() { session.goal = 0.5; }, assert.strictEqual(
new RangeError("goal must be integer >= 1"), session.rules.raising, RaisingRule.UntilEnough, "new raising rule");
"float goal");
assert.throws(
function() { session.goal = 0; },
new RangeError("goal must be integer >= 1"),
"small goal");
assert.verifySteps(["event"], "event happened once"); assert.verifySteps(["event", "event"], "event happened twice");
assert.true(session.updated >= session.created, "was updated"); assert.true(session.updated >= session.created, "was updated");
}); });
@ -66,7 +64,7 @@ export default function() {
let session = new Session(); let session = new Session();
session.anotherGame(); session.anotherGame();
session.currentGame.currentRound.winner = Team.We; 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.currentGame.currentRound.winner = Team.They;
assert.strictEqual(session.games.length, 1, "single game"); assert.strictEqual(session.games.length, 1, "single game");
@ -89,10 +87,10 @@ export default function() {
let session = new Session(); let session = new Session();
session.anotherGame(); session.anotherGame();
session.currentGame.currentRound.winner = Team.We; 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.currentGame.currentRound.winner = Team.They;
session.anotherGame(); 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; session.currentGame.currentRound.winner = Team.We;
assert.strictEqual(session.games.length, 2, "two games") assert.strictEqual(session.games.length, 2, "two games")
@ -194,7 +192,7 @@ export default function() {
let struct = session.toStruct(); let struct = session.toStruct();
let expected = { let expected = {
goal: 11, rules: session.rules.toStruct(),
ourTeam: "", ourTeam: "",
theirTeam: "", theirTeam: "",
games: [], games: [],
@ -219,18 +217,13 @@ export default function() {
session.theirTeam = "This is them!"; session.theirTeam = "This is them!";
let struct = session.toStruct(); 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 = { let expected = {
id: 15, id: 15,
goal: 3, rules: session.rules.toStruct(),
ourTeam: "This is us!", ourTeam: "This is us!",
theirTeam: "This is them!", theirTeam: "This is them!",
games: [ finished.toStruct() ], games: session.games.map(g => g.toStruct()),
currentGame: unfinished.toStruct(), currentGame: session.currentGame.toStruct(),
created: session.created, created: session.created,
updated: session.updated, updated: session.updated,
}; };
@ -240,14 +233,17 @@ export default function() {
QUnit.test("fromStruct - current", function(assert) { QUnit.test("fromStruct - current", function(assert) {
let orig = new Session(); let orig = new Session();
orig.goal = 3; orig.rules.goal = 3;
orig.rules.raising = RaisingRule.UntilEnough;
orig.ourTeam = "This is us!"; orig.ourTeam = "This is us!";
orig.theirTeam = "This is them!"; orig.theirTeam = "This is them!";
let copy = new Session(orig.toStruct()); let copy = new Session(orig.toStruct());
assert.strictEqual(copy.id, orig.id, "IDs match"); assert.strictEqual(copy.id, orig.id, "IDs match");
assert.strictEqual(copy.id, null, "copy ID is null"); 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.ourTeam, orig.ourTeam, "our teams match");
assert.strictEqual(copy.theirTeam, orig.theirTeam, "their teams match"); assert.strictEqual(copy.theirTeam, orig.theirTeam, "their teams match");
assert.strictEqual( assert.strictEqual(
@ -288,9 +284,11 @@ export default function() {
assert.throws(function() { new Session(struct); }, error, message); 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; unfinished.currentRound.winner = Team.We;
let finished = new Game(3); let finished = new Game(rules);
finished.currentRound.raise(Team.We); finished.currentRound.raise(Team.We);
finished.currentRound.winner = Team.They; finished.currentRound.winner = Team.They;
@ -308,19 +306,32 @@ export default function() {
new TypeError("if struct contains id, then it must be a number")); new TypeError("if struct contains id, then it must be a number"));
delete struct.id; 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"; 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; struct.goal = Math.PI;
doIt( doIt(
"non-int goal", "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; struct.goal = 0;
doIt( doIt(
"small goal", "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.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( doIt(
"no ourTeam", new TypeError("struct must contain ourTeam as string")); "no ourTeam", new TypeError("struct must contain ourTeam as string"));
struct.ourTeam = 5; struct.ourTeam = 5;
@ -382,182 +393,134 @@ export default function() {
new Session(struct); new Session(struct);
}); });
// Data Import Tests // data for the version import tests
// ================= let rules = new GameRules();
// rules.goal = 3;
// The tests named "fromStruct - vXX - XXXXX" are there to ensure that rules.raising = RaisingRule.UntilEnough;
// future versions of the `Session` class still can correctly read in the let finished = new Game(rules);
// structural data exported by earlier versions. This is needed to ensure finished.currentRound.raise(Team.We);
// that the data remains usable. finished.currentRound.winner = Team.They;
// let unfinished = new Game(rules);
// These tests work by importing an old structural object, and then unfinished.currentRound.winner = Team.We;
// 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.
QUnit.test("fromStruct - v1 - new session", function(assert) { QUnit.test.each(
let struct = { "fromStruct - new session",
goal: 3, {
ourTeam: "", v1: {
theirTeam: "", goal: 3,
games: [], ourTeam: "",
currentGame: null, theirTeam: "",
}; games: [],
let session = new Session(struct); 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 = { QUnit.test.each(
goal: 3, "fromStruct - finished & unfinished",
ourTeam: "", {
theirTeam: "", v1: {
games: [], goal: 3,
currentGame: null, ourTeam: "This is us!",
created: new Date("2026-02-26T22:00:00"), theirTeam: "This is them!",
updated: new Date("2026-02-26T22:00:00"), games: [ finished.toStruct() ],
}; currentGame: unfinished.toStruct(),
assert.deepEqual(session.toStruct(), expected, "reexport matches"); },
}); v2: {
id: 17,
QUnit.test("fromStruct - v1 - finished & unfinished", function(assert) { goal: 3,
let finished = new Game(3); ourTeam: "This is us!",
finished.currentRound.raise(Team.We); theirTeam: "This is them!",
finished.currentRound.winner = Team.They; games: [ finished.toStruct() ],
let unfinished = new Game(3); currentGame: unfinished.toStruct(),
unfinished.currentRound.winner = Team.We; },
v3: {
let struct = { id: 17,
goal: 3, goal: 3,
ourTeam: "This is us!", ourTeam: "This is us!",
theirTeam: "This is them!", theirTeam: "This is them!",
games: [ finished.toStruct() ], games: [ finished.toStruct() ],
currentGame: unfinished.toStruct(), currentGame: unfinished.toStruct(),
}; created: new Date("2026-02-26T20:05:00"),
let session = new Session(struct); updated: new Date("2026-02-26T20:05:00"),
},
let expected = { v4: {
goal: 3, id: 17,
ourTeam: "This is us!", rules: rules.toStruct(),
theirTeam: "This is them!", ourTeam: "This is us!",
games: [ finished.toStruct() ], theirTeam: "This is them!",
currentGame: unfinished.toStruct(), games: [ finished.toStruct() ],
created: new Date("2026-02-26T22:00:00"), currentGame: unfinished.toStruct(),
updated: new Date("2026-02-26T22:00:00"), created: new Date("2026-02-26T20:05:00"),
}; updated: new Date("2026-02-26T20:05:00"),
assert.deepEqual(session.toStruct(), expected, "reexport matches"); },
}); },
function(assert, input) {
QUnit.test("fromStruct - v2 - new session", function(assert) { let session = new Session(input);
let struct = { let expected = {
id: 23, rules: rules.toStruct(),
goal: 3, ourTeam: "This is us!",
ourTeam: "", theirTeam: "This is them!",
theirTeam: "", games: [ finished.toStruct() ],
games: [], currentGame: unfinished.toStruct(),
currentGame: null, created: new Date("2026-02-26T22:00:00"),
}; updated: new Date("2026-02-26T22:00:00"),
let session = new Session(struct); };
if ("id" in input)
let expected = { expected.id = input.id;
id: 23, if ("created" in input)
goal: 3, expected.created = new Date(input.created);
ourTeam: "", if ("updated" in input)
theirTeam: "", expected.updated = new Date(input.updated);
games: [], assert.deepEqual(session.toStruct(), expected, "reexport matches");
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");
});
}); });
} }