There are two reasons for this: 1. I've reconsidered my original plan to store the past games in localStorage, because that would make it difficult to display them in historical order, and would necessitate more complex logic for updating and removing sessions. 2. I've been unhappy with how I did the testing of the serialization and deserialization logic. So I redid it, and now I'm satisfied with it. I've noticed that the testing methodology for the invalid fromStruct method tests is not fully sound. If a check is accidentally removed that test would not detect that, as long as it is not the very last. That is because then the next error triggers. Therefore that will need to be revisited.
214 lines
6.5 KiB
JavaScript
214 lines
6.5 KiB
JavaScript
"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.
|
|
*/
|
|
export class Round extends EventTarget {
|
|
/** The event triggered when the round is won. */
|
|
static winEvent= "roundWon";
|
|
|
|
/** The maximum the "we" team may raise to.
|
|
*
|
|
* @todo rename to ourLimit
|
|
*/
|
|
#weLimit = 11;
|
|
/** The maximum the "they" team may raise to.
|
|
*
|
|
* @todo rename to theirLimit
|
|
*/
|
|
#theyLimit = 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("`weLimit` must be larger than default points");
|
|
if (theyLimit < this.#points)
|
|
throw new RangeError("`theyLimit` must be larger than default points");
|
|
this.#weLimit = value;
|
|
this.#theyLimit = theyLimit;
|
|
} else if (typeof value === "object" && theyLimit === undefined) {
|
|
this.#fromStruct(value);
|
|
} else {
|
|
throw new TypeError("unknown form for 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.winEvent));
|
|
}
|
|
|
|
/** 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.#weLimit) {
|
|
this.winner = Team.They;
|
|
return;
|
|
}
|
|
|
|
if (team === Team.They && this.points >= this.#theyLimit) {
|
|
this.winner = Team.We;
|
|
return;
|
|
}
|
|
|
|
this.#raisedLast = team;
|
|
this.#points += 1;
|
|
}
|
|
|
|
/** 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.#weLimit,
|
|
theirLimit: this.#theyLimit,
|
|
}
|
|
}
|
|
|
|
/** 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.#weLimit = 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.#theyLimit = value.theirLimit;
|
|
}
|
|
}
|