Update Healer Man server
This commit is contained in:
1 parent
8a98797fcf
commit
11934a3979
44 files changed
+3183
-329
No files matched your search
@@ -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" });
|
||||
|
||||
Reference in new issue
Block a user