This commit is contained in:
parent
52615a5086
commit
e6fa86519c
16 changed files with 3277 additions and 6 deletions
207
server/dice-sync.test.mjs
Normal file
207
server/dice-sync.test.mjs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { createServer } from "node:http";
|
||||
import test from "node:test";
|
||||
import { WebSocket } from "ws";
|
||||
import { attachDiceSyncServer } from "./dice-sync.mjs";
|
||||
|
||||
const createInbox = (socket) => {
|
||||
const messages = [];
|
||||
const waiters = [];
|
||||
socket.on("message", (raw) => {
|
||||
const message = JSON.parse(raw.toString());
|
||||
const waiterIndex = waiters.findIndex(
|
||||
(waiter) => waiter.type === message.type && waiter.predicate(message),
|
||||
);
|
||||
if (waiterIndex >= 0) {
|
||||
const [waiter] = waiters.splice(waiterIndex, 1);
|
||||
clearTimeout(waiter.timeout);
|
||||
waiter.resolve(message);
|
||||
} else {
|
||||
messages.push(message);
|
||||
}
|
||||
});
|
||||
return {
|
||||
next(type, predicate = () => true) {
|
||||
const messageIndex = messages.findIndex(
|
||||
(message) => message.type === type && predicate(message),
|
||||
);
|
||||
if (messageIndex >= 0)
|
||||
return Promise.resolve(messages.splice(messageIndex, 1)[0]);
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error(`Timed out waiting for ${type}`)),
|
||||
2000,
|
||||
);
|
||||
waiters.push({ type, predicate, resolve, timeout });
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const connect = (url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const socket = new WebSocket(url);
|
||||
socket.once("open", () => resolve(socket));
|
||||
socket.once("error", reject);
|
||||
});
|
||||
|
||||
test("a host creates a room and broadcasts one authoritative roll", async () => {
|
||||
const httpServer = createServer();
|
||||
const { wss } = attachDiceSyncServer(httpServer);
|
||||
await new Promise((resolve) => httpServer.listen(0, "127.0.0.1", resolve));
|
||||
const address = httpServer.address();
|
||||
const url = `ws://127.0.0.1:${address.port}/sync`;
|
||||
const host = await connect(url);
|
||||
const guest = await connect(url);
|
||||
const hostInbox = createInbox(host);
|
||||
const guestInbox = createInbox(guest);
|
||||
|
||||
host.send(JSON.stringify({ type: "create" }));
|
||||
const hostState = await hostInbox.next("room-state");
|
||||
const tokenMessage = await hostInbox.next("host-token");
|
||||
assert.equal(hostState.isHost, true);
|
||||
assert.equal(tokenMessage.roomId, hostState.roomId);
|
||||
|
||||
guest.send(JSON.stringify({ type: "join", roomId: hostState.roomId }));
|
||||
const guestState = await guestInbox.next("room-state");
|
||||
assert.equal(guestState.isHost, false);
|
||||
|
||||
const hostRoll = hostInbox.next("rolled");
|
||||
const guestRoll = guestInbox.next("rolled");
|
||||
host.send(JSON.stringify({ type: "roll" }));
|
||||
const [hostMessage, guestMessage] = await Promise.all([hostRoll, guestRoll]);
|
||||
assert.deepEqual(guestMessage.roll, hostMessage.roll);
|
||||
assert.equal(
|
||||
hostMessage.roll.sum,
|
||||
hostMessage.roll.dice[0] + hostMessage.roll.dice[1],
|
||||
);
|
||||
|
||||
const forbidden = guestInbox.next("error");
|
||||
guest.send(JSON.stringify({ type: "roll" }));
|
||||
assert.match((await forbidden).message, /Only the room host/);
|
||||
|
||||
host.terminate();
|
||||
guest.terminate();
|
||||
await new Promise((resolve) => wss.close(resolve));
|
||||
await new Promise((resolve) => httpServer.close(resolve));
|
||||
});
|
||||
|
||||
test("level settings and re-rolls stay authoritative across a classroom", async () => {
|
||||
const httpServer = createServer();
|
||||
const { wss } = attachDiceSyncServer(httpServer);
|
||||
await new Promise((resolve) => httpServer.listen(0, "127.0.0.1", resolve));
|
||||
const address = httpServer.address();
|
||||
const url = `ws://127.0.0.1:${address.port}/sync`;
|
||||
const host = await connect(url);
|
||||
const guest = await connect(url);
|
||||
const hostInbox = createInbox(host);
|
||||
const guestInbox = createInbox(guest);
|
||||
|
||||
host.send(
|
||||
JSON.stringify({
|
||||
type: "create",
|
||||
settings: { gridSize: 9, undoLimit: 0, rerollLimit: 2 },
|
||||
}),
|
||||
);
|
||||
const hostState = await hostInbox.next("room-state");
|
||||
await hostInbox.next("host-token");
|
||||
assert.deepEqual(hostState.settings, {
|
||||
gridSize: 9,
|
||||
undoLimit: 0,
|
||||
rerollLimit: 2,
|
||||
});
|
||||
assert.equal(hostState.rerollsUsed, 0);
|
||||
|
||||
guest.send(JSON.stringify({ type: "join", roomId: hostState.roomId }));
|
||||
const guestState = await guestInbox.next("room-state");
|
||||
assert.deepEqual(guestState.settings, hostState.settings);
|
||||
|
||||
const configuredSettings = {
|
||||
gridSize: 8,
|
||||
undoLimit: 4,
|
||||
rerollLimit: 1,
|
||||
};
|
||||
const hostSettings = hostInbox.next("settings");
|
||||
const guestSettings = guestInbox.next("settings");
|
||||
host.send(
|
||||
JSON.stringify({ type: "configure", settings: configuredSettings }),
|
||||
);
|
||||
const [hostSettingsMessage, guestSettingsMessage] = await Promise.all([
|
||||
hostSettings,
|
||||
guestSettings,
|
||||
]);
|
||||
assert.deepEqual(hostSettingsMessage.settings, configuredSettings);
|
||||
assert.deepEqual(guestSettingsMessage.settings, configuredSettings);
|
||||
|
||||
const forbiddenConfiguration = guestInbox.next("error");
|
||||
guest.send(
|
||||
JSON.stringify({
|
||||
type: "configure",
|
||||
settings: { gridSize: 5, undoLimit: 3, rerollLimit: 3 },
|
||||
}),
|
||||
);
|
||||
assert.match((await forbiddenConfiguration).message, /Only the room host/);
|
||||
|
||||
const firstHostRoll = hostInbox.next("rolled");
|
||||
const firstGuestRoll = guestInbox.next("rolled");
|
||||
host.send(JSON.stringify({ type: "roll" }));
|
||||
const [hostRollMessage] = await Promise.all([firstHostRoll, firstGuestRoll]);
|
||||
|
||||
const hostReroll = hostInbox.next("rerolled");
|
||||
const guestReroll = guestInbox.next("rerolled");
|
||||
host.send(JSON.stringify({ type: "reroll" }));
|
||||
const [hostRerollMessage, guestRerollMessage] = await Promise.all([
|
||||
hostReroll,
|
||||
guestReroll,
|
||||
]);
|
||||
assert.deepEqual(guestRerollMessage.roll, hostRerollMessage.roll);
|
||||
assert.equal(hostRerollMessage.roll.id, hostRollMessage.roll.id);
|
||||
assert.equal(hostRerollMessage.rerollsUsed, 1);
|
||||
|
||||
const exhausted = hostInbox.next("error");
|
||||
host.send(JSON.stringify({ type: "reroll" }));
|
||||
assert.match((await exhausted).message, /no re-rolls remaining/);
|
||||
|
||||
const firstHostReset = hostInbox.next("round-reset");
|
||||
const firstGuestReset = guestInbox.next("round-reset");
|
||||
host.send(JSON.stringify({ type: "reset" }));
|
||||
const [resetMessage] = await Promise.all([firstHostReset, firstGuestReset]);
|
||||
assert.equal(resetMessage.rerollsUsed, 0);
|
||||
|
||||
const secondHostRoll = hostInbox.next("rolled");
|
||||
const secondGuestRoll = guestInbox.next("rolled");
|
||||
host.send(JSON.stringify({ type: "roll" }));
|
||||
await Promise.all([secondHostRoll, secondGuestRoll]);
|
||||
|
||||
const placedUpdate = hostInbox.next("leaderboard", (message) =>
|
||||
message.leaderboard.some(
|
||||
(player) => player.id === guestState.playerId && player.placed === 1,
|
||||
),
|
||||
);
|
||||
guest.send(
|
||||
JSON.stringify({ type: "progress", score: 8, placed: 1, finished: false }),
|
||||
);
|
||||
await placedUpdate;
|
||||
|
||||
const alreadyPlaced = hostInbox.next("error");
|
||||
host.send(JSON.stringify({ type: "reroll" }));
|
||||
assert.match((await alreadyPlaced).message, /already been placed/);
|
||||
|
||||
const secondHostReset = hostInbox.next("round-reset");
|
||||
const secondGuestReset = guestInbox.next("round-reset");
|
||||
host.send(JSON.stringify({ type: "reset" }));
|
||||
await Promise.all([secondHostReset, secondGuestReset]);
|
||||
|
||||
const newcomer = await connect(url);
|
||||
const newcomerInbox = createInbox(newcomer);
|
||||
newcomer.send(JSON.stringify({ type: "join", roomId: hostState.roomId }));
|
||||
const newcomerState = await newcomerInbox.next("room-state");
|
||||
assert.deepEqual(newcomerState.settings, configuredSettings);
|
||||
assert.equal(newcomerState.rerollsUsed, 0);
|
||||
|
||||
host.terminate();
|
||||
guest.terminate();
|
||||
newcomer.terminate();
|
||||
await new Promise((resolve) => wss.close(resolve));
|
||||
await new Promise((resolve) => httpServer.close(resolve));
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue