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(); } });