"use strict"; /** A specific team. * @enum {number} */ export const Team = Object.freeze({ /** The "we" team, from the perspective of the score keeper. */ We: 1, /** The "they" team, from the perspective of the score keeper. */ They: 2, /** Check if the passed value is a team. * * @param {Team} team The team to check. * @returns Whether the value is a team. */ isTeam(team) { return (team === Team.We) || (team === Team.They); } }); /** A single round of watten. * * A game consists of multiple rounds, for each of which points can be won. * Rounds are mostly independet from each other. The only bleedover is how * often each team can raise the points available. * * This class is specifically meant to represent the current round, and is not * ideal for storing past results. * * This project is not concerned with creating an online version of the game, * the aim is to create a convenient score keeping system. Therefore this class * only implements the raising mechanics, and no actual game play. * * Note that round points are positive, players want to accumulate them. */ export class Round extends EventTarget { /** The event triggered when something about the round changes. */ static get EVENT_CHANGE() { return "wb:round:change"; } /** The maximum the "we" team may raise to. */ #ourLimit = 11; /** The maximum the "they" team may raise to. */ #theirLimit = 11; constructor(value, theyLimit) { super(); if (value === undefined && theyLimit === undefined) { } else if (typeof value === "number" && typeof theyLimit === "number") { if (value < this.#points) throw new RangeError("`ourLimit` must be larger than default points"); if (theyLimit < this.#points) throw new RangeError( "`theirLimit` must be larger than default points"); this.#ourLimit = value; this.#theirLimit = theyLimit; } else if (typeof value === "object" && theyLimit === undefined) { this.#fromStruct(value); } else { throw new TypeError("unknown form of Round constructor"); } } /** How many points the game is worth. */ #points = 2; /** Get how many points the current game is worth. */ get points() { return this.#points; } /** Which team raised last. * @type {?Team} */ #raisedLast = null; /** Who won the round. * @type {?Team} */ #winner = null; /** Get the winner of the round. * * @returns {?Team} The winning team, or `null` if the round is not yet * decided. */ get winner() { return this.#winner; } /** Set the winner of the round. * * @param {Team} team The team that won the round. */ set winner(team) { if (team !== Team.We && team !== Team.They) throw new TypeError("only actual teams can win"); if (this.decided) throw new Error("decided round cannot be won again"); this.#winner = team; this.dispatchEvent(new CustomEvent(Round.EVENT_CHANGE)); } /** Check whether the round has been decided. */ get decided() { return this.#winner !== null; } /** Check whether a team can raise. * * Note that this only checks if the team can raise. It does not check * whether the team may raise. * * @param {Team} team The team to check for. * @returns {boolean} Whether the team can raise. */ canRaise(team) { if (team !== Team.We && team !== Team.They) throw new TypeError("only actual teams can raise"); return !this.decided && this.#raisedLast !== team; } /** A team raises the points. * * Does nothing if the team cannot raise. Ends the round if a team raises * that may not do so. Raises the points otherwise. * * @param {Team} team The team that wishes to raise. */ raise(team) { if (team !== Team.We && team !== Team.They) throw new TypeError("only actual teams can raise"); if (!this.canRaise(team)) return; if (team === Team.We && this.points >= this.#ourLimit) { this.winner = Team.They; return; } if (team === Team.They && this.points >= this.#theirLimit) { this.winner = Team.We; return; } this.#raisedLast = team; this.#points += 1; this.dispatchEvent(new CustomEvent(Round.EVENT_CHANGE)); } /** Export the data of this `Round` 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 `Round` 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() { return { points: this.#points, raisedLast: this.#raisedLast, winner: this.#winner, ourLimit: this.#ourLimit, theirLimit: this.#theirLimit, } } /** Read in an object created by `Round.toStruct` */ #fromStruct(value) { if (typeof value !== "object") throw new TypeError("struct must be an object"); if (typeof value.points !== "number") throw new TypeError("struct must contain points as number"); if (!Number.isInteger(value.points) || value.points < 2) throw new RangeError("struct must contain points >= 2 as integer"); this.#points = value.points; if (!("raisedLast" in value)) throw new TypeError("struct must contain raisedLast"); if (value.raisedLast !== null && !Team.isTeam(value.raisedLast)) throw new TypeError("struct must contain raisedLast as Team or null"); this.#raisedLast = value.raisedLast; if (!("winner" in value)) throw new TypeError("struct must contain winner"); if (value.winner !== null && !Team.isTeam(value.winner)) throw new TypeError("struct must contain winner as Team or null"); this.#winner = value.winner; if (typeof value.ourLimit !== "number") throw new TypeError("struct must contain ourLimit as number"); if (!Number.isInteger(value.ourLimit) || value.ourLimit < 2) throw new RangeError("struct must contain ourLimit >= 2 as integer"); this.#ourLimit = value.ourLimit; if (typeof value.theirLimit !== "number") throw new TypeError("struct must contain theirLimit as number"); if (!Number.isInteger(value.theirLimit) || value.theirLimit < 2) throw new RangeError("struct must contain theirLimit >= 2 as integer"); this.#theirLimit = value.theirLimit; } }