implement game rules model
This commit is contained in:
parent
542af83a5a
commit
14aad4a73d
128
models/game_rules.js
Normal file
128
models/game_rules.js
Normal file
@ -0,0 +1,128 @@
|
||||
"use strict";
|
||||
|
||||
/** Rules for how long teams can raise. */
|
||||
export const RaisingRule = Object.freeze({
|
||||
/** Teams can raìse unless they are stricken.
|
||||
*
|
||||
* This means a team can win until it needs two points or less to win the
|
||||
* game. This corresponds to the starting amount of points.
|
||||
*/
|
||||
UnlessStricken: 1,
|
||||
/** Teams can raise until they would win the game if they won the round. */
|
||||
UntilEnough: 2,
|
||||
|
||||
/** Check if the passed value is a raising rule.
|
||||
*
|
||||
* @param {RaisingRule} rule The rule to check.
|
||||
* @returns {boolean} Whether the value is a raising rule.
|
||||
*/
|
||||
isRaisingRule(rule) {
|
||||
return (rule === RaisingRule.UnlessStricken)
|
||||
|| (rule === RaisingRule.UntilEnough);
|
||||
}
|
||||
});
|
||||
|
||||
/** The rules of a specific game. */
|
||||
export default class GameRules extends EventTarget {
|
||||
/** The event triggered when something about the game rules changes. */
|
||||
static get EVENT_CHANGE() { return "wb:game_rules:change"};
|
||||
|
||||
/** The points target needed to win a round. */
|
||||
#goal = 11;
|
||||
|
||||
/** Get the points target needed to win a round. */
|
||||
get goal() {
|
||||
return this.#goal;
|
||||
}
|
||||
|
||||
/** Set the points target needed to win a round.
|
||||
*
|
||||
* Must be at least 1.
|
||||
*/
|
||||
set goal(value) {
|
||||
if (!Number.isInteger(value))
|
||||
throw new TypeError("goal must be an integer value");
|
||||
if (value < 1)
|
||||
throw new RangeError("goal must be at least one");
|
||||
this.#goal = value;
|
||||
this.dispatchEvent(new CustomEvent(GameRules.EVENT_CHANGE));
|
||||
}
|
||||
|
||||
/** The rules about how long teams can raise. */
|
||||
#raising = RaisingRule.UnlessStricken;
|
||||
|
||||
/** Get the rules about how long teams can raise. */
|
||||
get raising() {
|
||||
return this.#raising;
|
||||
}
|
||||
|
||||
/** Set the rules about how long teams can raise. */
|
||||
set raising(value) {
|
||||
if (!RaisingRule.isRaisingRule(value))
|
||||
throw new TypeError("raising rule must be actual raising rule");
|
||||
this.#raising = value;
|
||||
this.dispatchEvent(new CustomEvent(GameRules.EVENT_CHANGE));
|
||||
}
|
||||
|
||||
constructor(value) {
|
||||
super();
|
||||
if (value === undefined){
|
||||
} else if (value instanceof GameRules) {
|
||||
this.goal = value.goal;
|
||||
this.raising = value.raising;
|
||||
} else if (typeof value === "object") {
|
||||
this.#fromStruct(value);
|
||||
} else {
|
||||
throw new TypeError("unknown form of GameRules constructor");
|
||||
}
|
||||
}
|
||||
|
||||
/** Calculate to what number a team can raise the points.
|
||||
*
|
||||
* @param {number} currentPoints The teams current points.
|
||||
* @returns {number} The target to which the team can raise.
|
||||
*/
|
||||
raisingLimit(currentPoints) {
|
||||
if (this.#raising === RaisingRule.UnlessStricken)
|
||||
return (currentPoints >= (this.#goal - 2)) ? 2 : Number.MAX_SAFE_INTEGER;
|
||||
if (this.#raising === RaisingRule.UntilEnough)
|
||||
return Math.max(this.#goal - currentPoints, 2);
|
||||
throw new TypeError("unknown raising rule");
|
||||
}
|
||||
|
||||
/** Export the data of this `GameRules` 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 `GameRules` 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 {
|
||||
goal: this.#goal,
|
||||
raising: this.#raising,
|
||||
};
|
||||
}
|
||||
|
||||
/** Read in an object created by `GameRules.toStruct` */
|
||||
#fromStruct(value) {
|
||||
if (typeof value !== "object")
|
||||
throw new TypeError("struct must be an object");
|
||||
|
||||
if (typeof value.goal !== "number")
|
||||
throw new TypeError("struct must contain goal as number");
|
||||
if (!Number.isInteger(value.goal) || value.goal < 1)
|
||||
throw new RangeError("struct must contain goal >= 1 as integer");
|
||||
this.#goal = value.goal;
|
||||
|
||||
if (!("raising" in value) || !RaisingRule.isRaisingRule(value.raising))
|
||||
throw new TypeError("struct must contain valid raising rule");
|
||||
this.#raising = value.raising;
|
||||
}
|
||||
}
|
||||
182
models/game_rules.test.js
Normal file
182
models/game_rules.test.js
Normal file
@ -0,0 +1,182 @@
|
||||
"use strict";
|
||||
|
||||
import GameRules, { RaisingRule } from "/models/game_rules.js";
|
||||
|
||||
export default function() {
|
||||
QUnit.module("game_rules", function() {
|
||||
QUnit.test("default constructor", function(assert) {
|
||||
let rules = new GameRules();
|
||||
assert.strictEqual(rules.goal, 11, "initial goal");
|
||||
assert.strictEqual(
|
||||
rules.raising, RaisingRule.UnlessStricken, "initial raising rule");
|
||||
});
|
||||
|
||||
QUnit.test("copy constructor", function(assert) {
|
||||
let first = new GameRules();
|
||||
first.goal = 5;
|
||||
first.raising = RaisingRule.UntilEnough;
|
||||
|
||||
let second = new GameRules(first);
|
||||
assert.strictEqual(second.goal, first.goal, "copied goal");
|
||||
assert.strictEqual(second.raising, first.raising, "copied raising rule");
|
||||
});
|
||||
|
||||
QUnit.test("invalid constructor", function(assert) {
|
||||
assert.throws(
|
||||
function() { new GameRules("nope"); },
|
||||
new TypeError("unknown form of GameRules constructor"));
|
||||
});
|
||||
|
||||
QUnit.test("setting goal", function(assert) {
|
||||
let rules = new GameRules();
|
||||
rules.addEventListener(
|
||||
GameRules.EVENT_CHANGE, () => assert.step("change"));
|
||||
|
||||
rules.goal = 15;
|
||||
assert.strictEqual(rules.goal, 15, "correct goal");
|
||||
assert.verifySteps(["change"], "change triggered");
|
||||
|
||||
assert.throws(
|
||||
() => rules.goal = true,
|
||||
new TypeError("goal must be an integer value"),
|
||||
"bool goal");
|
||||
|
||||
assert.throws(
|
||||
() => rules.goal = 0,
|
||||
new RangeError("goal must be at least one"),
|
||||
"zero goal");
|
||||
|
||||
assert.throws(
|
||||
() => rules.goal = -15,
|
||||
new RangeError("goal must be at least one"),
|
||||
"negative goal");
|
||||
|
||||
rules.goal = 1;
|
||||
assert.strictEqual(rules.goal, 1, "one goal");
|
||||
assert.verifySteps(["change"], "change triggered");
|
||||
});
|
||||
|
||||
QUnit.test("setting raising rule", function(assert) {
|
||||
let rules = new GameRules();
|
||||
rules.addEventListener(
|
||||
GameRules.EVENT_CHANGE, () => assert.step("change"));
|
||||
|
||||
rules.raising = RaisingRule.UntilEnough;
|
||||
assert.strictEqual(
|
||||
rules.raising, RaisingRule.UntilEnough, "until enough");
|
||||
assert.verifySteps(["change"], "change triggered");
|
||||
|
||||
rules.raising = RaisingRule.UnlessStricken;
|
||||
assert.strictEqual(
|
||||
rules.raising, RaisingRule.UnlessStricken, "unless sticken");
|
||||
assert.verifySteps(["change"], "change triggered");
|
||||
|
||||
assert.throws(
|
||||
() => rules.raising = 5,
|
||||
new TypeError("raising rule must be actual raising rule"),
|
||||
"integer raising rule");
|
||||
|
||||
assert.throws(
|
||||
() => rules.raising = true,
|
||||
new TypeError("raising rule must be actual raising rule"),
|
||||
"boolean raising rule");
|
||||
});
|
||||
|
||||
QUnit.test.each(
|
||||
"raisingLimit - unless stricken",
|
||||
[
|
||||
{ rule: RaisingRule.UnlessStricken, goal: 2, points: 0, limit: 2 },
|
||||
{ rule: RaisingRule.UnlessStricken, goal: 3, points: 0 },
|
||||
{ rule: RaisingRule.UnlessStricken, goal: 11, points: 0 },
|
||||
{ rule: RaisingRule.UnlessStricken, goal: 11, points: 2 },
|
||||
{ rule: RaisingRule.UnlessStricken, goal: 11, points: 9, limit: 2 },
|
||||
{ rule: RaisingRule.UnlessStricken, goal: 11, points: 11, limit: 2 },
|
||||
{ rule: RaisingRule.UnlessStricken, goal: 11, points: 13, limit: 2 },
|
||||
{ rule: RaisingRule.UntilEnough, goal: 2, points: 0, limit: 2 },
|
||||
{ rule: RaisingRule.UntilEnough, goal: 11, points: 0, limit: 11 },
|
||||
{ rule: RaisingRule.UntilEnough, goal: 11, points: 2, limit: 9 },
|
||||
{ rule: RaisingRule.UntilEnough, goal: 11, points: 9, limit: 2 },
|
||||
{ rule: RaisingRule.UntilEnough, goal: 11, points: 11, limit: 2 },
|
||||
{ rule: RaisingRule.UntilEnough, goal: 11, points: 13, limit: 2 },
|
||||
],
|
||||
function(assert, input) {
|
||||
let rules = new GameRules();
|
||||
rules.goal = input.goal;
|
||||
rules.raising = input.rule;
|
||||
assert.strictEqual(
|
||||
rules.raisingLimit(input.points),
|
||||
input.limit ?? Number.MAX_SAFE_INTEGER,
|
||||
"correct limit");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("toStruct", function(assert) {
|
||||
let rules = new GameRules();
|
||||
let struct = rules.toStruct();
|
||||
|
||||
let expected = {
|
||||
goal: 11,
|
||||
raising: RaisingRule.UnlessStricken,
|
||||
};
|
||||
|
||||
assert.deepEqual(struct, expected, "successfull structurizing");
|
||||
});
|
||||
|
||||
QUnit.test.each(
|
||||
"fromStruct",
|
||||
{
|
||||
"v1": { goal: 15, raising: RaisingRule.UntilEnough },
|
||||
},
|
||||
function(assert, input) {
|
||||
let rules = new GameRules(input);
|
||||
let expected = {
|
||||
goal: 15,
|
||||
raising: RaisingRule.UntilEnough,
|
||||
};
|
||||
assert.deepEqual(rules.toStruct(), expected, "reexport matches");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test.each(
|
||||
"invalid fromStruct",
|
||||
{
|
||||
"no goal": {
|
||||
struct: { },
|
||||
error: new TypeError("struct must contain goal as number"),
|
||||
},
|
||||
"boolean goal": {
|
||||
struct: { goal: true },
|
||||
error: new TypeError("struct must contain goal as number"),
|
||||
},
|
||||
"non-integer goal": {
|
||||
struct: { goal: 1.5 },
|
||||
error: new RangeError("struct must contain goal >= 1 as integer"),
|
||||
},
|
||||
"zero goal": {
|
||||
struct: { goal: 0 },
|
||||
error: new RangeError("struct must contain goal >= 1 as integer"),
|
||||
},
|
||||
"negative goal": {
|
||||
struct: { goal: -15 },
|
||||
error: new RangeError("struct must contain goal >= 1 as integer"),
|
||||
},
|
||||
"no raising rule": {
|
||||
struct: { goal: 2 },
|
||||
error: new TypeError("struct must contain valid raising rule"),
|
||||
},
|
||||
"boolean raising rule": {
|
||||
struct: { goal: 2, raising: true },
|
||||
error: new TypeError("struct must contain valid raising rule"),
|
||||
},
|
||||
"integer raising rule": {
|
||||
struct: { goal: 2, raising: 5 },
|
||||
error: new TypeError("struct must contain valid raising rule"),
|
||||
},
|
||||
},
|
||||
function(assert, input) {
|
||||
assert.throws(
|
||||
() => new GameRules(input.struct), input.error, "correct error");
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
2
test.js
2
test.js
@ -2,6 +2,7 @@
|
||||
|
||||
import round from "/models/round.test.js";
|
||||
import roundResult from "/models/round_result.test.js";
|
||||
import gameRules from "/models/game_rules.test.js";
|
||||
import game from "/models/game.test.js";
|
||||
import session from "/models/session.test.js";
|
||||
|
||||
@ -11,6 +12,7 @@ import session_repo from "/data/session_repo.test.js";
|
||||
QUnit.module("models", function() {
|
||||
round();
|
||||
roundResult();
|
||||
gameRules();
|
||||
game();
|
||||
session();
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user