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.
291 lines
8.4 KiB
JavaScript
291 lines
8.4 KiB
JavaScript
"use strict";
|
|
|
|
import GameRules, { RaisingRule } from "/models/game_rules.js";
|
|
import Game from "/models/game.js";
|
|
import { Team } from "/models/round.js";
|
|
|
|
/** A session of Watten.
|
|
*
|
|
* A session consists of several games, and can be continued for as long as the
|
|
* players want to.
|
|
*
|
|
* This class keeps track of various games and offers the functionality to
|
|
* start new ones. It also has a `result` property, that calculates the current
|
|
* points totals.
|
|
*
|
|
* Note that game points are punitive, players want to avoid earning them.
|
|
* Sessions are also self contained, there is no higher construct they are a
|
|
* part of.
|
|
*/
|
|
export default class Session extends EventTarget {
|
|
/** The event triggered when something about the session changes. */
|
|
static get EVENT_CHANGE() { return "wb:session:change"; }
|
|
|
|
/** The ID of this session. */
|
|
#id = null;
|
|
|
|
/** Get the ID of this session. */
|
|
get id() {
|
|
return this.#id;
|
|
}
|
|
|
|
/** Set the ID of this session.
|
|
*
|
|
* Note that an existing ID cannot be overwritten. Setting an ID also doesn't
|
|
* trigger the `Session.EVENT_CHANGE` event.
|
|
*/
|
|
set id(value) {
|
|
if (this.#id !== null)
|
|
throw new Error("the ID cannot be changed if it has been set");
|
|
this.#id = value;
|
|
}
|
|
|
|
/** The time when this session was initially created. */
|
|
#created = new Date();
|
|
|
|
/** Get the time when this session was initially created. */
|
|
get created() {
|
|
return this.#created;
|
|
}
|
|
|
|
/** The time when this session was last updated. */
|
|
#updated = new Date();
|
|
|
|
/** Get the time when this session was last updated. */
|
|
get updated() {
|
|
return this.#updated;
|
|
}
|
|
|
|
/** Mark this session as changed.
|
|
*
|
|
* Triggers the `Session.EVENT_CHANGE` event and sets the update time.
|
|
*/
|
|
#changed = () => {
|
|
this.#updated = new Date();
|
|
this.dispatchEvent(new CustomEvent(Session.EVENT_CHANGE));
|
|
}
|
|
|
|
/** The rules of the next game. */
|
|
#rules = new GameRules();
|
|
|
|
/** Get the rules of the next game.
|
|
*
|
|
* Note that the returned object can be manipulated.
|
|
*/
|
|
get rules() {
|
|
return this.#rules;
|
|
}
|
|
|
|
/** The name or members of the "we" team. */
|
|
#ourTeam = "";
|
|
|
|
/** Get the name or members of the "we" team. */
|
|
get ourTeam() {
|
|
return this.#ourTeam;
|
|
}
|
|
|
|
/** Set the name or members of the "we" team. */
|
|
set ourTeam(value) {
|
|
this.#ourTeam = value;
|
|
this.#changed();
|
|
}
|
|
|
|
/** The name or members of the "they" team. */
|
|
#theirTeam = "";
|
|
|
|
/** Get the name or members of the "they" team. */
|
|
get theirTeam() {
|
|
return this.#theirTeam;
|
|
}
|
|
|
|
/** Set the name or members of the "they" team. */
|
|
set theirTeam(value) {
|
|
this.#theirTeam = value;
|
|
this.#changed();
|
|
}
|
|
|
|
/** The finished games.
|
|
* @type {Game[]}
|
|
*/
|
|
#games = [];
|
|
|
|
/** Get the finished games.
|
|
*
|
|
* DO NOT write to the returned object.
|
|
*/
|
|
get games() {
|
|
return this.#games;
|
|
}
|
|
|
|
/** The currently played game.
|
|
* @type {?Game}
|
|
*/
|
|
#currentGame = null;
|
|
|
|
/** Get the currently played game. */
|
|
get currentGame() {
|
|
return this.#currentGame;
|
|
}
|
|
|
|
/** Add another round if there is no current one. */
|
|
anotherGame() {
|
|
if (this.#currentGame === null) {
|
|
this.#currentGame = new Game(this.rules);
|
|
this.#currentGame.addEventListener(
|
|
Game.EVENT_CHANGE, this.#boundHandleGameChange);
|
|
this.#changed();
|
|
}
|
|
}
|
|
|
|
/** Get the current amouts of points.
|
|
*
|
|
* Note that on this level points are a punishment.
|
|
*/
|
|
get result() {
|
|
let ourPoints = 0;
|
|
let theirPoints = 0;
|
|
|
|
for (let g of this.#games) {
|
|
let r = g.result;
|
|
if (r.winner === Team.We) {
|
|
theirPoints += r.points;
|
|
} else if (r.winner === Team.They) {
|
|
ourPoints += r.points;
|
|
}
|
|
}
|
|
|
|
return { ourPoints, theirPoints };
|
|
}
|
|
|
|
/** Handle changes to the current game. */
|
|
#handleGameChange() {
|
|
if (this.#currentGame.decided) {
|
|
this.#currentGame.removeEventListener(
|
|
Game.EVENT_CHANGE, this.#boundHandleGameChange);
|
|
this.#games.push(this.#currentGame);
|
|
this.#currentGame = null;
|
|
}
|
|
this.#changed();
|
|
}
|
|
|
|
/** #handleGameChange, but bound to this instance. */
|
|
#boundHandleGameChange = this.#handleGameChange.bind(this);
|
|
|
|
constructor(value) {
|
|
super();
|
|
if (value === undefined) {
|
|
this.#rules.addEventListener(GameRules.EVENT_CHANGE, this.#changed);
|
|
} else if (typeof value === "object") {
|
|
this.#fromStruct(value);
|
|
} else {
|
|
throw new TypeError("unknown form of Session constructor");
|
|
}
|
|
}
|
|
|
|
/** Export the data of this `Session` as a plain JS object with fields.
|
|
*
|
|
* The internals of the returned object are not stabilized, even if they are
|
|
* visible. It should be treated as opaque.
|
|
*
|
|
* There are only two stabile uses of the object:
|
|
* 1. It can be passed to the `Session` constructor as a single argument. The
|
|
* constructor will then create a behaviourally identical instance to the
|
|
* one from which the object was created. This is guaranteed to be
|
|
* backwards compatible, i.e. a revised version of this class can still
|
|
* use the objects created by an older version.
|
|
* 2. It can be stored using IndexedDB.
|
|
*/
|
|
toStruct() {
|
|
let res = {
|
|
rules: this.#rules.toStruct(),
|
|
ourTeam: this.#ourTeam,
|
|
theirTeam: this.#theirTeam,
|
|
games: this.#games.map((g) => g.toStruct()),
|
|
currentGame:
|
|
this.#currentGame !== null ? this.#currentGame.toStruct() : null,
|
|
created: this.#created,
|
|
updated: this.#updated,
|
|
};
|
|
|
|
if (this.#id !== null)
|
|
res.id = this.#id;
|
|
|
|
return res;
|
|
}
|
|
|
|
/** Read in an object created by `Session.toStruct` */
|
|
#fromStruct(value) {
|
|
if (typeof value !== "object")
|
|
throw new TypeError("struct must be an object");
|
|
|
|
if ("id" in value) {
|
|
if (typeof value.id !== "number")
|
|
throw new TypeError("if struct contains id, then it must be a number");
|
|
if (!Number.isInteger(value.id))
|
|
throw new RangeError(
|
|
"if struct contains id, then it must be an integer");
|
|
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("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");
|
|
this.#ourTeam = value.ourTeam;
|
|
|
|
if (typeof value.theirTeam !== "string")
|
|
throw new TypeError("struct must contain theirTeam as string");
|
|
this.#theirTeam = value.theirTeam;
|
|
|
|
if (!("games" in value))
|
|
throw new TypeError("struct must contain games");
|
|
if (!Array.isArray(value.games))
|
|
throw new TypeError("struct must contain games as array");
|
|
this.#games = value.games.map((g) => new Game(g));
|
|
for (let g of this.#games)
|
|
if (g.result.winner === null)
|
|
throw new Error("past games must be finished");
|
|
|
|
if (typeof value.currentGame !== "object")
|
|
throw new TypeError("struct must contain currentGame as object");
|
|
if (value.currentGame !== null) {
|
|
this.#currentGame = new Game(value.currentGame);
|
|
this.#currentGame.addEventListener(
|
|
Game.EVENT_CHANGE, this.#boundHandleGameChange);
|
|
if (this.#currentGame.result.winner !== null)
|
|
throw new Error("currentGame in struct must not be finished");
|
|
}
|
|
|
|
if ("created" in value) {
|
|
if (!(value.created instanceof Date))
|
|
throw new TypeError(
|
|
"if struct contains creation time, it must be a date");
|
|
this.#created = value.created;
|
|
} else
|
|
this.#created = new Date("2026-02-26T22:00:00");
|
|
|
|
if ("updated" in value) {
|
|
if (!(value.updated instanceof Date))
|
|
throw new TypeError(
|
|
"if struct contains update time, it must be a date");
|
|
this.#updated = value.updated;
|
|
} else
|
|
this.#updated = new Date("2026-02-26T22:00:00");
|
|
}
|
|
}
|