Add multiplayer rooms and Vercel analytics
This commit is contained in:
163
scripts/test-multiplayer-http.mjs
Normal file
163
scripts/test-multiplayer-http.mjs
Normal 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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user