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";
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 ("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("struct must contain goal as number");
throw new TypeError("if struct contains goal, it must be a number");
if (!Number.isInteger(value.goal) || value.goal < 1)
throw new RangeError("struct must contain goal >= 1 as integer");
this.#goal = value.goal;
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");

View File

@ -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.
QUnit.test("fromStruct - v1 - unfinished", function(assert) {
let past = new RoundResult(2, Team.We);
let current = new Round(2, 3);
current.raise(Team.They);
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) {
// 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;
let struct = {
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");
}
);
QUnit.test.each(
"fromStruct - finished",
{
v1: {
goal: 3,
rounds: [ round1.toStruct(), round2.toStruct() ],
currentRound: null,
};
let game = new Game(struct);
let expected = {
goal: 3,
},
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(), expected, "reexport matches");
});
assert.deepEqual(game.toStruct(), expeted, "reexport matches");
}
);
});
}

View File

@ -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 ("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("struct must contain goal as number");
throw new TypeError("if struct contains goal, it must be a number");
if (!Number.isInteger(value.goal) || value.goal < 1)
throw new RangeError("struct must contain goal >= 1 as integer");
this.#goal = value.goal;
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");

View File

@ -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.
QUnit.test("fromStruct - v1 - new session", function(assert) {
let struct = {
goal: 3,
ourTeam: "",
theirTeam: "",
games: [],
currentGame: null,
};
let session = new Session(struct);
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);
// 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(3);
let unfinished = new Game(rules);
unfinished.currentRound.winner = Team.We;
let struct = {
QUnit.test.each(
"fromStruct - new session",
{
v1: {
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 = {
ourTeam: "",
theirTeam: "",
games: [],
currentGame: null,
},
v2: {
id: 23,
goal: 3,
ourTeam: "",
theirTeam: "",
games: [],
currentGame: null,
};
let session = new Session(struct);
let expected = {
},
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");
});
}
);
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 = {
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(),
};
let session = new Session(struct);
let expected = {
},
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");
});
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");
});
}
);
});
}