Update Healer Man server

This commit is contained in:
phenom committed 2026-09-26 22:02:18 -04:00
1 parent 8a98797fcf
commit 11934a3979
44 files changed
+3183 -329

No files matched your search

+68 -5
View File
@@ -5,14 +5,21 @@ 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 { botOpponent, botProgress, botTierForRecord, hashRaceSeed } from "./dungeon-race-rules.mjs";
import { createGameServer } from "./server.mjs";
import { createDungeonRaceRepository } from "./dungeon-races.mjs";
import { DatabaseSync } from "node:sqlite";
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];
const bossIdsFor = (match) => {
const difficulty = catalog.dungeons.find((entry) => entry.id === match.dungeonId)
.difficulties.find((entry) => entry.id === match.difficultyId);
const ids = difficulty.finalBossIds ?? [difficulty.finalBossId];
const seed = match.dungeonId.startsWith("randomized-bite-sized-")
? `${match.seed}:boss` : match.seed;
return [ids[hashRaceSeed(seed) % ids.length]];
};
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"]) {
@@ -27,6 +34,32 @@ test("NPC tiers follow rolling records with seeded adjacent variation and milest
}
});
test("migrates existing race matches onto their seeded boss boards", () => {
const database = new DatabaseSync(":memory:");
try {
database.exec(`CREATE TABLE accounts (id TEXT PRIMARY KEY);
CREATE TABLE dungeon_race_matches (id TEXT PRIMARY KEY, dungeon_id TEXT NOT NULL,
seed TEXT NOT NULL, difficulty_id TEXT NOT NULL, boss_difficulty INTEGER NOT NULL,
status TEXT NOT NULL, created_at INTEGER NOT NULL, start_at INTEGER, settled_at INTEGER,
winner_side INTEGER, bot_json TEXT)`);
const insert = database.prepare(`INSERT INTO dungeon_race_matches
(id, dungeon_id, seed, difficulty_id, boss_difficulty, status, created_at, settled_at)
VALUES (?, ?, ?, ?, 1, 'settled', 1, 2)`);
const dungeonIds = ["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"];
for (const dungeonId of dungeonIds) insert.run(`race-old-${dungeonId}`, dungeonId,
`race-old-${dungeonId}`, dungeon.defaultDifficultyId);
createDungeonRaceRepository(database, { ErrorType: Error });
for (const dungeonId of dungeonIds) {
const seed = `race-old-${dungeonId}`;
assert.equal(database.prepare("SELECT boss_id FROM dungeon_race_matches WHERE id = ?").get(seed).boss_id,
bossIdsFor({ dungeonId, difficultyId: dungeon.defaultDifficultyId, seed })[0]);
}
} finally { database.close(); }
});
async function fixture(run) {
const dataDir = mkdtempSync(path.join(os.tmpdir(), "healer-man-race-"));
const database = openGameDatabase({ dataDir });
@@ -57,6 +90,9 @@ test("random and specific tickets pair across WoW and RoM, then a later losing c
assert.equal(service.ready(rom, paired.matchId).status, "countdown");
advance(3_000);
assert.equal(service.state(wow).status, "active");
const bossIds = bossIdsFor(paired);
assert.throws(() => service.finish(wow, paired.matchId, { runId: paired.seed,
bossIds: [dungeon.difficulties[0].finalBossIds.find((id) => id !== bossIds[0])] }), { status: 400 });
const first = service.finish(wow, paired.matchId, { runId: paired.seed, bossIds });
assert.equal(first.result, "win");
assert.equal(service.state(rom).result, "loss");
@@ -64,10 +100,37 @@ test("random and specific tickets pair across WoW and RoM, then a later losing c
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(database.listDungeonRaceLeaderboard({ view: "times", period: "all", dungeonId: dungeon.id,
bossId: bossIds[0] }, time()).total, 2);
const otherBossId = dungeon.difficulties[0].finalBossIds.find((id) => id !== bossIds[0]);
assert.equal(database.listDungeonRaceLeaderboard({ view: "times", period: "all", dungeonId: dungeon.id,
bossId: otherBossId }, time()).total, 0);
assert.equal(database.listDungeonRaceLeaderboard({ view: "wins", period: "all", dungeonId: dungeon.id,
bossId: bossIds[0] }, time()).total, 2);
assert.equal(service.finish(rom, paired.matchId, { runId: paired.seed, bossIds }).clearDurationMs, late.clearDurationMs);
}));
test("both randomized creature pools validate and rank their cross-dungeon final boss", async () => fixture(async ({ database, service, player, advance, time }) => {
for (const [index, dungeonId] of ["randomized-bite-sized-stormwind-stockade",
"randomized-bite-sized-stormwind-stockade-available"].entries()) {
const left = await player(`race_random_${index}_a`);
const right = await player(`race_random_${index}_b`);
service.queue(left, { characterId: `race_random_${index}_a-character`, role: "healer", pool: "randomized", dungeonId });
const match = service.queue(right, { characterId: `race_random_${index}_b-character`, role: "healer", pool: "randomized", dungeonId });
assert.equal(match.dungeonId, dungeonId);
service.ready(left, match.matchId); service.ready(right, match.matchId);
advance(3_000);
const bossIds = bossIdsFor(match);
assert.match(bossIds[0], /^randomized:(wow|rom):boss:/);
const wrongBossId = catalog.dungeons.find((entry) => entry.id === dungeonId).difficulties[0]
.finalBossIds.find((id) => id !== bossIds[0]);
assert.throws(() => service.finish(left, match.matchId, { runId: match.seed, bossIds: [wrongBossId] }), { status: 400 });
assert.equal(service.finish(left, match.matchId, { runId: match.seed, bossIds }).result, "win");
assert.equal(database.listDungeonRaceLeaderboard({ view: "times", period: "all", dungeonId,
bossId: bossIds[0] }, time()).total, 1);
}
}));
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" });