Add multiplayer rooms and Vercel analytics

This commit is contained in:
2026-10-07 12:28:45 +05:00
parent ce26e6fdbc
commit 48b34db6f1
20 changed files with 2345 additions and 9 deletions

View File

@@ -0,0 +1,163 @@
import assert from "node:assert/strict";
import { existsSync } from "node:fs";
import { createClient } from "@supabase/supabase-js";
if (existsSync(".env.local")) process.loadEnvFile(".env.local");
const origin = new URL(process.argv[2] || "http://127.0.0.1:3007").origin;
const db = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SECRET_KEY,
{ auth: { persistSession: false, autoRefreshToken: false } },
);
const rooms = [];
function player() {
let cookie;
return async (path, body, expected = 200) => {
const response = await fetch(`${origin}/api/v1/${path}`, {
method: body === undefined ? "GET" : "POST",
headers: {
...(body === undefined
? {}
: { origin, "content-type": "application/json" }),
...(cookie ? { cookie } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(30000),
});
if (response.headers.get("set-cookie"))
cookie = response.headers.get("set-cookie").split(";")[0];
const data = await response.json();
assert.equal(response.status, expected, JSON.stringify({ path, data }));
assert.match(response.headers.get("cache-control"), /no-store/);
return data;
};
}
try {
const host = player(),
friend = player(),
outsider = player();
const catalog = await host("catalog");
const datasetId = catalog.maps[0]?.id;
assert.ok(datasetId, "A published map is required");
const created = await host("rooms", {
datasetId,
difficulty: "hard",
displayName: "HTTP Host",
playerLimit: 2,
});
rooms.push(created.id);
const path = `rooms/${created.id}`;
const hostView = await host(path);
const preview = await outsider(path);
assert.equal(preview.you, null);
assert.deepEqual(preview.rounds, []);
await outsider(`${path}/media`, undefined, 403);
const joined = await friend(`${path}/join`, { displayName: "HTTP Friend" });
assert.equal(
(await friend(`${path}/join`, { displayName: "HTTP Other" })).you,
joined.you,
);
await outsider(`${path}/join`, { displayName: "Overflow" }, 400);
await friend(`${path}/start`, {}, 403);
await host(`${path}/start`, {});
let results;
for (let round = 0; round < 5; round++) {
const [hostMedia, friendMedia] = await Promise.all([
host(`${path}/media`),
friend(`${path}/media`),
]);
assert.equal(hostMedia.round, round);
assert.equal(friendMedia.round, round);
assert.deepEqual(
hostMedia.faces.map((url) => new URL(url).pathname),
friendMedia.faces.map((url) => new URL(url).pathname),
);
assert.equal(hostMedia.faces.length, 6);
if (round === 0)
for (const url of [hostMedia.map, hostMedia.faces[0]])
assert.equal((await fetch(url)).status, 200, "Signed media readable");
await host(`${path}/ready`, { round });
const playing = await friend(`${path}/ready`, { round });
assert.equal(playing.status, "playing");
const synchronized = await host(path);
assert.equal(
synchronized.rounds[round].deadline,
playing.rounds[round].deadline,
);
const pointResult = await db
.from("multiplayer_rounds")
.select("viewpoint")
.eq("room_id", created.id)
.eq("round_number", round)
.single();
if (pointResult.error) throw pointResult.error;
const point = pointResult.data.viewpoint;
const first = await host(`${path}/guess`, {
round,
x: point.x,
z: point.z,
});
assert.equal(first.status, "playing");
assert.deepEqual(first.rounds[round].results, []);
assert.equal(
(await host(path)).players.find((p) => p.id === hostView.you).submitted,
true,
"Reconnect preserves submitted guess",
);
await host(`${path}/guess`, {
round,
x: hostMedia.bounds.minX,
z: hostMedia.bounds.minZ,
});
const b = hostMedia.bounds;
const x = point.x - (b.minX + b.maxX) / 2 >= 0 ? b.minX : b.maxX;
const z = point.z - (b.minZ + b.maxZ) / 2 >= 0 ? b.minZ : b.maxZ;
results = await friend(`${path}/guess`, { round, x, z });
assert.equal(results.status, round === 4 ? "finished" : "results");
assert.equal(
results.rounds[round].results.find((r) => r.playerId === hostView.you)
.score,
5000,
);
if (round < 4) await host(`${path}/next`, { round });
}
assert.equal(results.players[0].id, hostView.you);
assert.equal(results.players[0].total, 25000);
assert.equal(results.rounds.flatMap((round) => round.results).length, 10);
const capacityHost = player();
const full = await capacityHost("rooms", {
datasetId,
difficulty: "normal",
displayName: "Capacity Host",
playerLimit: 16,
});
rooms.push(full.id);
await Promise.all(
Array.from({ length: 14 }, (_, i) =>
player()(`rooms/${full.id}/join`, { displayName: `Capacity ${i + 2}` }),
),
);
const attempts = await Promise.allSettled([
player()(`rooms/${full.id}/join`, { displayName: "Last A" }),
player()(`rooms/${full.id}/join`, { displayName: "Last B" }),
]);
assert.equal(
attempts.filter((result) => result.status === "fulfilled").length,
1,
"Exactly one of the simultaneous last-slot joins succeeds",
);
const rejection = attempts.find((result) => result.status === "rejected");
assert.match(rejection.reason.message, /This room is full/);
assert.equal((await capacityHost(`rooms/${full.id}`)).playerCount, 16);
console.log(
"Multiplayer HTTP smoke passed: private invite joining, shared signed media and timers, five-round ranking, reconnects, retries, and simultaneous 16-player capacity checks.",
);
} finally {
if (rooms.length) {
const { error } = await db
.from("multiplayer_rooms")
.delete()
.in("id", rooms);
if (error)
throw new Error(`Multiplayer test-room cleanup failed: ${error.message}`);
}
}