Files
healer-man/server/dungeon-leaderboards.test.mjs
2026-09-27 10:16:42 -04:00

206 lines
15 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { openGameDatabase } from "./database.mjs";
import { createGameServer } from "./server.mjs";
import catalog from "../src/game/generated/dungeonLeaderboardCatalog.generated.json" with { type: "json" };
import { DatabaseSync } from "node:sqlite";
import { createDungeonLeaderboardRepository } from "./dungeon-leaderboards.mjs";
function run(id, durationMs, fullClear = true, dungeonId = "deadmines", difficultyId) {
const dungeon = catalog.dungeons.find((entry) => entry.id === dungeonId);
return { id, dungeonId, difficultyId: difficultyId ?? dungeon.defaultDifficultyId, durationMs,
finishedAt: 1700000000000, killedBossIds: fullClear ? dungeon.bosses.map((boss) => boss.id) : [dungeon.finalBossId],
party: [{ name: "Tester", classId: "priest", level: 20, role: "healer", npc: false },
{ name: "Garrick", classId: "warrior", level: 20, role: "tank", npc: true }] };
}
async function withDatabase(callback) {
const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-leaderboard-test-"));
let database = openGameDatabase({ dataDir: directory });
try { await callback(database, () => { database.close(); database = openGameDatabase({ dataDir: directory }); return database; }); }
finally { database.close(); rmSync(directory, { recursive: true, force: true }); }
}
test("persists ranked party/boss records, derives all-boss eligibility, handles ties, and retries without duplicates", () => withDatabase((database, reopen) => {
database.submitDungeonRun(null, run("run-fastest", 1000, false));
database.submitDungeonRun(null, run("run-full-one", 2000));
database.submitDungeonRun(null, run("run-full-tie", 2000));
const last = run("run-slowest", 3000);
const result = database.submitDungeonRun(null, last);
assert.deepEqual(result.placement, { finalRank: 4, finalTotal: 4, fullRank: 3, fullTotal: 3 });
assert.deepEqual(database.submitDungeonRun(null, last), result);
assert.throws(() => database.submitDungeonRun(null, { ...last, durationMs: 5 }), { status: 409 });
const final = database.listDungeonLeaderboard({ dungeonId: "deadmines", clear: "final" });
assert.deepEqual(final.entries.map((entry) => entry.rank), [1, 2, 2, 4]);
const full = database.listDungeonLeaderboard({ dungeonId: "deadmines", clear: "full" });
assert.deepEqual(full.entries.map((entry) => entry.rank), [1, 1, 3]);
assert.deepEqual(full.entries[0].party, last.party);
assert.equal(full.entries[0].killedBossIds.length, last.killedBossIds.length);
const page = database.listDungeonLeaderboard({ dungeonId: "deadmines", clear: "final", limit: 2, offset: 2 });
assert.equal(page.total, 4); assert.deepEqual(page.entries.map((entry) => entry.rank), [2, 4]);
database = reopen();
assert.deepEqual(database.getDungeonRun(last.id), result);
}));
test("validates final boss, dungeon/difficulty, duration and party without trusting client full-clear flags", () => withDatabase((database) => {
const valid = run("valid-run-id", 1000, false);
for (const invalid of [{ ...valid, durationMs: 0 }, { ...valid, durationMs: Infinity },
{ ...valid, killedBossIds: [] }, { ...valid, killedBossIds: ["fake-boss"] },
{ ...valid, dungeonId: "unknown" }, { ...valid, difficultyId: "fake" },
{ ...valid, party: [] }, { ...valid, party: [{ ...valid.party[0], classId: "fake" }] },
{ ...valid, party: [{ ...valid.party[0], level: 81 }] }])
assert.throws(() => database.submitDungeonRun(null, invalid), { code: "invalid_dungeon_run" });
assert.equal(database.submitDungeonRun(null, { ...valid, fullClear: true }).run.fullClear, false);
const caves = run("barren-caves-test", 2000, true, "barren-caves");
caves.party[0].classId = "rom-priest";
assert.equal(database.submitDungeonRun(null, caves).placement.fullRank, 1);
assert.equal(database.listDungeonLeaderboard({ dungeonId: "deadmines" }).total, 1);
assert.throws(() => database.listDungeonLeaderboard({ dungeonId: "deadmines", clear: "bad" }), { status: 400 });
assert.throws(() => database.listDungeonLeaderboard({ dungeonId: "deadmines", offset: -1 }), { status: 400 });
}));
test("separates each rolled boss's board within its dungeon", () => withDatabase((database, reopen) => {
for (const dungeonId of ["bite-sized-stormwind-stockade", "randomized-bite-sized-stormwind-stockade",
"randomized-bite-sized-stormwind-stockade-available",
"bite-sized-wailing-caverns", "bite-sized-deadmines-ship", "bite-sized-utgarde-pinnacle-skadi",
"bite-sized-forsaken-abbey-ancalon", "bite-sized-paspers-shrine-hafiz", "bite-sized-mystic-altar-golem",
"bite-sized-grafu-castle-barkud", "bite-sized-hoto-teeth", "bite-sized-hall-of-survivors-pursuer"]) {
const dungeon = catalog.dungeons.find((entry) => entry.id === dungeonId);
const difficulty = dungeon.difficulties[0];
assert.equal(difficulty.finalBossIds.length, dungeon.bosses.length);
assert.ok(difficulty.finalBossIds.length > 1);
for (const [index, bossId] of difficulty.finalBossIds.entries()) {
const payload = run(`${dungeonId}-${index}-run`, 1000 + index, false, dungeon.id);
payload.killedBossIds = [bossId];
const submitted = database.submitDungeonRun(null, payload);
assert.equal(submitted.run.fullClear, true);
assert.deepEqual(submitted.placement, { finalRank: 1, finalTotal: 1, fullRank: 1, fullTotal: 1 });
assert.equal(database.listDungeonLeaderboard({ dungeonId, bossId, clear: "full" }).total, 1);
assert.equal(database.listDungeonLeaderboard({ dungeonId, bossId, clear: "full" }).entries[0].id, payload.id);
}
assert.equal(database.listDungeonLeaderboard({ dungeonId: dungeon.id, clear: "full" }).total, 1);
assert.throws(() => database.listDungeonLeaderboard({ dungeonId, bossId: "wrong-boss" }), { status: 400 });
const impossible = run(`${dungeonId}-multiple-run`, 1234, false, dungeonId);
impossible.killedBossIds = difficulty.finalBossIds.slice(0, 2);
assert.throws(() => database.submitDungeonRun(null, impossible), { status: 400 });
}
database = reopen();
const id = "bite-sized-mystic-altar-golem";
const bosses = catalog.dungeons.find((entry) => entry.id === id).difficulties[0].finalBossIds;
assert.equal(database.listDungeonLeaderboard({ dungeonId: id, bossId: bosses[1] }).total, 1);
}));
test("the one-boss RoM slices have their own final-boss boards", () => withDatabase((database) => {
for (const [dungeonId, sourceId] of [
["bite-sized-barren-caves-anglo", "barren-caves"],
["bite-sized-kawaks-tomb-linobia", "kawaks-tomb"],
]) {
const dungeon = catalog.dungeons.find((entry) => entry.id === dungeonId);
const bossId = dungeon.difficulties[0].finalBossIds[0];
assert.equal(dungeon.difficulties[0].finalBossIds.length, 1);
const payload = run(`${dungeonId}-run`, 2000, false, dungeonId);
payload.killedBossIds = [bossId];
assert.equal(database.submitDungeonRun(null, payload).placement.finalRank, 1);
assert.equal(database.listDungeonLeaderboard({ dungeonId, bossId }).total, 1);
assert.equal(database.listDungeonLeaderboard({ dungeonId: sourceId, bossId }).total, 0);
}
}));
test("public HTTP leaderboards accept completion records and return results with rank while rejecting unfinished runs", async () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-leaderboard-http-"));
const { server } = createGameServer({ dataDir: directory, enableShelvedModes: false });
try {
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${server.address().port}`;
const payload = run("http-caves-clear", 15555, true, "barren-caves");
const response = await fetch(`${base}/api/dungeon-runs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
assert.equal(response.status, 201);
const result = await response.json(); assert.equal(result.placement.finalRank, 1);
const leaderboard = await fetch(`${base}/api/dungeon-leaderboards?dungeon=barren-caves&clear=full`);
assert.equal(leaderboard.status, 200); assert.equal((await leaderboard.json()).entries[0].durationMs, 15555);
assert.deepEqual(await (await fetch(`${base}/api/dungeon-runs/${payload.id}`)).json(), result);
const slider = { ...payload, id: "http-slider-clear", bossDifficulty: 10 };
const sliderResponse = await fetch(`${base}/api/dungeon-runs`, { method: "POST",
headers: { "Content-Type": "application/json" }, body: JSON.stringify(slider) });
assert.equal(sliderResponse.status, 201);
const sliderBoard = await fetch(`${base}/api/dungeon-leaderboards?dungeon=barren-caves&clear=full&bossDifficulty=10`);
assert.equal(sliderBoard.status, 200);
assert.deepEqual((await sliderBoard.json()).entries.map((entry) => entry.id), [slider.id]);
assert.equal((await (await fetch(`${base}/api/dungeon-leaderboards?dungeon=barren-caves&bossDifficulty=1`)).json()).total, 0);
assert.equal((await fetch(`${base}/api/dungeon-leaderboards?dungeon=barren-caves&bossDifficulty=11`)).status, 400);
const romDungeon = catalog.dungeons.find((entry) => entry.id === "bite-sized-mystic-altar-golem");
const romBoss = romDungeon.difficulties[0].finalBossIds[1];
const romRun = run("http-rom-boss-run", 23456, false, romDungeon.id);
romRun.killedBossIds = [romBoss];
assert.equal((await fetch(`${base}/api/dungeon-runs`, { method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify(romRun) })).status, 201);
const romBoard = await fetch(`${base}/api/dungeon-leaderboards?dungeon=${romDungeon.id}&boss=${romBoss}`);
assert.equal(romBoard.status, 200);
assert.deepEqual((await romBoard.json()).entries.map((entry) => entry.id), [romRun.id]);
assert.equal((await (await fetch(`${base}/api/dungeon-leaderboards?dungeon=${romDungeon.id}`)).json()).total, 0);
const bad = await fetch(`${base}/api/dungeon-runs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...payload, id: "unfinished-run", killedBossIds: [] }) });
assert.equal(bad.status, 400);
const unauthorized = await fetch(`${base}/api/dungeon-runs`, { method: "POST", headers: { Authorization: "Bearer invalid", "Content-Type": "application/json" }, body: JSON.stringify(payload) });
assert.equal(unauthorized.status, 401);
} finally { await new Promise((resolve) => server.close(resolve)); rmSync(directory, { recursive: true, force: true }); }
});
test("separates normal and heroic times and counts only bosses that exist on each difficulty", () => withDatabase((database) => {
const dungeon = catalog.dungeons.find((entry) => entry.difficulties.length > 1 && entry.difficulties.every((difficulty) => difficulty.finalBossId));
for (const difficulty of dungeon.difficulties) {
const payload = run(`difficulty-${difficulty.id}-test`, 1000, true, dungeon.id, difficulty.id);
payload.killedBossIds = difficulty.bossIds;
const result = database.submitDungeonRun(null, payload);
assert.equal(result.run.fullClear, true);
assert.equal(result.placement.finalRank, 1); assert.equal(result.placement.finalTotal, 1);
assert.equal(database.listDungeonLeaderboard({ dungeonId: dungeon.id, difficultyId: difficulty.id, clear: "full" }).total, 1);
}
}));
test("isolates ten boss difficulties and legacy scores, preserving retries and ranks after restart", () => withDatabase((database, reopen) => {
database.submitDungeonRun(null, run("legacy-fastest-run", 1));
for (let difficulty = 1; difficulty <= 10; difficulty += 1) {
const payload = { ...run(`slider-level-${difficulty}`, 10000 + difficulty), bossDifficulty: difficulty };
const submitted = database.submitDungeonRun(null, payload);
assert.deepEqual(submitted.placement, { finalRank: 1, finalTotal: 1, fullRank: 1, fullTotal: 1 });
assert.deepEqual(database.submitDungeonRun(null, payload), submitted);
assert.throws(() => database.submitDungeonRun(null, { ...payload, bossDifficulty: difficulty === 10 ? 1 : 10 }), { status: 409 });
}
database = reopen();
for (let bossDifficulty = 1; bossDifficulty <= 10; bossDifficulty += 1) {
const page = database.listDungeonLeaderboard({ dungeonId: "deadmines", bossDifficulty, clear: "full" });
assert.equal(page.total, 1); assert.equal(page.entries[0].bossDifficulty, bossDifficulty);
assert.equal(database.getDungeonRun(page.entries[0].id).placement.finalRank, 1);
}
assert.equal(database.listDungeonLeaderboard({ dungeonId: "deadmines" }).total, 1);
for (const bossDifficulty of [0, -1, 11, 1.5, "2", NaN]) {
assert.throws(() => database.submitDungeonRun(null, { ...run("invalid-slider-run", 1000), bossDifficulty }), { status: 400 });
}
}));
test("migrates the old leaderboard schema and keeps its scores outside Easy", () => {
const database = new DatabaseSync(":memory:");
try {
database.exec(`CREATE TABLE accounts (id TEXT PRIMARY KEY);
CREATE TABLE dungeon_runs (id TEXT PRIMARY KEY, dungeon_id TEXT NOT NULL, difficulty_id TEXT NOT NULL,
duration_ms INTEGER NOT NULL, full_clear INTEGER NOT NULL, finished_at INTEGER NOT NULL,
party_json TEXT NOT NULL, bosses_json TEXT NOT NULL, submitted_by_account_id TEXT, received_at INTEGER NOT NULL)`);
const legacy = run("pre-slider-result", 5000);
const insert = database.prepare("INSERT INTO dungeon_runs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
insert.run(legacy.id,
legacy.dungeonId, legacy.difficultyId, legacy.durationMs, 1, legacy.finishedAt,
JSON.stringify(legacy.party), JSON.stringify(legacy.killedBossIds), null, Date.now());
const oldShip = run("pre-single-boss-ship", 6000, false, "bite-sized-deadmines-ship");
oldShip.killedBossIds = ["creature-79333", "creature-79336"];
insert.run(oldShip.id, oldShip.dungeonId, oldShip.difficultyId, oldShip.durationMs, 1,
oldShip.finishedAt, JSON.stringify(oldShip.party), JSON.stringify(oldShip.killedBossIds), null, Date.now());
const repository = createDungeonLeaderboardRepository(database, { ErrorType: Error });
assert.equal(repository.listDungeonLeaderboard({ dungeonId: "deadmines" }).total, 1);
assert.equal(repository.listDungeonLeaderboard({ dungeonId: "deadmines", bossDifficulty: 1 }).total, 0);
assert.equal(repository.listDungeonLeaderboard({ dungeonId: oldShip.dungeonId,
bossId: "creature-79336" }).total, 1);
assert.equal(repository.listDungeonLeaderboard({ dungeonId: oldShip.dungeonId,
bossId: "creature-79333" }).total, 0);
assert.equal(repository.getDungeonRun(oldShip.id).placement.finalTotal, 1);
createDungeonLeaderboardRepository(database, { ErrorType: Error });
assert.equal(repository.getDungeonRun(legacy.id).run.durationMs, 5000);
} finally { database.close(); }
});