import assert from "node:assert/strict"; import { test } from "node:test"; import { mkdtempSync, rmSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { openGameDatabase } from "./database.mjs"; import { createDungeonRaceService } from "./dungeon-race-service.mjs"; import { botOpponent, botProgress, botTierForRecord } from "./dungeon-race-rules.mjs"; import { createGameServer } from "./server.mjs"; import catalog from "../src/game/generated/dungeonLeaderboardCatalog.generated.json" with { type: "json" }; const dungeon = catalog.dungeons.find((entry) => entry.id === "bite-sized-stormwind-stockade"); const bossIds = [dungeon.difficulties[0].finalBossId]; const bossIdsFor = (match) => [catalog.dungeons.find((entry) => entry.id === match.dungeonId) .difficulties.find((entry) => entry.id === match.difficultyId).finalBossId]; test("NPC tiers follow rolling records with seeded adjacent variation and milestones", () => { for (const seed of ["race-tier-a", "race-tier-b", "race-tier-c", "race-tier-d"]) { assert.ok(["novice", "regular", "veteran"].includes(botTierForRecord(0, 0, seed))); assert.ok(["novice", "regular"].includes(botTierForRecord(0, 20, seed))); assert.ok(["veteran", "elite"].includes(botTierForRecord(20, 0, seed))); const bot = botOpponent(seed, "regular", "rom"); assert.equal(bot.roster.length, 5); assert.equal(botProgress(0, bot), 0); assert.equal(botProgress(bot.finishMs, bot), 100); assert.ok(botProgress(bot.finishMs / 2, bot) > 48); } }); async function fixture(run) { const dataDir = mkdtempSync(path.join(os.tmpdir(), "healer-man-race-")); const database = openGameDatabase({ dataDir }); let clock = Date.now(); const service = createDungeonRaceService({ database, now: () => clock, autoPump: false }); async function player(username, source = "wow") { const { account } = await database.register(username, "test-password"); database.putCloudSave(account.id, { version: 2, characters: [{ id: `${username}-character`, name: username, categoryId: source, classId: source === "rom" ? "rom-priest" : "priest", specializationId: source === "rom" ? undefined : "priest:discipline", level: 30 }] }); return account; } const advance = (duration) => { clock += duration; service.pump(); }; try { await run({ database, service, player, advance, time: () => clock }); } finally { service.close(); database.close(); rmSync(dataDir, { recursive: true, force: true }); } } test("random and specific tickets pair across WoW and RoM, then a later losing clear still ranks", async () => fixture(async ({ database, service, player, advance, time }) => { const wow = await player("race_wow"); const rom = await player("race_rom", "rom"); assert.equal(service.queue(wow, { characterId: "race_wow-character", role: "healer", pool: "authored" }).status, "queued"); const paired = service.queue(rom, { characterId: "race_rom-character", role: "healer", pool: "authored", dungeonId: dungeon.id }); assert.equal(paired.status, "preparing"); assert.equal(service.state(wow).seed, paired.seed); assert.equal(paired.opponent.source, "wow"); service.ready(wow, paired.matchId); assert.equal(service.ready(rom, paired.matchId).status, "countdown"); advance(3_000); assert.equal(service.state(wow).status, "active"); const first = service.finish(wow, paired.matchId, { runId: paired.seed, bossIds }); assert.equal(first.result, "win"); assert.equal(service.state(rom).result, "loss"); advance(1_000); const late = service.finish(rom, paired.matchId, { runId: paired.seed, bossIds }); assert.equal(late.result, "loss"); assert.equal(database.listDungeonRaceLeaderboard({ view: "wins", period: "30d" }, time()).entries[0].username, "race_wow"); assert.equal(database.listDungeonRaceLeaderboard({ view: "times", period: "all", dungeonId: dungeon.id }, time()).total, 2); assert.equal(service.finish(rom, paired.matchId, { runId: paired.seed, bossIds }).clearDurationMs, late.clearDurationMs); })); test("five-second unmatched queue fills with a modeled NPC party", async () => fixture(async ({ database, service, player, advance, time }) => { const account = await player("race_solo"); service.queue(account, { characterId: "race_solo-character", role: "healer", pool: "authored" }); advance(4_999); assert.equal(service.state(account).status, "queued"); advance(1); const state = service.state(account); assert.equal(state.status, "preparing"); assert.equal(state.opponent.type, "bot"); assert.equal(state.opponent.roster.length, 5); assert.equal(state.opponent.milestones.length, 5); service.ready(account, state.matchId); advance(3_000 + state.opponent.finishMs); assert.equal(service.state(account).result, "loss"); assert.deepEqual(database.listDungeonRaceLeaderboard({ period: "all" }, time()).entries .map((entry) => ({ username: entry.username, wins: entry.wins, losses: entry.losses })), [{ username: "race_solo", wins: 0, losses: 1 }]); })); test("a missing human during preparation becomes a bot without charging the ready player", async () => fixture(async ({ service, player, advance }) => { const left = await player("race_ready"); const right = await player("race_missing"); service.queue(left, { characterId: "race_ready-character", role: "healer", pool: "authored" }); const match = service.queue(right, { characterId: "race_missing-character", role: "healer", pool: "authored" }); service.ready(left, match.matchId); advance(5 * 60_000); assert.equal(service.state(left).opponent.type, "bot"); assert.equal(service.state(right).status, "idle"); assert.equal(service.state(left).status, "countdown"); })); test("leaving during the countdown replaces the rival without a result", async () => fixture(async ({ service, player, advance }) => { const left = await player("race_cancel_a"); const right = await player("race_cancel_b"); service.queue(left, { characterId: "race_cancel_a-character", role: "healer", pool: "authored" }); const match = service.queue(right, { characterId: "race_cancel_b-character", role: "healer", pool: "authored" }); service.ready(left, match.matchId); service.ready(right, match.matchId); assert.equal(service.leave(left, match.matchId).status, "idle"); assert.equal(service.state(right).opponent.type, "bot"); assert.equal(service.state(right).result, null); advance(3_000); assert.equal(service.state(right).status, "active"); })); test("the 30-day window expires while all-time wins and times remain", async () => fixture(async ({ database, service, player, advance, time }) => { const left = await player("race_window_a"); const right = await player("race_window_b"); service.queue(left, { characterId: "race_window_a-character", role: "healer", pool: "authored" }); const match = service.queue(right, { characterId: "race_window_b-character", role: "healer", pool: "authored" }); service.ready(left, match.matchId); service.ready(right, match.matchId); advance(3_000); service.finish(left, match.matchId, { runId: match.seed, bossIds: bossIdsFor(match) }); assert.equal(database.listDungeonRaceLeaderboard({ period: "30d" }, time()).total, 2); advance(30 * 24 * 60 * 60_000 + 1); assert.equal(database.listDungeonRaceLeaderboard({ period: "30d" }, time()).total, 0); assert.equal(database.listDungeonRaceLeaderboard({ period: "all" }, time()).total, 2); })); test("an active disconnect forfeits after the grace period and the other player can still clear", async () => fixture(async ({ service, player, advance }) => { const left = await player("race_drop_a"); const right = await player("race_drop_b"); service.queue(left, { characterId: "race_drop_a-character", role: "healer", pool: "authored" }); const match = service.queue(right, { characterId: "race_drop_b-character", role: "healer", pool: "authored" }); service.ready(left, match.matchId); service.ready(right, match.matchId); advance(3_000); service.state(right); advance(29_000); assert.equal(service.state(left).result, "loss"); assert.equal(service.state(right).result, "win"); assert.equal(service.finish(right, match.matchId, { runId: match.seed, bossIds: bossIdsFor(match) }).result, "win"); })); test("production HTTP routes expose ranked queue and public boards without shelved modes", async () => { const dataDir = mkdtempSync(path.join(os.tmpdir(), "healer-man-race-http-")); const runtime = createGameServer({ dataDir, staticDir: dataDir, contentDir: dataDir, enableShelvedModes: false }); await new Promise((resolve) => runtime.server.listen(0, "127.0.0.1", resolve)); const base = `http://127.0.0.1:${runtime.server.address().port}`; try { const auth = await (await fetch(`${base}/api/auth/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "race_http", password: "test-password" }) })).json(); const headers = { "Content-Type": "application/json", Authorization: `Bearer ${auth.token}` }; await fetch(`${base}/api/cloud-save`, { method: "PUT", headers, body: JSON.stringify({ data: { version: 2, characters: [{ id: "http-character", name: "Runner", classId: "priest", categoryId: "wow", level: 30 }] } }) }); const response = await fetch(`${base}/api/dungeon-races/queue`, { method: "POST", headers, body: JSON.stringify({ characterId: "http-character", role: "healer", pool: "authored" }) }); assert.equal(response.status, 200); assert.equal((await response.json()).status, "queued"); assert.equal((await fetch(`${base}/api/dungeon-races/state`, { headers })).status, 200); assert.equal((await fetch(`${base}/api/dungeon-races/leaderboard?period=30d`)).status, 200); assert.equal((await fetch(`${base}/api/dungeon-races/queue`, { method: "DELETE", headers })).status, 200); } finally { await new Promise((resolve) => runtime.server.close(resolve)); rmSync(dataDir, { recursive: true, force: true }); } });