bring server files into project.
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -21,3 +21,6 @@ Thumbs.db
 | 
				
			|||||||
# Vite
 | 
					# Vite
 | 
				
			||||||
vite.config.js.timestamp-*
 | 
					vite.config.js.timestamp-*
 | 
				
			||||||
vite.config.ts.timestamp-*
 | 
					vite.config.ts.timestamp-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Development
 | 
				
			||||||
 | 
					cert
 | 
				
			||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
	"useTabs": true,
 | 
						"useTabs": true,
 | 
				
			||||||
	"singleQuote": true,
 | 
					 | 
				
			||||||
	"trailingComma": "none",
 | 
						"trailingComma": "none",
 | 
				
			||||||
	"printWidth": 100,
 | 
						"printWidth": 100,
 | 
				
			||||||
	"plugins": ["prettier-plugin-svelte"],
 | 
						"plugins": ["prettier-plugin-svelte"],
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										41
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										41
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -13,6 +13,7 @@
 | 
				
			|||||||
				"@sveltejs/adapter-auto": "^3.0.0",
 | 
									"@sveltejs/adapter-auto": "^3.0.0",
 | 
				
			||||||
				"@sveltejs/kit": "^2.0.0",
 | 
									"@sveltejs/kit": "^2.0.0",
 | 
				
			||||||
				"@sveltejs/vite-plugin-svelte": "^4.0.0",
 | 
									"@sveltejs/vite-plugin-svelte": "^4.0.0",
 | 
				
			||||||
 | 
									"@types/node": "^22.10.7",
 | 
				
			||||||
				"eslint": "^9.7.0",
 | 
									"eslint": "^9.7.0",
 | 
				
			||||||
				"eslint-config-prettier": "^9.1.0",
 | 
									"eslint-config-prettier": "^9.1.0",
 | 
				
			||||||
				"eslint-plugin-svelte": "^2.36.0",
 | 
									"eslint-plugin-svelte": "^2.36.0",
 | 
				
			||||||
@ -1128,6 +1129,16 @@
 | 
				
			|||||||
			"dev": true,
 | 
								"dev": true,
 | 
				
			||||||
			"license": "MIT"
 | 
								"license": "MIT"
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							"node_modules/@types/node": {
 | 
				
			||||||
 | 
								"version": "22.10.7",
 | 
				
			||||||
 | 
								"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
 | 
				
			||||||
 | 
								"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
 | 
				
			||||||
 | 
								"dev": true,
 | 
				
			||||||
 | 
								"license": "MIT",
 | 
				
			||||||
 | 
								"dependencies": {
 | 
				
			||||||
 | 
									"undici-types": "~6.20.0"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		"node_modules/@typescript-eslint/eslint-plugin": {
 | 
							"node_modules/@typescript-eslint/eslint-plugin": {
 | 
				
			||||||
			"version": "8.20.0",
 | 
								"version": "8.20.0",
 | 
				
			||||||
			"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
 | 
								"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
 | 
				
			||||||
@ -2568,6 +2579,19 @@
 | 
				
			|||||||
				"node": ">=8.6"
 | 
									"node": ">=8.6"
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							"node_modules/micromatch/node_modules/picomatch": {
 | 
				
			||||||
 | 
								"version": "2.3.1",
 | 
				
			||||||
 | 
								"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
 | 
				
			||||||
 | 
								"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
 | 
				
			||||||
 | 
								"dev": true,
 | 
				
			||||||
 | 
								"license": "MIT",
 | 
				
			||||||
 | 
								"engines": {
 | 
				
			||||||
 | 
									"node": ">=8.6"
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								"funding": {
 | 
				
			||||||
 | 
									"url": "https://github.com/sponsors/jonschlinkert"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		"node_modules/minimatch": {
 | 
							"node_modules/minimatch": {
 | 
				
			||||||
			"version": "3.1.2",
 | 
								"version": "3.1.2",
 | 
				
			||||||
			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
 | 
								"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
 | 
				
			||||||
@ -2742,13 +2766,15 @@
 | 
				
			|||||||
			"license": "ISC"
 | 
								"license": "ISC"
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		"node_modules/picomatch": {
 | 
							"node_modules/picomatch": {
 | 
				
			||||||
			"version": "2.3.1",
 | 
								"version": "4.0.2",
 | 
				
			||||||
			"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
 | 
								"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
 | 
				
			||||||
			"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
 | 
								"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
 | 
				
			||||||
			"dev": true,
 | 
								"dev": true,
 | 
				
			||||||
			"license": "MIT",
 | 
								"license": "MIT",
 | 
				
			||||||
 | 
								"optional": true,
 | 
				
			||||||
 | 
								"peer": true,
 | 
				
			||||||
			"engines": {
 | 
								"engines": {
 | 
				
			||||||
				"node": ">=8.6"
 | 
									"node": ">=12"
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			"funding": {
 | 
								"funding": {
 | 
				
			||||||
				"url": "https://github.com/sponsors/jonschlinkert"
 | 
									"url": "https://github.com/sponsors/jonschlinkert"
 | 
				
			||||||
@ -3432,6 +3458,13 @@
 | 
				
			|||||||
				"typescript": ">=4.8.4 <5.8.0"
 | 
									"typescript": ">=4.8.4 <5.8.0"
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							"node_modules/undici-types": {
 | 
				
			||||||
 | 
								"version": "6.20.0",
 | 
				
			||||||
 | 
								"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
 | 
				
			||||||
 | 
								"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
 | 
				
			||||||
 | 
								"dev": true,
 | 
				
			||||||
 | 
								"license": "MIT"
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		"node_modules/uri-js": {
 | 
							"node_modules/uri-js": {
 | 
				
			||||||
			"version": "4.4.1",
 | 
								"version": "4.4.1",
 | 
				
			||||||
			"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
 | 
								"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -20,6 +20,7 @@
 | 
				
			|||||||
		"@sveltejs/adapter-auto": "^3.0.0",
 | 
							"@sveltejs/adapter-auto": "^3.0.0",
 | 
				
			||||||
		"@sveltejs/kit": "^2.0.0",
 | 
							"@sveltejs/kit": "^2.0.0",
 | 
				
			||||||
		"@sveltejs/vite-plugin-svelte": "^4.0.0",
 | 
							"@sveltejs/vite-plugin-svelte": "^4.0.0",
 | 
				
			||||||
 | 
							"@types/node": "^22.10.7",
 | 
				
			||||||
		"eslint": "^9.7.0",
 | 
							"eslint": "^9.7.0",
 | 
				
			||||||
		"eslint-config-prettier": "^9.1.0",
 | 
							"eslint-config-prettier": "^9.1.0",
 | 
				
			||||||
		"eslint-plugin-svelte": "^2.36.0",
 | 
							"eslint-plugin-svelte": "^2.36.0",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								src/hooks.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/hooks.server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					import type { Handle } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const handle: Handle = async ({ event, resolve }) => {
 | 
				
			||||||
 | 
						console.log("this got called", event.isSubRequest);
 | 
				
			||||||
 | 
						return await resolve(event);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										28
									
								
								src/lib/GameData.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/lib/GameData.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					import { hasOnlyKeys, hasProperty } from "./validation";
 | 
				
			||||||
 | 
					import type { Id } from "./server/Id";
 | 
				
			||||||
 | 
					import type { State } from "./State";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface GameData {
 | 
				
			||||||
 | 
						isStarted: boolean;
 | 
				
			||||||
 | 
						players: Id[];
 | 
				
			||||||
 | 
						state: State;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isGameData(target: unknown): target is GameData {
 | 
				
			||||||
 | 
						if (!hasProperty(target, "isStarted", "boolean")) {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!hasProperty(target, "players", "string[]")) {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// the user cannot update this property, they have to send it as it is, so if we
 | 
				
			||||||
 | 
						// receive something that isn't a correctly formed state, that will get rejected
 | 
				
			||||||
 | 
						// anyway (since it won't match what we already have).
 | 
				
			||||||
 | 
						if (!hasProperty(target, "state", "object")) {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return hasOnlyKeys(target, ["players", "isStarted", "state"]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										455
									
								
								src/lib/GameEvent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										455
									
								
								src/lib/GameEvent.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,455 @@
 | 
				
			|||||||
 | 
					import type { GameData } from "$lib/GameData";
 | 
				
			||||||
 | 
					import { getDiceRoll } from "$lib/server/getDiceRoll";
 | 
				
			||||||
 | 
					import type { State } from "$lib/State";
 | 
				
			||||||
 | 
					import { hasOnlyKeys, hasProperty } from "$lib/validation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const FIRST_ROLL_LOST = -1;
 | 
				
			||||||
 | 
					export const FIRST_ROLL_PENDING = 0;
 | 
				
			||||||
 | 
					export const GAME_SCORE_THRESHOLD = 10_000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum GameEventKind {
 | 
				
			||||||
 | 
						SeatPlayers = "SeatPlayers",
 | 
				
			||||||
 | 
						RollForFirst = "RollForFirst",
 | 
				
			||||||
 | 
						Roll = "Roll",
 | 
				
			||||||
 | 
						Hold = "Hold",
 | 
				
			||||||
 | 
						Score = "Score",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface GameEventData {
 | 
				
			||||||
 | 
						kind: string;
 | 
				
			||||||
 | 
						player?: number;
 | 
				
			||||||
 | 
						value?: number | number[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface GameEvent extends GameEventData {
 | 
				
			||||||
 | 
						run: (state: State) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isGameEventData(target: unknown): target is GameEvent {
 | 
				
			||||||
 | 
						if (typeof target !== "object") {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!hasProperty(target, "kind", "string")) {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: add checks to make sure that, if it has optional properties, they
 | 
				
			||||||
 | 
						//       are the correct type.
 | 
				
			||||||
 | 
						return hasOnlyKeys(target, ["kind", "player", "value"]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getGameEvent(data: GameData, event: GameEventData): GameEvent {
 | 
				
			||||||
 | 
						switch (event.kind) {
 | 
				
			||||||
 | 
							case GameEventKind.SeatPlayers:
 | 
				
			||||||
 | 
								if (event.value !== data.players.length) {
 | 
				
			||||||
 | 
									throw new Error("must seat all of the players in the game");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return new SeatPlayers(event);
 | 
				
			||||||
 | 
							case GameEventKind.RollForFirst:
 | 
				
			||||||
 | 
								return new RollForFirst(event);
 | 
				
			||||||
 | 
							case GameEventKind.Roll:
 | 
				
			||||||
 | 
								// Obviously a client can't send the roll they want to make. Instead, the
 | 
				
			||||||
 | 
								// client sends a Roll event with a value representing the number of dice
 | 
				
			||||||
 | 
								// they intend to roll, and then the server creates an array of that length
 | 
				
			||||||
 | 
								// with random dice values.
 | 
				
			||||||
 | 
								if (typeof event.value !== "number") {
 | 
				
			||||||
 | 
									throw new Error("event must include the number of dice to roll");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								event.value = getDiceRoll(event.value, Math.random);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return new Roll(event);
 | 
				
			||||||
 | 
							case GameEventKind.Hold:
 | 
				
			||||||
 | 
								return new Hold(event);
 | 
				
			||||||
 | 
							case GameEventKind.Score:
 | 
				
			||||||
 | 
								return new Score(event);
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								throw new Error(`${event.kind} is not a valid kind of event`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * SeatPlayers takes a value which represents the number of players who will be playing.
 | 
				
			||||||
 | 
					 * It initializes the scores for that number of players.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class SeatPlayers implements GameEvent {
 | 
				
			||||||
 | 
						kind: GameEventKind.SeatPlayers;
 | 
				
			||||||
 | 
						value: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(event: GameEventData) {
 | 
				
			||||||
 | 
							const value = throwIfNoValueNumber(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.kind = GameEventKind.SeatPlayers;
 | 
				
			||||||
 | 
							this.value = value;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						run(state: State) {
 | 
				
			||||||
 | 
							throwIfGameOver(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (state.scores !== undefined) {
 | 
				
			||||||
 | 
								throw new Error("players already seated!");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							state.scores = new Array(this.value).fill(FIRST_ROLL_PENDING);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * RollForFirst takes a player index and a value which represents the pips on the die
 | 
				
			||||||
 | 
					 * that the player rolled. It represents and attempt to roll the highest die and go
 | 
				
			||||||
 | 
					 * first.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Players can roll in any order. Each time a player rolls, the event checks to see if
 | 
				
			||||||
 | 
					 * everyone has rolled and if there is a winner, it sets that person as the first active
 | 
				
			||||||
 | 
					 * player and ends the rollling-for-first stage of the game. If there was a tie, those
 | 
				
			||||||
 | 
					 * players are expected to re-roll as a tie breaker. Re-rolling continues until someone
 | 
				
			||||||
 | 
					 * wins.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class RollForFirst implements GameEvent {
 | 
				
			||||||
 | 
						kind: GameEventKind.RollForFirst;
 | 
				
			||||||
 | 
						player: number;
 | 
				
			||||||
 | 
						value: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(event: GameEventData) {
 | 
				
			||||||
 | 
							this.kind = GameEventKind.RollForFirst;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const player = throwIfNoPlayer(event);
 | 
				
			||||||
 | 
							const value = throwIfNoValueNumber(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.player = player;
 | 
				
			||||||
 | 
							this.value = value;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						run(state: State) {
 | 
				
			||||||
 | 
							const scores = state.scores ?? [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							throwIfGameOver(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (state.playing !== undefined) {
 | 
				
			||||||
 | 
								throw new Error("first player has already been selected");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (scores.length <= this.player) {
 | 
				
			||||||
 | 
								throw new Error("this player index is out of bounds");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (scores[this.player] !== FIRST_ROLL_PENDING) {
 | 
				
			||||||
 | 
								throw new Error("this player has already rolled");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							scores[this.player] = this.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let best = 0;
 | 
				
			||||||
 | 
							let winningIndex = -1;
 | 
				
			||||||
 | 
							const ties = new Set<number>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// check for a winner
 | 
				
			||||||
 | 
							for (let i = 0; i < scores.length; i++) {
 | 
				
			||||||
 | 
								const score = scores[i];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// if someone hasn't rolled, no winner yet
 | 
				
			||||||
 | 
								if (score === 0) {
 | 
				
			||||||
 | 
									best = 0;
 | 
				
			||||||
 | 
									winningIndex = -1;
 | 
				
			||||||
 | 
									ties.clear();
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (score > best) {
 | 
				
			||||||
 | 
									// This is the new high score...
 | 
				
			||||||
 | 
									best = score;
 | 
				
			||||||
 | 
									winningIndex = i;
 | 
				
			||||||
 | 
									ties.clear();
 | 
				
			||||||
 | 
								} else if (score === best) {
 | 
				
			||||||
 | 
									// ...or it is tied for the best score.
 | 
				
			||||||
 | 
									ties.add(winningIndex);
 | 
				
			||||||
 | 
									ties.add(i);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (winningIndex > -1) {
 | 
				
			||||||
 | 
								// If a winner was found...
 | 
				
			||||||
 | 
								if (ties.size < 1) {
 | 
				
			||||||
 | 
									// ...and there are no ties...
 | 
				
			||||||
 | 
									state.playing = winningIndex;
 | 
				
			||||||
 | 
									state.scores = scores.fill(0);
 | 
				
			||||||
 | 
									state.dieCount = 6;
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									// ...otherwise, setup for tie breaking rolls.
 | 
				
			||||||
 | 
									state.scores = scores.map((_, i) =>
 | 
				
			||||||
 | 
										ties.has(i) ? FIRST_ROLL_PENDING : FIRST_ROLL_LOST,
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Roll takes a player index and a value which is an array of numbers representing the
 | 
				
			||||||
 | 
					 * number of pips on each rolled die. The Roll event represents a player rolling however
 | 
				
			||||||
 | 
					 * many dice they have available.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class Roll implements GameEvent {
 | 
				
			||||||
 | 
						kind: GameEventKind.Roll;
 | 
				
			||||||
 | 
						player: number;
 | 
				
			||||||
 | 
						value: number[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(event: GameEventData) {
 | 
				
			||||||
 | 
							this.kind = GameEventKind.Roll;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const player = throwIfNoPlayer(event);
 | 
				
			||||||
 | 
							const value = throwIfNoValueArray(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.player = player;
 | 
				
			||||||
 | 
							this.value = value;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						run(state: State) {
 | 
				
			||||||
 | 
							const scores = state.scores ?? [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							throwIfGameOver(state);
 | 
				
			||||||
 | 
							throwIfWrongTurn(state, this.player);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (state.dieCount !== this.value.length) {
 | 
				
			||||||
 | 
								throw new Error("player is rolling the wrong number if dice");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (state.dice !== undefined) {
 | 
				
			||||||
 | 
								throw new Error("player has already rolled");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							state.dice = this.value;
 | 
				
			||||||
 | 
							delete state.dieCount;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Hold takes a player index and a value which is an array of numbers representing
 | 
				
			||||||
 | 
					 * indexes of rolled dice that a player wants to hold. Hold represents the part of a
 | 
				
			||||||
 | 
					 * player's turn where they select scoring dice to hold onto.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * If the player holds all the dice, the number of dice they can roll will reset to 6,
 | 
				
			||||||
 | 
					 * and they will be required to roll again (a player must always roll when they have 6)
 | 
				
			||||||
 | 
					 * dice available.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class Hold implements GameEvent {
 | 
				
			||||||
 | 
						kind: GameEventKind.Hold;
 | 
				
			||||||
 | 
						player: number;
 | 
				
			||||||
 | 
						value: number[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(event: GameEventData) {
 | 
				
			||||||
 | 
							this.kind = GameEventKind.Hold;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const player = throwIfNoPlayer(event);
 | 
				
			||||||
 | 
							const value = throwIfNoValueArray(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.player = player;
 | 
				
			||||||
 | 
							this.value = value;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						run(state: State) {
 | 
				
			||||||
 | 
							const { dice } = state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							throwIfGameOver(state);
 | 
				
			||||||
 | 
							throwIfWrongTurn(state, this.player);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!dice) {
 | 
				
			||||||
 | 
								throw new Error("player hasn't rolled yet");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (this.value.length < 1) {
 | 
				
			||||||
 | 
								throw new Error("player cannot hold 0 dice");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// heldValues represents the actual values of the held dice based on the indexes
 | 
				
			||||||
 | 
							// of the held dice.
 | 
				
			||||||
 | 
							const heldValues = this.value.map((val) => {
 | 
				
			||||||
 | 
								if (dice[val] === undefined) {
 | 
				
			||||||
 | 
									throw new Error("player is trying to hold non-existent dice");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return dice[val];
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const counts = new Map<number, number>();
 | 
				
			||||||
 | 
							let total = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const valueSet = new Set(heldValues);
 | 
				
			||||||
 | 
							if (valueSet.size === 6) {
 | 
				
			||||||
 | 
								// Detect a run of six: if the held values create a set of values with a size
 | 
				
			||||||
 | 
								// of six, then, since there are only six dice, it MUST be a run of six.
 | 
				
			||||||
 | 
								total = 2_000;
 | 
				
			||||||
 | 
							} else if (valueSet.size === 2 && this.value.length === 6) {
 | 
				
			||||||
 | 
								// Detect two threes of a kind: if the number of held values was six, and the
 | 
				
			||||||
 | 
								// set of unique values is two, then there MUST have been two threes of a
 | 
				
			||||||
 | 
								// kind.
 | 
				
			||||||
 | 
								total = 1_500;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// A player can use a "push" if they are using every one of their rolled
 | 
				
			||||||
 | 
								// dice.
 | 
				
			||||||
 | 
								let canPush = this.value.length === dice.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for (const val of heldValues) {
 | 
				
			||||||
 | 
									const count = counts.get(val) ?? 0;
 | 
				
			||||||
 | 
									counts.set(val, count + 1);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for (let pips = 1; pips <= 6; pips++) {
 | 
				
			||||||
 | 
									if (!counts.has(pips)) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// This for sure exists because of the check at t he top of the loop.
 | 
				
			||||||
 | 
									const count = counts.get(pips)!;
 | 
				
			||||||
 | 
									const pipsScore = scorePips(count, pips);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if (!pipsScore) {
 | 
				
			||||||
 | 
										// The player can only hold scoring dice...
 | 
				
			||||||
 | 
										if (!canPush || count !== 2) {
 | 
				
			||||||
 | 
											throw new Error("player is holding non-scoring dice");
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											// ...unless they have a push.
 | 
				
			||||||
 | 
											canPush = false;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									total += pipsScore;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							state.dieCount = dice.length - this.value.length;
 | 
				
			||||||
 | 
							state.heldScore = total;
 | 
				
			||||||
 | 
							delete state.dice;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (state.dieCount === 0) {
 | 
				
			||||||
 | 
								state.dieCount = 6;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Score takes a player index and a value which is a number representing the number of
 | 
				
			||||||
 | 
					 * points that a player has accrued by rolling and holding dice. Once a player has
 | 
				
			||||||
 | 
					 * scored, the active player is passed on to the next player and the players total score
 | 
				
			||||||
 | 
					 * is updated.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * If a player's score matches or exceeds 10,000, then a turn countdown begins, allowing
 | 
				
			||||||
 | 
					 * all the remaining players to take one more turn before the game ends.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class Score implements GameEvent {
 | 
				
			||||||
 | 
						kind: GameEventKind.Score;
 | 
				
			||||||
 | 
						player: number;
 | 
				
			||||||
 | 
						value: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(event: GameEventData) {
 | 
				
			||||||
 | 
							const player = throwIfNoPlayer(event);
 | 
				
			||||||
 | 
							const value = throwIfNoValueNumber(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.kind = GameEventKind.Score;
 | 
				
			||||||
 | 
							this.player = player;
 | 
				
			||||||
 | 
							this.value = value;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						run(state: State) {
 | 
				
			||||||
 | 
							const { dieCount, heldScore, scores } = state;
 | 
				
			||||||
 | 
							const playerCount = scores?.length ?? 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							throwIfGameOver(state);
 | 
				
			||||||
 | 
							throwIfWrongTurn(state, this.player);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (dieCount === 6) {
 | 
				
			||||||
 | 
								throw new Error("player must roll when they have six free dice");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (this.value !== state.heldScore) {
 | 
				
			||||||
 | 
								throw new Error("player score must match the held score");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// It's safe to tell the compiler that the player is not undefined because of the
 | 
				
			||||||
 | 
							// check above.
 | 
				
			||||||
 | 
							state.scores![this.player] += heldScore ?? 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Increment the index of the active player, circling back to 1 if the player
 | 
				
			||||||
 | 
							// who just scored was the last player in the array.
 | 
				
			||||||
 | 
							state.playing =
 | 
				
			||||||
 | 
								playerCount - 1 === this.player
 | 
				
			||||||
 | 
									? (state.playing = 0)
 | 
				
			||||||
 | 
									: (state.playing = this.player + 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							state.dieCount = 6;
 | 
				
			||||||
 | 
							delete state.heldScore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If there is an active turn countdown, then now is the time to decrement it,
 | 
				
			||||||
 | 
							// otherwise, if the player has crossed 10,000, then it's time to start the
 | 
				
			||||||
 | 
							// turn countdown.
 | 
				
			||||||
 | 
							if (state.turnCountdown !== undefined) {
 | 
				
			||||||
 | 
								state.turnCountdown--;
 | 
				
			||||||
 | 
							} else if (state.scores![this.player] >= GAME_SCORE_THRESHOLD) {
 | 
				
			||||||
 | 
								state.turnCountdown = playerCount - 1;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function scorePips(count: number, pips: number) {
 | 
				
			||||||
 | 
						if (count < 3) {
 | 
				
			||||||
 | 
							// If not a three of a kind, return the raw dice value...
 | 
				
			||||||
 | 
							return pipScore(pips) * count;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ...otherwise, this is a three or more of a kind.
 | 
				
			||||||
 | 
						if (pips === 1) {
 | 
				
			||||||
 | 
							// Ones are treated as 10s.
 | 
				
			||||||
 | 
							pips = 10;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Three of a kind is the pips times 100, times two for each additional rolled dice.
 | 
				
			||||||
 | 
						return pips * 100 * (count - 2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * TODO: There are variations to the rules of ten thousand. It would be interesting if
 | 
				
			||||||
 | 
					 *       code like this could actually come from a setting somewhere.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function pipScore(pips: number) {
 | 
				
			||||||
 | 
						switch (pips) {
 | 
				
			||||||
 | 
							case 1:
 | 
				
			||||||
 | 
								return 100;
 | 
				
			||||||
 | 
							case 5:
 | 
				
			||||||
 | 
								return 50;
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function throwIfNoPlayer(event: GameEventData): number {
 | 
				
			||||||
 | 
						if (event.player === undefined) {
 | 
				
			||||||
 | 
							throw new Error("missing player index");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return event.player;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function throwIfNoValueNumber({ value }: GameEventData): number {
 | 
				
			||||||
 | 
						if (typeof value !== "number") {
 | 
				
			||||||
 | 
							throw new Error("missing value");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return value;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function throwIfNoValueArray({ value }: GameEventData): number[] {
 | 
				
			||||||
 | 
						if (!Array.isArray(value)) {
 | 
				
			||||||
 | 
							throw new Error("missing value array");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return value;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function throwIfGameOver(state: State) {
 | 
				
			||||||
 | 
						if (state.turnCountdown === 0) throw new Error("the game is over");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function throwIfWrongTurn(state: State, player: number) {
 | 
				
			||||||
 | 
						if (state.playing === undefined || state.playing !== player)
 | 
				
			||||||
 | 
							throw new Error("it's not this player's turn");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/lib/Listing.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/lib/Listing.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import { hasProperty } from "$lib/validation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Listing<T> {
 | 
				
			||||||
 | 
						id: string;
 | 
				
			||||||
 | 
						createdAt: string;
 | 
				
			||||||
 | 
						modifiedAt: string | null;
 | 
				
			||||||
 | 
						deleted: boolean;
 | 
				
			||||||
 | 
						data: T;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isListing<T>(
 | 
				
			||||||
 | 
						target: unknown,
 | 
				
			||||||
 | 
						dataGuard?: (target: unknown) => target is T
 | 
				
			||||||
 | 
					): target is Listing<T> {
 | 
				
			||||||
 | 
						if (!hasProperty(target, "id", "string")) return false;
 | 
				
			||||||
 | 
						if (!hasProperty(target, "createdAt", "string")) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!hasProperty(target, "modifiedAt", "null")) {
 | 
				
			||||||
 | 
							if (!hasProperty(target, "modifiedAt", "string")) return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!hasProperty(target, "deleted", "boolean")) return false;
 | 
				
			||||||
 | 
						if (!hasProperty(target, "data", "object")) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return dataGuard?.((target as any)["data"]) ?? true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/lib/ServerResponse.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/lib/ServerResponse.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					import type { Listing } from "$lib/Listing";
 | 
				
			||||||
 | 
					import { hasProperty } from "$lib/validation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ServerResponse =
 | 
				
			||||||
 | 
						| { item: Listing<unknown> }
 | 
				
			||||||
 | 
						| { items: Listing<unknown>[] }
 | 
				
			||||||
 | 
						| { error: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isServerResponse(target: unknown): target is ServerResponse {
 | 
				
			||||||
 | 
						if (hasProperty(target, "item", "object")) return true;
 | 
				
			||||||
 | 
						if (hasProperty(target, "items", "object[]")) return true;
 | 
				
			||||||
 | 
						if (hasProperty(target, "error", "string")) return true;
 | 
				
			||||||
 | 
						return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/lib/State.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/lib/State.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					export interface State {
 | 
				
			||||||
 | 
						dice?: number[];
 | 
				
			||||||
 | 
						dieCount?: number;
 | 
				
			||||||
 | 
						gameOver?: boolean;
 | 
				
			||||||
 | 
						heldScore?: number;
 | 
				
			||||||
 | 
						playing?: number;
 | 
				
			||||||
 | 
						scores?: number[];
 | 
				
			||||||
 | 
						turnCountdown?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
// place files you want to import through the `$lib` alias in this folder.
 | 
					 | 
				
			||||||
							
								
								
									
										25
									
								
								src/lib/server/Game.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/lib/server/Game.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import type { Id } from "./Id";
 | 
				
			||||||
 | 
					import type { GameData } from "../GameData";
 | 
				
			||||||
 | 
					import type { State } from "../State";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Game implements GameData {
 | 
				
			||||||
 | 
						players: Id[];
 | 
				
			||||||
 | 
						isStarted: boolean;
 | 
				
			||||||
 | 
						state: State;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							this.players = [];
 | 
				
			||||||
 | 
							this.isStarted = false;
 | 
				
			||||||
 | 
							this.state = {};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						addPlayer(id: Id) {
 | 
				
			||||||
 | 
							this.players.push(id);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						start() {
 | 
				
			||||||
 | 
							if (this.isStarted) throw new Error("game is already started");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.isStarted = true;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/lib/server/Id.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/lib/server/Id.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export type Id = string;
 | 
				
			||||||
							
								
								
									
										4
									
								
								src/lib/server/cache.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/lib/server/cache.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					import type { GameData } from "../GameData";
 | 
				
			||||||
 | 
					import type { Listing } from "./modifyListing";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const games: Listing<GameData>[] = [];
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/lib/server/getDiceRoll.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/lib/server/getDiceRoll.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					export function getDiceRoll(dieCount: number, rand: () => number = Math.random) {
 | 
				
			||||||
 | 
						const dice: number[] = [];
 | 
				
			||||||
 | 
						for (let i = 0; i < dieCount; i++) {
 | 
				
			||||||
 | 
							const value = Math.floor(rand() * 7);
 | 
				
			||||||
 | 
							dice.push(value);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return dice;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/lib/server/modifyListing.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/lib/server/modifyListing.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import { randomUUID } from "crypto";
 | 
				
			||||||
 | 
					import type { Listing } from "$lib/Listing";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createNewListing<T>(data: T): Listing<T> {
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							id: randomUUID(),
 | 
				
			||||||
 | 
							createdAt: new Date().toISOString(),
 | 
				
			||||||
 | 
							modifiedAt: null,
 | 
				
			||||||
 | 
							deleted: false,
 | 
				
			||||||
 | 
							data
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function updateListing<T>(listing: Listing<T>, data: T): Listing<T> {
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							...listing,
 | 
				
			||||||
 | 
							modifiedAt: new Date().toISOString(),
 | 
				
			||||||
 | 
							data
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/lib/server/responseBodies.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/lib/server/responseBodies.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					export function listResponse(items: unknown[]) {
 | 
				
			||||||
 | 
						return Response.json({ items });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function singleResponse(item: unknown) {
 | 
				
			||||||
 | 
						return Response.json({ item });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function badRequestResponse(error: string = "Bad Request") {
 | 
				
			||||||
 | 
						return Response.json({ error }, { status: 400 });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function notFoundResponse() {
 | 
				
			||||||
 | 
						return Response.json({ error: "Not Found" }, { status: 404 });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function serverErrorResponse() {
 | 
				
			||||||
 | 
						return Response.json({ error: "Unexpected Server Error" }, { status: 500 });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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));
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										94
									
								
								src/lib/validation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/lib/validation.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					// hasProperty takes a target, which is unknown, propertyName and propertyType, which
 | 
				
			||||||
 | 
					// are string and isNullable, which is a boolean. If the property exists on the target,
 | 
				
			||||||
 | 
					// and the type of the property value matches the string passes (as determined using the
 | 
				
			||||||
 | 
					// typeof keyword) then hasProperty returns true. An important exception: if the user
 | 
				
			||||||
 | 
					// passes null or an array and a propertyType of "object", hasProperty will return false.
 | 
				
			||||||
 | 
					// To check if something is an array, pass "array" and to check if something is null, pass
 | 
				
			||||||
 | 
					// "null" as the property type. If hasProperty receives a null value, it will still return
 | 
				
			||||||
 | 
					// true as long as "isNullable" has been set to true (it defaults to false).
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// hasProperty can also receive a specific array type which resembles defining arrays of
 | 
				
			||||||
 | 
					// items in TypeScript: e.g. string[] is an array of string, (string|number)[] is an array of
 | 
				
			||||||
 | 
					// strings or numbers. It does not recognize the Array<string> syntax.
 | 
				
			||||||
 | 
					export function hasProperty<T extends typeof Object>(
 | 
				
			||||||
 | 
						target: unknown,
 | 
				
			||||||
 | 
						propertyName: string,
 | 
				
			||||||
 | 
						propertyType: string | T,
 | 
				
			||||||
 | 
						isNullable: boolean = false,
 | 
				
			||||||
 | 
					): boolean {
 | 
				
			||||||
 | 
						if (target === null || target === undefined) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const p = (target as any)[propertyName];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (p === undefined) {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (p === null) {
 | 
				
			||||||
 | 
							return propertyType === "null" || isNullable;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (typeof propertyType !== "string") {
 | 
				
			||||||
 | 
							return p instanceof propertyType
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						if (propertyType === "array") {
 | 
				
			||||||
 | 
							return Array.isArray(p);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: this logic won't work on arrays of array, for instance, string[][]
 | 
				
			||||||
 | 
						if (propertyType.substring(propertyType.length - 2) === "[]") {
 | 
				
			||||||
 | 
							const types = parseArrayType(propertyType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!Array.isArray(p)) {
 | 
				
			||||||
 | 
								return false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (const item of p) {
 | 
				
			||||||
 | 
								let match = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for (const type of types) {
 | 
				
			||||||
 | 
									if (typeof item === type) {
 | 
				
			||||||
 | 
										match = true;
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (!match) {
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return typeof p === propertyType;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// hasOnlyKeys takes a target, which is unknown, and keys, which is a string array.
 | 
				
			||||||
 | 
					// if the target has ONLY own properties that are listed on the string array, then
 | 
				
			||||||
 | 
					// hasOnlyKeys returns true. If there is any extra properties, it returns false.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// hasOnlyKeys does not make sure that the keys provided exist on the target, only
 | 
				
			||||||
 | 
					// that keys not provided do not exist.
 | 
				
			||||||
 | 
					export function hasOnlyKeys(target: unknown, keys: string[]) {
 | 
				
			||||||
 | 
						if (typeof target !== "object") return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const keySet = new Set(keys);
 | 
				
			||||||
 | 
						for (const key in target) {
 | 
				
			||||||
 | 
							if (target.hasOwnProperty(key)) {
 | 
				
			||||||
 | 
								if (!keySet.has(key)) return false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function parseArrayType(propertyType: string): string[] {
 | 
				
			||||||
 | 
						if (propertyType[0] === "(") {
 | 
				
			||||||
 | 
							const typesString = propertyType.substring(1, propertyType.length - 3);
 | 
				
			||||||
 | 
							return typesString.split("|");
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return [propertyType.substring(0, propertyType.length - 2)];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								src/routes/api/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/routes/api/+server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					import { listResponse } from "../../lib/server/responseBodies"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET: RequestHandler = (): Response => {
 | 
				
			||||||
 | 
						return listResponse(["games", "players"]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								src/routes/api/games/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/routes/api/games/+server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					import { listResponse, singleResponse } from "$lib/server/responseBodies";
 | 
				
			||||||
 | 
					import { createNewListing } from "$lib/server/modifyListing";
 | 
				
			||||||
 | 
					import { Game } from "$lib/server/Game";
 | 
				
			||||||
 | 
					import { games } from "$lib/server/cache";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET: RequestHandler = (): Response => {
 | 
				
			||||||
 | 
						return listResponse(games);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const POST: RequestHandler = (): Response => {
 | 
				
			||||||
 | 
						const newListing = createNewListing(new Game());
 | 
				
			||||||
 | 
						games.push(newListing);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return singleResponse(newListing.id);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/routes/api/games/[gameid]/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/routes/api/games/[gameid]/+server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					import { games } from "$lib/server/cache";
 | 
				
			||||||
 | 
					import { notFoundResponse, singleResponse } from "$lib/server/responseBodies";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET: RequestHandler = ({ params }): Response => {
 | 
				
			||||||
 | 
						const id = params["gameid"];
 | 
				
			||||||
 | 
						const game = games.find(({ id: gid }) => id === gid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!game) {
 | 
				
			||||||
 | 
							return notFoundResponse();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return singleResponse(game);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										52
									
								
								src/routes/games/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/routes/games/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					    type GameData = {id: number, name: string, players: string[]}
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const games: GameData[] = [
 | 
				
			||||||
 | 
					        {id: 1, name: "The Worst", players: ["Bob", "Ted", "George"]},
 | 
				
			||||||
 | 
					        {id: 2, name: "The Best", players: ["Shelly", "William", "Abby"]},
 | 
				
			||||||
 | 
					        {id: 3, name: "The One with the Treasure Chest", players: ["Jack"]},
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<main>
 | 
				
			||||||
 | 
					    <h1>Let’s Play Ten Thousand</h1>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <table>
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <td>No.</td>
 | 
				
			||||||
 | 
					                <td>Name</td>
 | 
				
			||||||
 | 
					                <td>Players</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					            {#each games as game}
 | 
				
			||||||
 | 
					                {@render GameRow(game)}
 | 
				
			||||||
 | 
					            {/each}
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					</main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#snippet GameRow (game: GameData)}
 | 
				
			||||||
 | 
					    <tr>
 | 
				
			||||||
 | 
					        <td>{game.id}</td>    
 | 
				
			||||||
 | 
					        <td>{game.name}</td>
 | 
				
			||||||
 | 
					        <td>{game.players.length}</td>
 | 
				
			||||||
 | 
					    </tr>
 | 
				
			||||||
 | 
					{/snippet}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					    main {
 | 
				
			||||||
 | 
					        width: 60rem;
 | 
				
			||||||
 | 
					        margin: auto;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    td {
 | 
				
			||||||
 | 
					        border: solid black 1px;
 | 
				
			||||||
 | 
					        padding: 1rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    table {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										15
									
								
								src/routes/games/[gameid]/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/routes/games/[gameid]/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					    import type { PageData } from "./$types"
 | 
				
			||||||
 | 
					    let { data }: { data: PageData } = $props();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<main>
 | 
				
			||||||
 | 
					    <h1>This is game {data.id}</h1>
 | 
				
			||||||
 | 
					</main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					    main {
 | 
				
			||||||
 | 
					        width: 60rem;
 | 
				
			||||||
 | 
					        margin: auto;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/routes/games/[gameid]/+page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/routes/games/[gameid]/+page.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import { isGameData, type GameData } from "$lib/GameData";
 | 
				
			||||||
 | 
					import { isListing } from "$lib/Listing";
 | 
				
			||||||
 | 
					import { isServerResponse } from "$lib/ServerResponse";
 | 
				
			||||||
 | 
					import { error } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					import type { PageLoad } from "./$types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load: PageLoad = async ({ fetch, params }) => {
 | 
				
			||||||
 | 
						const url = `/api/games/${params.gameid}`;
 | 
				
			||||||
 | 
						let res: Response;
 | 
				
			||||||
 | 
						let body: unknown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							res = await fetch(url);
 | 
				
			||||||
 | 
							body = await res.json();
 | 
				
			||||||
 | 
						} catch (err) {
 | 
				
			||||||
 | 
							error(500, "unable to call API");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (res.status === 404) {
 | 
				
			||||||
 | 
							error(404, `Not Found: ${params.gameid}`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!isServerResponse(body)) {
 | 
				
			||||||
 | 
							error(500, "expected to receive a properly formatted server response body");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ("item" in body && isListing<GameData>(body.item, isGameData)) {
 | 
				
			||||||
 | 
							return body.item;
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							error(500, "expected response body to contain game data");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										41
									
								
								tests/requests.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								tests/requests.http
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					GET http://localhost:5173/api
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GET http://localhost:5173/api/games
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					POST http://localhost:5173/api/games
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GET http://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PUT http://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					Content-Type: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    "state": {},
 | 
				
			||||||
 | 
					    "isStarted": true,
 | 
				
			||||||
 | 
					    "players": ["2", "45", "10"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					POST http://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3/turns
 | 
				
			||||||
 | 
					Accept: application/json
 | 
				
			||||||
 | 
					Content-Type: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    "kind": "Roll",
 | 
				
			||||||
 | 
					    "player": 2,
 | 
				
			||||||
 | 
					    "value": 4
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,10 +1,19 @@
 | 
				
			|||||||
import { defineConfig } from 'vitest/config';
 | 
					import { defineConfig } from "vitest/config";
 | 
				
			||||||
import { sveltekit } from '@sveltejs/kit/vite';
 | 
					import { sveltekit } from "@sveltejs/kit/vite";
 | 
				
			||||||
 | 
					import { readFileSync } from "fs";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
	plugins: [sveltekit()],
 | 
						plugins: [sveltekit()],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						server: {
 | 
				
			||||||
 | 
							https: {
 | 
				
			||||||
 | 
								key: readFileSync(`${__dirname}/cert/key.pem`),
 | 
				
			||||||
 | 
								cert: readFileSync(`${__dirname}/cert/cert.pem`)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							proxy: {}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	test: {
 | 
						test: {
 | 
				
			||||||
		include: ['src/**/*.{test,spec}.{js,ts}']
 | 
							include: ["src/**/*.{test,spec}.{js,ts}"]
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user