bring server files into project.
This commit is contained in:
32
src/lib/server/test/Game.test.ts
Normal file
32
src/lib/server/test/Game.test.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { Game } from "../../Game";
|
||||
import { deepEqual, ok, throws } from "node:assert/strict";
|
||||
|
||||
describe("Game", () => {
|
||||
describe("addPlayer", () => {
|
||||
it("should push a player id into the player array", () => {
|
||||
const game = new Game();
|
||||
deepEqual(game.players, []);
|
||||
|
||||
game.addPlayer("some-id");
|
||||
deepEqual(game.players, ["some-id"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
it("start shoud start the game", () => {
|
||||
const game = new Game();
|
||||
game.isStarted = false;
|
||||
|
||||
game.start();
|
||||
ok(game.isStarted);
|
||||
});
|
||||
|
||||
it("start should throw if the game is already started", () => {
|
||||
const game = new Game();
|
||||
|
||||
game.start();
|
||||
throws(() => game.start());
|
||||
});
|
||||
});
|
||||
});
|
50
src/lib/server/test/GameData.test.ts
Normal file
50
src/lib/server/test/GameData.test.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { GameData, isGameData } from "../../GameData";
|
||||
import { equal, ok } from "node:assert/strict";
|
||||
|
||||
describe("GameData", () => {
|
||||
describe("isGameData", () => {
|
||||
it("rejects a malformed object", () => {
|
||||
let data: unknown = {
|
||||
players: ["id", 3],
|
||||
isStarted: false,
|
||||
state: {},
|
||||
};
|
||||
equal(isGameData(data), false);
|
||||
|
||||
data = {
|
||||
players: ["id"],
|
||||
isStarted: null,
|
||||
state: {},
|
||||
};
|
||||
equal(isGameData(data), false);
|
||||
|
||||
data = {
|
||||
players: ["id"],
|
||||
isStarted: false,
|
||||
};
|
||||
equal(isGameData(data), false);
|
||||
});
|
||||
|
||||
it("rejects an object with extra properties", () => {
|
||||
const data: GameData & { extra: boolean } = {
|
||||
players: ["id"],
|
||||
isStarted: false,
|
||||
state: {},
|
||||
extra: true,
|
||||
};
|
||||
|
||||
equal(isGameData(data), false);
|
||||
});
|
||||
|
||||
it("should accept a proper GameData object", () => {
|
||||
const data: GameData = {
|
||||
players: ["id"],
|
||||
state: {},
|
||||
isStarted: false,
|
||||
};
|
||||
|
||||
ok(isGameData(data));
|
||||
});
|
||||
});
|
||||
});
|
991
src/lib/server/test/GameEvent.test.ts
Normal file
991
src/lib/server/test/GameEvent.test.ts
Normal file
@ -0,0 +1,991 @@
|
||||
import {
|
||||
FIRST_ROLL_LOST,
|
||||
FIRST_ROLL_PENDING,
|
||||
GameEventKind,
|
||||
getGameEvent,
|
||||
Hold,
|
||||
isGameEventData,
|
||||
Roll,
|
||||
RollForFirst,
|
||||
Score,
|
||||
SeatPlayers,
|
||||
} from "../../GameEvent";
|
||||
import type { GameEventData } from "../../GameEvent";
|
||||
import type { GameData } from "../../GameData";
|
||||
import { describe, it } from "node:test";
|
||||
import type { State } from "../../State";
|
||||
import { doesNotThrow, deepStrictEqual, equal, ok, throws } from "assert";
|
||||
|
||||
describe("Game Events", () => {
|
||||
describe("isGameEventData", () => {
|
||||
it("should return false if the target is not an object", () => {
|
||||
// const target = {
|
||||
// kind: GameEventKind.Hold,
|
||||
// player: 0,
|
||||
// value: [1, 4],
|
||||
// };
|
||||
|
||||
equal(isGameEventData("target"), false);
|
||||
});
|
||||
|
||||
it("should return false if the target has no kind", () => {
|
||||
const target = {
|
||||
player: 0,
|
||||
value: [1, 4],
|
||||
};
|
||||
|
||||
equal(isGameEventData(target), false);
|
||||
});
|
||||
|
||||
it("should return false if the target has uknown keys", () => {
|
||||
const target = {
|
||||
kind: GameEventKind.Hold,
|
||||
player: 0,
|
||||
value: [1, 4],
|
||||
isWeird: true,
|
||||
};
|
||||
|
||||
equal(isGameEventData(target), false);
|
||||
});
|
||||
|
||||
it("should return true of the target is a GameEventData", () => {
|
||||
const target = {
|
||||
kind: GameEventKind.Hold,
|
||||
player: 0,
|
||||
value: [1, 4],
|
||||
};
|
||||
|
||||
ok(isGameEventData(target));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getGameEvent", () => {
|
||||
it("should throw if the kind is unkown", () => {
|
||||
const data: GameData = {
|
||||
isStarted: false,
|
||||
players: ["42", "1,"],
|
||||
state: {},
|
||||
};
|
||||
|
||||
const event: GameEventData = {
|
||||
kind: "GameEventKind",
|
||||
player: 0,
|
||||
value: [1, 2],
|
||||
};
|
||||
|
||||
throws(() => getGameEvent(data, event));
|
||||
});
|
||||
|
||||
it("should throw when SeatPlayers has the wrong number of players", () => {
|
||||
const data: GameData = {
|
||||
isStarted: true,
|
||||
players: ["42", "1,"],
|
||||
state: {},
|
||||
};
|
||||
|
||||
const event: GameEventData = {
|
||||
kind: GameEventKind.SeatPlayers,
|
||||
value: 3,
|
||||
};
|
||||
|
||||
throws(() => getGameEvent(data, event));
|
||||
});
|
||||
|
||||
it("should return a SeatPlayers object when the number of players is correct", () => {
|
||||
const data: GameData = {
|
||||
isStarted: true,
|
||||
players: ["42", "1,"],
|
||||
state: {},
|
||||
};
|
||||
|
||||
const event: GameEventData = {
|
||||
kind: GameEventKind.SeatPlayers,
|
||||
value: 2,
|
||||
};
|
||||
|
||||
ok(getGameEvent(data, event) instanceof SeatPlayers);
|
||||
});
|
||||
|
||||
it("should throw an error if the player passes a full roll with Roll", () => {
|
||||
const data: GameData = {
|
||||
isStarted: true,
|
||||
players: ["42", "1,"],
|
||||
state: {},
|
||||
};
|
||||
|
||||
const event: GameEventData = {
|
||||
kind: GameEventKind.Roll,
|
||||
player: 0,
|
||||
value: [1, 2, 3, 4, 5, 6],
|
||||
};
|
||||
|
||||
throws(() => getGameEvent(data, event));
|
||||
});
|
||||
|
||||
it("should return a Roll object with dice values when the player passes a die count as a value", () => {
|
||||
const data: GameData = {
|
||||
isStarted: true,
|
||||
players: ["42", "1,"],
|
||||
state: {},
|
||||
};
|
||||
|
||||
const event: GameEventData = {
|
||||
kind: GameEventKind.Roll,
|
||||
player: 0,
|
||||
value: 4,
|
||||
};
|
||||
|
||||
const roll = getGameEvent(data, event);
|
||||
|
||||
ok(roll instanceof Roll);
|
||||
ok(Array.isArray(roll.value));
|
||||
equal(roll.value.length, 4);
|
||||
});
|
||||
|
||||
it("should return the class that corresponds with a given kind", () => {
|
||||
const data: GameData = {
|
||||
isStarted: true,
|
||||
players: ["42", "1,"],
|
||||
state: {},
|
||||
};
|
||||
|
||||
const event: GameEventData = {
|
||||
kind: "",
|
||||
player: 0,
|
||||
value: 4,
|
||||
};
|
||||
|
||||
const rollForFirst = getGameEvent(data, {
|
||||
...event,
|
||||
kind: GameEventKind.RollForFirst,
|
||||
});
|
||||
|
||||
const hold = getGameEvent(data, {
|
||||
...event,
|
||||
value: [0, 1, 3],
|
||||
kind: GameEventKind.Hold,
|
||||
});
|
||||
|
||||
const score = getGameEvent(data, {
|
||||
...event,
|
||||
value: 2_000,
|
||||
kind: GameEventKind.Score,
|
||||
});
|
||||
|
||||
ok(rollForFirst instanceof RollForFirst);
|
||||
ok(hold instanceof Hold);
|
||||
ok(score instanceof Score);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SeatPlayers", () => {
|
||||
describe("constructor", () => {
|
||||
it("should throw when value is not a number", () => {
|
||||
throws(() => new SeatPlayers({ kind: GameEventKind.SeatPlayers, value: [1] }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("run", () => {
|
||||
it("should throw if the game is over", () => {
|
||||
const ev = new SeatPlayers({ kind: GameEventKind.SeatPlayers, value: 3 });
|
||||
const state: State = { turnCountdown: 0 };
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if players are already seated", () => {
|
||||
const ev = new SeatPlayers({ kind: GameEventKind.SeatPlayers, value: 3 });
|
||||
const state: State = {};
|
||||
|
||||
doesNotThrow(() => ev.run(state), "should not throw before players are seated");
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should seat the number of players provided", () => {
|
||||
const ev = new SeatPlayers({ kind: GameEventKind.SeatPlayers, value: 4 });
|
||||
const state: State = {};
|
||||
|
||||
ev.run(state);
|
||||
deepStrictEqual(state.scores, [0, 0, 0, 0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Some of these tests rely on implementation details in an undesirable way.
|
||||
// It might be better to add some features to State so that we can examine it
|
||||
// and understand its meaning without having to understand all the gritty
|
||||
// details.
|
||||
describe("RollForFirst", () => {
|
||||
describe("constructor", () => {
|
||||
it("should throw if player is missing", () => {
|
||||
throws(() => new RollForFirst({ kind: GameEventKind.RollForFirst, value: 5 }));
|
||||
});
|
||||
|
||||
it("should throw if the value is not a number", () => {
|
||||
throws(
|
||||
() =>
|
||||
new RollForFirst({ kind: GameEventKind.RollForFirst, player: 0, value: [4] }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("run", () => {
|
||||
it("should throw if the game is over", () => {
|
||||
const ev = new RollForFirst({
|
||||
kind: GameEventKind.RollForFirst,
|
||||
player: 0,
|
||||
value: 3,
|
||||
});
|
||||
const state: State = {
|
||||
scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING],
|
||||
turnCountdown: 0,
|
||||
};
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the game has already started", () => {
|
||||
const ev = new RollForFirst({
|
||||
kind: GameEventKind.RollForFirst,
|
||||
player: 0,
|
||||
value: 3,
|
||||
});
|
||||
const state: State = {
|
||||
scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING],
|
||||
playing: 0,
|
||||
};
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the player index is out of bounds", () => {
|
||||
const ev = new RollForFirst({
|
||||
kind: GameEventKind.RollForFirst,
|
||||
player: 2,
|
||||
value: 3,
|
||||
});
|
||||
const state: State = { scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING] };
|
||||
|
||||
throws(() => ev.run(state));
|
||||
console.log("done");
|
||||
});
|
||||
|
||||
it("should throw if the player has already rolled", () => {
|
||||
const ev = new RollForFirst({
|
||||
kind: GameEventKind.RollForFirst,
|
||||
player: 1,
|
||||
value: 3,
|
||||
});
|
||||
const state: State = { scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING] };
|
||||
|
||||
doesNotThrow(() => ev.run(state));
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should set the players score to match the dice roll", () => {
|
||||
const ev = new RollForFirst({
|
||||
kind: GameEventKind.RollForFirst,
|
||||
player: 1,
|
||||
value: 3,
|
||||
});
|
||||
const state: State = { scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING] };
|
||||
|
||||
ev.run(state);
|
||||
deepStrictEqual(state, { scores: [FIRST_ROLL_PENDING, 3] });
|
||||
});
|
||||
|
||||
it("should reset the scores and set the winning player when everyone has rolled", () => {
|
||||
const state: State = {
|
||||
scores: [
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
],
|
||||
};
|
||||
|
||||
let ev = new RollForFirst({
|
||||
kind: GameEventKind.RollForFirst,
|
||||
player: 1,
|
||||
value: 4,
|
||||
});
|
||||
ev.run(state);
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 0, value: 3 });
|
||||
ev.run(state);
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 2, value: 6 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, { scores: [3, 4, 6, 0] });
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 3, value: 4 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 6,
|
||||
scores: [
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
],
|
||||
playing: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("should reset tied players for tie breaker", () => {
|
||||
const state: State = {
|
||||
scores: [
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
],
|
||||
};
|
||||
|
||||
let ev = new RollForFirst({
|
||||
kind: GameEventKind.RollForFirst,
|
||||
player: 3,
|
||||
value: 5,
|
||||
});
|
||||
ev.run(state);
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 0, value: 5 });
|
||||
ev.run(state);
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 1, value: 3 });
|
||||
ev.run(state);
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 2, value: 1 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
scores: [
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_LOST,
|
||||
FIRST_ROLL_LOST,
|
||||
FIRST_ROLL_PENDING,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw if a player whose lost tries to roll again", () => {
|
||||
const state: State = {
|
||||
scores: [
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_LOST,
|
||||
FIRST_ROLL_LOST,
|
||||
FIRST_ROLL_PENDING,
|
||||
],
|
||||
};
|
||||
|
||||
const ev = new RollForFirst({
|
||||
kind: GameEventKind.RollForFirst,
|
||||
player: 1,
|
||||
value: 3,
|
||||
});
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should allow tied players to keep rolling until somoene wins", () => {
|
||||
const state: State = {
|
||||
scores: [
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_LOST,
|
||||
FIRST_ROLL_PENDING,
|
||||
],
|
||||
};
|
||||
|
||||
// simulate another 3-way tie
|
||||
let ev = new RollForFirst({
|
||||
kind: GameEventKind.RollForFirst,
|
||||
player: 0,
|
||||
value: 5,
|
||||
});
|
||||
ev.run(state);
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 1, value: 5 });
|
||||
ev.run(state);
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 3, value: 5 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(
|
||||
state,
|
||||
{
|
||||
scores: [
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_LOST,
|
||||
FIRST_ROLL_PENDING,
|
||||
],
|
||||
},
|
||||
"shouldn't change in a 3-way tie",
|
||||
);
|
||||
|
||||
// simulate a 2-way tie
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 0, value: 2 });
|
||||
ev.run(state);
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 1, value: 1 });
|
||||
ev.run(state);
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 3, value: 2 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(
|
||||
state,
|
||||
{
|
||||
scores: [
|
||||
FIRST_ROLL_PENDING,
|
||||
FIRST_ROLL_LOST,
|
||||
FIRST_ROLL_LOST,
|
||||
FIRST_ROLL_PENDING,
|
||||
],
|
||||
},
|
||||
"should update for a smaller tie",
|
||||
);
|
||||
|
||||
// finally find a winner
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 0, value: 3 });
|
||||
ev.run(state);
|
||||
|
||||
ev = new RollForFirst({ kind: GameEventKind.RollForFirst, player: 3, value: 1 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(
|
||||
state,
|
||||
{
|
||||
dieCount: 6,
|
||||
scores: [0, 0, 0, 0],
|
||||
playing: 0,
|
||||
},
|
||||
"should finally select a winner",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: It's very hard to tell if these tests are throwing for the right reason.
|
||||
// Some system should be devised for making sure that the correct errors are
|
||||
// being thrown.
|
||||
describe("Roll", () => {
|
||||
describe("constructor", () => {
|
||||
it("should throw if player is missing", () => {
|
||||
throws(() => new Roll({ kind: GameEventKind.Roll, value: 5 }));
|
||||
});
|
||||
|
||||
it("should throw if the value is not a number array", () => {
|
||||
throws(() => new Roll({ kind: GameEventKind.Roll, player: 0, value: 4 }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("run", () => {
|
||||
it("should throw if the game is over", () => {
|
||||
const state: State = {
|
||||
scores: [0, 0],
|
||||
dieCount: 6,
|
||||
playing: 0,
|
||||
turnCountdown: 0,
|
||||
};
|
||||
|
||||
const ev = new Roll({
|
||||
kind: GameEventKind.Roll,
|
||||
player: 0,
|
||||
value: [1, 2, 3, 4, 5, 6],
|
||||
});
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if it's not the player's turn", () => {
|
||||
const state: State = { scores: [0, 0], dieCount: 6, playing: 1 };
|
||||
const ev = new Roll({
|
||||
kind: GameEventKind.Roll,
|
||||
player: 0,
|
||||
value: [1, 2, 3, 4, 5, 6],
|
||||
});
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the player index is out of bounds", () => {
|
||||
const state: State = { scores: [0, 0], dieCount: 6, playing: 0 };
|
||||
const ev = new Roll({
|
||||
kind: GameEventKind.Roll,
|
||||
player: 3,
|
||||
value: [1, 2, 3, 4, 5, 6],
|
||||
});
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the player is rolling more dice than they have", () => {
|
||||
const state: State = { scores: [0, 0], dieCount: 4, playing: 0 };
|
||||
const ev = new Roll({
|
||||
kind: GameEventKind.Roll,
|
||||
player: 0,
|
||||
value: [1, 2, 3, 4, 5, 6],
|
||||
});
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the player has already rolled", () => {
|
||||
const state: State = {
|
||||
scores: [0, 0],
|
||||
dieCount: 6,
|
||||
playing: 0,
|
||||
dice: [1, 2, 3, 4, 5, 6],
|
||||
};
|
||||
|
||||
const ev = new Roll({
|
||||
kind: GameEventKind.Roll,
|
||||
player: 0,
|
||||
value: [1, 2, 3, 4, 5, 6],
|
||||
});
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should set the dice when the player rolls", () => {
|
||||
const state: State = {
|
||||
scores: [0, 0],
|
||||
dieCount: 4,
|
||||
playing: 0,
|
||||
};
|
||||
|
||||
const ev = new Roll({ kind: GameEventKind.Roll, player: 0, value: [1, 2, 3, 4] });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
scores: [0, 0],
|
||||
playing: 0,
|
||||
dice: [1, 2, 3, 4],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Hold", () => {
|
||||
describe("constructor", () => {
|
||||
it("should throw if player missing", () => {
|
||||
throws(() => new Hold({ kind: GameEventKind.Hold, value: [0] }));
|
||||
});
|
||||
|
||||
it("should throw if the value is not a number array", () => {
|
||||
throws(() => new Hold({ kind: GameEventKind.Hold, player: 0, value: 3 }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("run", () => {
|
||||
it("should throw if the game is over", () => {
|
||||
const state: State = {
|
||||
scores: [0, 0, 0],
|
||||
playing: 0,
|
||||
dice: [1, 1, 5, 3],
|
||||
turnCountdown: 0,
|
||||
};
|
||||
const ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [0, 1, 2] });
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if it is not the player's turn", () => {
|
||||
const state: State = {
|
||||
scores: [0, 0, 0],
|
||||
playing: 1,
|
||||
dice: [1, 1, 5, 3],
|
||||
};
|
||||
const ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [0, 1, 2] });
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the player has not rolled yet", () => {
|
||||
const state: State = {
|
||||
scores: [0, 0, 0],
|
||||
playing: 0,
|
||||
};
|
||||
const ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [0, 1, 2] });
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the player is trying to hold no dice", () => {
|
||||
const state: State = {
|
||||
scores: [0, 0, 0],
|
||||
playing: 0,
|
||||
dice: [1, 1, 5, 3],
|
||||
};
|
||||
const ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [] });
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the player is trying to hold non-existent dice", () => {
|
||||
const state: State = {
|
||||
scores: [0, 0, 0],
|
||||
playing: 0,
|
||||
dice: [1, 1, 5, 3],
|
||||
};
|
||||
const ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [0, 2, 9] });
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the player is trying to hold non-scoring, non-push dice", () => {
|
||||
const state: State = {
|
||||
scores: [0, 0, 0],
|
||||
playing: 0,
|
||||
dice: [1, 1, 5, 3],
|
||||
};
|
||||
const ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [0, 2, 3] });
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should allow the player to hold ones for 100", () => {
|
||||
let state: State = {
|
||||
dice: [1, 1, 5, 3],
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
let ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [0] });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 3,
|
||||
heldScore: 100,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
});
|
||||
|
||||
state = {
|
||||
dice: [1, 1, 5, 3],
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [0, 1] });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 2,
|
||||
heldScore: 200,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow the player to hold fives for 50", () => {
|
||||
let state: State = {
|
||||
dice: [5, 5, 5, 3],
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
let ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [0] });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 3,
|
||||
heldScore: 50,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
});
|
||||
|
||||
state = {
|
||||
dice: [5, 5, 5, 3],
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [0, 1] });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 2,
|
||||
heldScore: 100,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow the player to hold three of a kind", () => {
|
||||
testNOfAKind(3);
|
||||
});
|
||||
|
||||
it("should allow the player to hold four of a kind", () => {
|
||||
testNOfAKind(4);
|
||||
});
|
||||
|
||||
it("should allow the player to hold five of a kind", () => {
|
||||
testNOfAKind(5);
|
||||
});
|
||||
|
||||
it("should allow the player to hold six of a kind", () => {
|
||||
testNOfAKind(6);
|
||||
});
|
||||
|
||||
it("should allow the player to hold a run of six for 2,000 points", () => {
|
||||
const state: State = {
|
||||
dice: [1, 2, 3, 4, 5, 6],
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
const ev = new Hold({
|
||||
kind: GameEventKind.Hold,
|
||||
player: 0,
|
||||
value: [1, 0, 2, 5, 4, 3],
|
||||
});
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 6,
|
||||
heldScore: 2_000,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow the player to hold 2 threes of a kind for 1,500 points", () => {
|
||||
const state: State = {
|
||||
dice: [2, 2, 2, 3, 3, 3],
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
const ev = new Hold({
|
||||
kind: GameEventKind.Hold,
|
||||
player: 0,
|
||||
value: [0, 1, 2, 3, 4, 5],
|
||||
});
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 6,
|
||||
heldScore: 1_500,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
it("shoud allow the player to re-roll on a push", () => {
|
||||
const state: State = {
|
||||
dice: [2, 2],
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
const ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: [0, 1] });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 6,
|
||||
heldScore: 0,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Score", () => {
|
||||
describe("constructor", () => {
|
||||
it("should throw if player missing", () => {
|
||||
throws(() => new Score({ kind: GameEventKind.Score, value: 200 }));
|
||||
});
|
||||
|
||||
it("should throw if the value is not a number", () => {
|
||||
throws(() => new Score({ kind: GameEventKind.Hold, player: 0, value: [200] }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("run", () => {
|
||||
it("should throw if the game is over", () => {
|
||||
const state: State = {
|
||||
dieCount: 4,
|
||||
heldScore: 200,
|
||||
playing: 0,
|
||||
turnCountdown: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
const ev = new Score({ kind: GameEventKind.Score, player: 0, value: 200 });
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if it's not the players turn", () => {
|
||||
const state: State = {
|
||||
dieCount: 4,
|
||||
heldScore: 200,
|
||||
playing: 1,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
const ev = new Score({ kind: GameEventKind.Score, player: 0, value: 200 });
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the player is trying to score with 6 dice available to roll", () => {
|
||||
const state: State = {
|
||||
dieCount: 6,
|
||||
heldScore: 200,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
const ev = new Score({ kind: GameEventKind.Score, player: 0, value: 200 });
|
||||
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should throw if the player is trying to score a different value than the held score", () => {
|
||||
const state: State = {
|
||||
dieCount: 4,
|
||||
heldScore: 200,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
const ev = new Score({ kind: GameEventKind.Score, player: 0, value: 250 });
|
||||
throws(() => ev.run(state));
|
||||
});
|
||||
|
||||
it("should add the score to the players score and activate the next player", () => {
|
||||
let state: State = {
|
||||
dieCount: 4,
|
||||
heldScore: 200,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
let ev = new Score({ kind: GameEventKind.Score, player: 0, value: 200 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 6,
|
||||
playing: 1,
|
||||
scores: [200, 0, 0],
|
||||
});
|
||||
|
||||
state = { ...state, heldScore: 300, dieCount: 3 };
|
||||
ev = new Score({ kind: GameEventKind.Score, player: 1, value: 300 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 6,
|
||||
playing: 2,
|
||||
scores: [200, 300, 0],
|
||||
});
|
||||
|
||||
state = { ...state, heldScore: 400, dieCount: 3 };
|
||||
ev = new Score({ kind: GameEventKind.Score, player: 2, value: 400 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 6,
|
||||
playing: 0,
|
||||
scores: [200, 300, 400],
|
||||
});
|
||||
});
|
||||
|
||||
it("should begin the turn countdown when a player crosses 10,000 points", () => {
|
||||
const state: State = {
|
||||
dieCount: 4,
|
||||
heldScore: 2_000,
|
||||
playing: 0,
|
||||
scores: [9_000, 0, 0],
|
||||
};
|
||||
|
||||
const ev = new Score({ kind: GameEventKind.Score, player: 0, value: 2_000 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 6,
|
||||
playing: 1,
|
||||
turnCountdown: 2,
|
||||
scores: [11_000, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
it("should decrement the turn countdown when it is active", () => {
|
||||
const state: State = {
|
||||
dieCount: 4,
|
||||
heldScore: 2_000,
|
||||
turnCountdown: 2,
|
||||
playing: 1,
|
||||
scores: [10_000, 0, 0],
|
||||
};
|
||||
|
||||
const ev = new Score({ kind: GameEventKind.Score, player: 1, value: 2_000 });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(state, {
|
||||
dieCount: 6,
|
||||
playing: 2,
|
||||
turnCountdown: 1,
|
||||
scores: [10_000, 2_000, 0],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function testNOfAKind(n: number) {
|
||||
// Each of these arrays starts with some three of a kind.
|
||||
const nOfAKind: number[][] = [];
|
||||
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
nOfAKind.push(new Array(n).fill(i));
|
||||
}
|
||||
|
||||
// Check each of these arrays.
|
||||
for (const roll of nOfAKind) {
|
||||
// The first value in each array is part of the three of a kind.
|
||||
const value = roll[0];
|
||||
let expectedValue: number;
|
||||
|
||||
const state: State = {
|
||||
dice: roll,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
};
|
||||
|
||||
if (value === 1) {
|
||||
// Ones are treated like tens, so three ones is 1,000.
|
||||
expectedValue = 1_000 * (n - 2);
|
||||
} else {
|
||||
// All other values are treated as themselves, and three of a kind
|
||||
// is the number of pips multiplied by 100.
|
||||
expectedValue = value * 100 * (n - 2);
|
||||
}
|
||||
|
||||
// Generate an array of the first n indexes.
|
||||
const holdValue: number[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
holdValue.push(i);
|
||||
}
|
||||
|
||||
const ev = new Hold({ kind: GameEventKind.Hold, player: 0, value: holdValue });
|
||||
ev.run(state);
|
||||
|
||||
deepStrictEqual(
|
||||
state,
|
||||
{
|
||||
dieCount: 6,
|
||||
heldScore: expectedValue,
|
||||
playing: 0,
|
||||
scores: [0, 0, 0],
|
||||
},
|
||||
`expected ${n} ${value}s to be worth ${expectedValue}`,
|
||||
);
|
||||
}
|
||||
}
|
33
src/lib/server/test/Listing.test.ts
Normal file
33
src/lib/server/test/Listing.test.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { createNewListing, updateListing } from "../modifyListing";
|
||||
import { Game } from "../../Game";
|
||||
import { deepEqual, equal, ok } from "node:assert/strict";
|
||||
|
||||
describe("Listing", () => {
|
||||
describe("createNewListing", () => {
|
||||
it("should create a new Listing with the provided data, and a new UUID", () => {
|
||||
const game = new Game();
|
||||
const listing = createNewListing(game);
|
||||
|
||||
ok(listing.data instanceof Game);
|
||||
ok(listing.createdAt instanceof Date);
|
||||
ok(listing.modifiedAt === null);
|
||||
ok(!listing.deleted);
|
||||
equal(typeof listing.id, "string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateListing", () => {
|
||||
it("should return a new listing with updated data and a modified date", () => {
|
||||
const game = new Game();
|
||||
const update = new Game();
|
||||
update.isStarted = !game.isStarted;
|
||||
|
||||
const listing = createNewListing(game);
|
||||
const updatedListing = updateListing(listing, update);
|
||||
|
||||
deepEqual(updatedListing.data, update);
|
||||
ok(updatedListing.modifiedAt instanceof Date);
|
||||
});
|
||||
});
|
||||
});
|
15
src/lib/server/test/getDiceRoll.test.ts
Normal file
15
src/lib/server/test/getDiceRoll.test.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { getDiceRoll } from "../../getDiceRoll";
|
||||
import { deepEqual } from "node:assert/strict";
|
||||
|
||||
function testRandom() {
|
||||
const val = [0, 0.2, 0.5, 0.5, 0.7, 0.9];
|
||||
return () => val.shift()!;
|
||||
}
|
||||
|
||||
describe("getDiceRoll", () => {
|
||||
it("should return an array of numbers from 1 to 6 with a given length", () => {
|
||||
let rand = getDiceRoll(6, testRandom());
|
||||
deepEqual(rand, [0, 1, 3, 3, 4, 6]);
|
||||
});
|
||||
});
|
140
src/lib/server/test/validation.test.ts
Normal file
140
src/lib/server/test/validation.test.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { equal, ok } from "node:assert/strict";
|
||||
import { hasProperty, hasOnlyKeys } from "../../validation";
|
||||
|
||||
describe("validation", () => {
|
||||
describe("hasProperty", () => {
|
||||
it("should return false if the property is undefined", () => {
|
||||
const target = { some: "property" };
|
||||
const result = hasProperty(target, "important", "string");
|
||||
|
||||
equal(result, false);
|
||||
});
|
||||
|
||||
it("should return false if passed a non-object", () => {
|
||||
const target = 45;
|
||||
const result = hasProperty(target, "important", "string");
|
||||
|
||||
equal(result, false);
|
||||
});
|
||||
|
||||
it("should return false if passed null", () => {
|
||||
const target = null;
|
||||
const result = hasProperty(target, "important", "string");
|
||||
|
||||
equal(result, false);
|
||||
});
|
||||
|
||||
it("should return false if passed undefined", () => {
|
||||
const target = undefined;
|
||||
const result = hasProperty(target, "important", "string");
|
||||
|
||||
equal(result, false);
|
||||
});
|
||||
|
||||
it("should return false if the property is of the wrong type", () => {
|
||||
const target = { important: 45 };
|
||||
const result = hasProperty(target, "important", "string");
|
||||
|
||||
equal(result, false);
|
||||
});
|
||||
|
||||
it("should return true if the property is the correct type", () => {
|
||||
const target = {
|
||||
first: "string",
|
||||
second: 2,
|
||||
third: false,
|
||||
fourth: null,
|
||||
fifth: { something: "important" },
|
||||
sixth: ["one", "two"],
|
||||
};
|
||||
|
||||
ok(hasProperty(target, "first", "string"));
|
||||
ok(hasProperty(target, "second", "number"));
|
||||
ok(hasProperty(target, "third", "boolean"));
|
||||
ok(hasProperty(target, "fourth", "null"));
|
||||
ok(hasProperty(target, "fifth", "object"));
|
||||
ok(hasProperty(target, "sixth", "array"));
|
||||
});
|
||||
|
||||
it("should return false if passed an array type and the property isn't an array", () => {
|
||||
const target = {
|
||||
arr: "not array",
|
||||
};
|
||||
|
||||
equal(hasProperty(target, "arr", "string[]"), false);
|
||||
});
|
||||
|
||||
it("should return false if the defined array contains a non-matching element", () => {
|
||||
const target = {
|
||||
arr: ["I", "was", "born", "in", 1989],
|
||||
};
|
||||
|
||||
equal(hasProperty(target, "arr", "string[]"), false);
|
||||
});
|
||||
|
||||
it("should return true if all the elements in a defined array match", () => {
|
||||
const target = {
|
||||
arr: ["I", "was", "born", "in", "1989"],
|
||||
};
|
||||
|
||||
ok(hasProperty(target, "arr", "string[]"));
|
||||
});
|
||||
|
||||
it("should return true if all the elements in a defined array match one of multiple types", () => {
|
||||
const target = {
|
||||
arr: ["I", "was", "born", "in", 1989],
|
||||
};
|
||||
|
||||
ok(hasProperty(target, "arr", "(string|number)[]"));
|
||||
});
|
||||
|
||||
it("should return true if type is null but property is nullable", () => {
|
||||
const target = {
|
||||
nullable: null,
|
||||
};
|
||||
|
||||
ok(hasProperty(target, "nullable", "string", true));
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasOnlyKeys", () => {
|
||||
it("returns false if the target is not an object", () => {
|
||||
equal(hasOnlyKeys(45, []), false);
|
||||
});
|
||||
|
||||
it("returns false has extra properties", () => {
|
||||
const target = {
|
||||
one: "one",
|
||||
two: "two",
|
||||
three: "three",
|
||||
};
|
||||
|
||||
const keys = ["one", "two"];
|
||||
|
||||
equal(hasOnlyKeys(target, keys), false);
|
||||
});
|
||||
|
||||
it("should return true if the target has only the provided keys", () => {
|
||||
const target = {
|
||||
one: "one",
|
||||
two: "two",
|
||||
three: "three",
|
||||
};
|
||||
|
||||
const keys = ["one", "two", "three"];
|
||||
|
||||
ok(hasOnlyKeys(target, keys));
|
||||
});
|
||||
|
||||
it("should return true if the target has only a subset of the provided keys", () => {
|
||||
const target = {
|
||||
one: "one",
|
||||
};
|
||||
|
||||
const keys = ["one", "two", "three"];
|
||||
|
||||
ok(hasOnlyKeys(target, keys));
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user