diff --git a/src/lib/GameData.ts b/src/lib/GameData.ts
index 10a30f7..df5f06e 100644
--- a/src/lib/GameData.ts
+++ b/src/lib/GameData.ts
@@ -1,10 +1,11 @@
import { hasOnlyKeys, hasProperty } from "./validation";
import { isId, type Id } from "./Id";
import type { State } from "./State";
+import { isGamePlayer, type GamePlayer } from "./GamePlayer";
export interface GameData {
isStarted: boolean;
- players: { id: Id; username: string }[];
+ players: GamePlayer[];
state: State;
}
@@ -17,13 +18,7 @@ export function isGameData(target: unknown): target is GameData {
const { players } = target as any;
for (const player of players) {
- if (!isId(player.id)) {
- return false;
- }
-
- if (!hasProperty(player, "username", "string")) {
- return false;
- }
+ if (!isGamePlayer(player)) return false;
}
} else {
return false;
diff --git a/src/lib/GamePlayer.ts b/src/lib/GamePlayer.ts
new file mode 100644
index 0000000..c0afc7b
--- /dev/null
+++ b/src/lib/GamePlayer.ts
@@ -0,0 +1,24 @@
+import { isId, type Id } from "./Id";
+import { hasOnlyKeys, hasProperty } from "./validation";
+
+export interface GamePlayer {
+ id: Id;
+ username: string;
+ isReady: boolean;
+}
+
+export function isGamePlayer(target: unknown): target is GamePlayer {
+ if (!isId((target as any).id)) {
+ return false;
+ }
+
+ if (!hasProperty(target, "username", "string")) {
+ return false;
+ }
+
+ if (!hasProperty(target, "isReady", "boolean")) {
+ return false;
+ }
+
+ return hasOnlyKeys(target, ["username", "id", "isReady"]);
+}
diff --git a/src/lib/components/PlayerList.svelte b/src/lib/components/PlayerList.svelte
index e10fbdd..cec7f59 100644
--- a/src/lib/components/PlayerList.svelte
+++ b/src/lib/components/PlayerList.svelte
@@ -1,19 +1,29 @@
-
{#each players as player}
- {#if me !== null && player.id === me.id}
- - you
- {:else}
- - {player.username}
- {/if}
+ - {player.username}
{/each}
@@ -23,6 +33,18 @@
font-weight: bold;
}
+ .ready::after {
+ content: " [ready]";
+ color: green;
+ font-weight: bold;
+ }
+
+ .unready::after {
+ content: " [unready]";
+ color: red;
+ font-weight: bold;
+ }
+
.list {
border: 1pt gray solid;
background: white;
diff --git a/src/lib/server/Game.ts b/src/lib/server/Game.ts
index 6dac908..ae96d71 100644
--- a/src/lib/server/Game.ts
+++ b/src/lib/server/Game.ts
@@ -1,9 +1,10 @@
import type { Id } from "../Id";
import type { GameData } from "../GameData";
import type { State } from "../State";
+import type { GamePlayer } from "$lib/GamePlayer";
export class Game implements GameData {
- players: { id: Id; username: string }[];
+ players: GamePlayer[];
isStarted: boolean;
state: State;
@@ -22,8 +23,18 @@ export class Game implements GameData {
return game;
}
- addPlayer(id: Id, username: string) {
- this.players.push({ id, username });
+ addPlayer(player: GamePlayer) {
+ this.players.push(player);
+ }
+
+ setPlayerReady({ id: playerId, isReady }: GamePlayer) {
+ const player = this.players.find(({ id }) => playerId === id);
+
+ if (!player) return null;
+
+ player.isReady = isReady;
+
+ return player;
}
start() {
diff --git a/src/lib/server/requestTools.ts b/src/lib/server/requestTools.ts
index 7780cf7..58a2f06 100644
--- a/src/lib/server/requestTools.ts
+++ b/src/lib/server/requestTools.ts
@@ -2,6 +2,8 @@ import type { LocalCredentials } from "./auth";
export enum ResourceId {
Game = "gameid",
+ Turn = "turnid",
+ Player = "playerid",
}
export function getUser(locals: { user: LocalCredentials }) {
diff --git a/src/lib/server/responseBodies.ts b/src/lib/server/responseBodies.ts
index 5378f8b..6bb885b 100644
--- a/src/lib/server/responseBodies.ts
+++ b/src/lib/server/responseBodies.ts
@@ -10,6 +10,10 @@ export function createdResponse(id: string) {
return Response.json({ item: id }, { status: 201 });
}
+export function conflictResponse() {
+ return Response.json({ error: "Conflict" }, { status: 409 });
+}
+
export function tokenResponse(token: string) {
return Response.json({ access_token: token });
}
diff --git a/src/lib/server/test/Game.spec.ts b/src/lib/server/test/Game.spec.ts
index 77a0420..42eec30 100644
--- a/src/lib/server/test/Game.spec.ts
+++ b/src/lib/server/test/Game.spec.ts
@@ -13,9 +13,9 @@ describe("Game", () => {
equal(game.players.length, 0);
- game.addPlayer(idString, user);
+ game.addPlayer({ id: idString, username: user, isReady: false });
equal(game.players.length, 1);
- deepEqual(game.players[0], { id: idString, username: user });
+ deepEqual(game.players[0], { id: idString, username: user, isReady: false });
});
});
diff --git a/src/lib/test/GameData.spec.ts b/src/lib/test/GameData.spec.ts
index a1b9f31..a9eea47 100644
--- a/src/lib/test/GameData.spec.ts
+++ b/src/lib/test/GameData.spec.ts
@@ -30,12 +30,25 @@ describe("GameData", () => {
});
it("rejects an object with a malformed players array", () => {
- const data: unknown = {
- players: [{ id: idString }],
+ let data: unknown = {
+ players: [{ id: idString, username: "Mr. User" }],
state: {},
isStarted: false,
};
+ equal(isGameData(data), false);
+ data = {
+ players: [{ id: idString, isReady: false }],
+ state: {},
+ isStarted: false,
+ };
+ equal(isGameData(data), false);
+
+ data = {
+ players: [{ username: "Mr. User", isReady: false }],
+ state: {},
+ isStarted: false,
+ };
equal(isGameData(data), false);
});
@@ -50,7 +63,7 @@ describe("GameData", () => {
it("rejects an object with extra properties", () => {
const data: unknown = {
- players: [{ username: "Mr. User", id: idString }],
+ players: [{ username: "Mr. User", id: idString, isReady: false }],
isStarted: false,
state: {},
extra: true,
@@ -61,7 +74,7 @@ describe("GameData", () => {
it("should accept a proper GameData object", () => {
const data: unknown = {
- players: [{ username: "Mr. User", id: idString }],
+ players: [{ username: "Mr. User", id: idString, isReady: false }],
state: {},
isStarted: false,
};
diff --git a/src/lib/test/GameEvent.spec.ts b/src/lib/test/GameEvent.spec.ts
index d44a0af..3c8f80a 100644
--- a/src/lib/test/GameEvent.spec.ts
+++ b/src/lib/test/GameEvent.spec.ts
@@ -58,11 +58,13 @@ describe("Game Events", () => {
const playerOne = {
id: createId(),
username: "Player One",
+ isReady: false,
};
const playerTwo = {
id: createId(),
username: "Player Two",
+ isReady: false,
};
it("should throw if the kind is unkown", () => {
diff --git a/src/routes/api/games/[gameid]/players/[playerid]/+server.ts b/src/routes/api/games/[gameid]/players/[playerid]/+server.ts
new file mode 100644
index 0000000..67a7b18
--- /dev/null
+++ b/src/routes/api/games/[gameid]/players/[playerid]/+server.ts
@@ -0,0 +1,60 @@
+import type { GameData } from "$lib/GameData";
+import { isGamePlayer, type GamePlayer } from "$lib/GamePlayer";
+import { isListing } from "$lib/Listing";
+import { Game } from "$lib/server/Game";
+import { updateListing } from "$lib/server/modifyListing";
+import {
+ readListingById,
+ ServerCollections,
+ writeUpdatedListing,
+} from "$lib/server/mongo";
+import { getBody, getParam, ResourceId } from "$lib/server/requestTools";
+import {
+ badRequestResponse,
+ conflictResponse,
+ notFoundResponse,
+ singleResponse,
+} from "$lib/server/responseBodies";
+import type { RequestHandler } from "@sveltejs/kit";
+
+export const PUT: RequestHandler = async ({ params, request }): Promise => {
+ const id = getParam(params, ResourceId.Game);
+
+ if (id === null) {
+ return badRequestResponse("missing playerid parameter");
+ }
+
+ let player: GamePlayer | null;
+ try {
+ player = await getBody(request, isGamePlayer);
+ } catch (err) {
+ return badRequestResponse("missing player body");
+ }
+
+ if (!player) {
+ return badRequestResponse("malformed request");
+ }
+
+ const listing = await readListingById(ServerCollections.Games, id, isListing);
+ if (!listing) {
+ return notFoundResponse();
+ }
+
+ if (listing.data.isStarted === true) {
+ return conflictResponse();
+ }
+
+ const game = Game.from(listing.data);
+ if (game.setPlayerReady(player) === null) return notFoundResponse();
+
+ // TODO: there's a potential race condition here where some player is unreadying as this
+ // function is running. This should do some kind of check in MongoDB to make sure
+ // all players are ready if it's starting the game. For now: good enough.
+ if (game.players.every(({ isReady }) => isReady)) {
+ game.start();
+ }
+
+ await writeUpdatedListing(ServerCollections.Games, updateListing(listing, game));
+
+ return singleResponse(game);
+};
diff --git a/src/routes/api/games/[gameid]/[turns]/+server.ts b/src/routes/api/games/[gameid]/turns/[turnid]/+server.ts
similarity index 98%
rename from src/routes/api/games/[gameid]/[turns]/+server.ts
rename to src/routes/api/games/[gameid]/turns/[turnid]/+server.ts
index a51ba2c..feaff62 100644
--- a/src/routes/api/games/[gameid]/[turns]/+server.ts
+++ b/src/routes/api/games/[gameid]/turns/[turnid]/+server.ts
@@ -12,7 +12,6 @@ import {
import { getBody, getParam, getUser, ResourceId } from "$lib/server/requestTools";
import {
badRequestResponse,
- createdResponse,
notFoundResponse,
singleResponse,
} from "$lib/server/responseBodies";
diff --git a/src/routes/games/+page.svelte b/src/routes/games/+page.svelte
index 56fea7d..dc90688 100644
--- a/src/routes/games/+page.svelte
+++ b/src/routes/games/+page.svelte
@@ -23,9 +23,7 @@
{prettyDate(new Date(game.createdAt))}
- {#each games as game}
-
- {/each}
+