import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { loadCanonicalBenchmarkBuilds } from "../../src/game/benchmark/benchmarkBuilds"; import { BENCHMARK_POLICY_ID } from "../../src/game/benchmark/benchmarkController"; import { runBiteSizedTrashBenchmark, runDungeonBossBenchmark } from "../../src/game/benchmark/dungeonBossBenchmark"; import { PLAYER_AGGRO_ID } from "../../src/game/aggro"; import { BITE_SIZED_STOCKADES_DUNGEON_ID, BITE_SIZED_WAILING_CAVERNS_DUNGEON_ID, BITE_SIZED_DEADMINES_DUNGEON_ID, BITE_SIZED_UTGARDE_PINNACLE_DUNGEON_ID, BITE_SIZED_FORSAKEN_ABBEY_DUNGEON_ID, BITE_SIZED_PASPERS_SHRINE_DUNGEON_ID, BITE_SIZED_MYSTIC_ALTAR_DUNGEON_ID, BITE_SIZED_GRAFU_CASTLE_DUNGEON_ID, BITE_SIZED_BARREN_CAVES_DUNGEON_ID, BITE_SIZED_HOTO_DUNGEON_ID, BITE_SIZED_KAWAKS_TOMB_DUNGEON_ID, BITE_SIZED_HALL_OF_SURVIVORS_DUNGEON_ID, dungeonContentSourceId, } from "../../src/game/dungeonContentSource"; import { requireDungeonDefinition } from "../../src/game/dungeonRegistry"; import { BITE_SIZED_STOCKADES_BOSS_CANDIDATES } from "../../src/game/dungeons/biteSizedStockades"; import { seededBossIndex } from "../../src/game/dungeons/seededBossSelection"; const ids = [ BITE_SIZED_STOCKADES_DUNGEON_ID, BITE_SIZED_WAILING_CAVERNS_DUNGEON_ID, BITE_SIZED_DEADMINES_DUNGEON_ID, BITE_SIZED_UTGARDE_PINNACLE_DUNGEON_ID, BITE_SIZED_FORSAKEN_ABBEY_DUNGEON_ID, BITE_SIZED_PASPERS_SHRINE_DUNGEON_ID, BITE_SIZED_MYSTIC_ALTAR_DUNGEON_ID, BITE_SIZED_GRAFU_CASTLE_DUNGEON_ID, BITE_SIZED_BARREN_CAVES_DUNGEON_ID, BITE_SIZED_HOTO_DUNGEON_ID, BITE_SIZED_KAWAKS_TOMB_DUNGEON_ID, BITE_SIZED_HALL_OF_SURVIVORS_DUNGEON_ID, ] as const; function valueAfter(flag: string): string | null { const index = process.argv.indexOf(flag); return index < 0 ? null : process.argv[index + 1] ?? null; } function selectedLevels(): number[] { if (process.argv.includes("--all-levels")) return Array.from({ length: 80 }, (_, index) => index + 1); const requested = valueAfter("--levels"); const levels = requested ? requested.split(",").map(Number) : [1, 10, 20, 30, 40, 50, 60, 70, 80]; if (levels.some((level) => !Number.isInteger(level) || level < 1 || level > 80)) { throw new Error("Levels must be integers from 1 through 80."); } return [...new Set(levels)].sort((left, right) => left - right); } function bossCatalog(dungeonId: string) { const candidates = dungeonId === BITE_SIZED_STOCKADES_DUNGEON_ID ? BITE_SIZED_STOCKADES_BOSS_CANDIDATES.map((boss) => ({ id: boss.sourceSpawnId, name: boss.name })) : requireDungeonDefinition(dungeonContentSourceId(dungeonId)).bosses; const found = new Map(); for (let index = 0; index < 2_000 && found.size < candidates.length; index += 1) { const seed = `bite-sized-balance-${index}`; const candidateIndex = seededBossIndex(seed, candidates.length); if (!found.has(candidateIndex)) found.set(candidateIndex, seed); } if (found.size !== candidates.length) { throw new Error(`${dungeonId}: ${candidates.length - found.size} source bosses could not be selected.`); } return candidates.map((boss, index) => ({ dungeonId, id: boss.id, name: boss.name, seed: found.get(index)! })); } async function main(): Promise { const selectedDungeons = (valueAfter("--dungeons")?.split(",") ?? ids).filter((id) => ids.includes(id as typeof ids[number])); const bosses = selectedDungeons.flatMap(bossCatalog); if (process.argv.includes("--catalog")) { console.log(JSON.stringify(bosses, null, 2)); return; } const levels = selectedLevels(); const output = resolve(valueAfter("--output") ?? "artifacts/bite-sized-balance/ready-priest-boss-sweep.json"); const previous = process.argv.includes("--resume") && existsSync(output) ? JSON.parse(readFileSync(output, "utf8")) as { results?: Record[]; trashResults?: Record[]; } : null; const results: Record[] = previous?.results ?? []; const trashResults: Record[] = previous?.trashResults ?? []; const includeBosses = !process.argv.includes("--trash-only"); const includeTrash = process.argv.includes("--trash") || process.argv.includes("--trash-only"); const writeReport = () => { mkdirSync(resolve(output, ".."), { recursive: true }); writeFileSync(output, `${JSON.stringify({ method: "Deterministic isolated boss simulation with live stats, companion AI, and baseline Discipline Priest rotation; no boss attacks or travel time.", scope: "All authored Bite-Sized dungeon boss choices; one fixed party seed; normal difficulty; 150-second timeout.", policy: BENCHMARK_POLICY_ID, levels, bosses, results, trashResults, }, null, 2)}\n`); }; for (const level of levels) { if (results.filter((result) => result.level === level).length >= (includeBosses ? bosses.length : 0) && trashResults.filter((result) => result.level === level).length >= (includeTrash ? selectedDungeons.length : 0)) { console.log(`Level ${level}: resumed from checkpoint.`); continue; } const build = (await loadCanonicalBenchmarkBuilds(level)).find((candidate) => ( candidate.classId === "priest" && candidate.role === "healer" )); if (!build) throw new Error(`No Ready discipline priest build exists at level ${level}.`); for (const boss of includeBosses ? bosses : []) { if (results.some((result) => result.level === level && result.bossId === boss.id && result.dungeonId === boss.dungeonId)) continue; const result = await runDungeonBossBenchmark(build, boss.id, boss.seed, false, { dungeonId: boss.dungeonId, durationMs: 150_000, partySeed: "bite-sized-balance-party", }); const seats = [ { id: PLAYER_AGGRO_ID, label: "Player · Discipline Priest" }, ...result.party.map((label, index) => ({ id: result.partyActorIds[index], label: `Companion ${index + 1} · ${label}`, })), ]; results.push({ level, dungeonId: boss.dungeonId, bossId: boss.id, bossName: boss.name, seconds: result.seconds, outcome: result.outcome, elapsedSeconds: result.elapsedSeconds, maxHealth: result.maxHealth, remainingHealth: "remainingHealth" in result ? result.remainingHealth : 0, damageByActor: result.damageByActor, seats, damageByAbility: result.damageByAbility, party: result.party }); } if (includeTrash) { for (const dungeonId of selectedDungeons) { if (trashResults.some((result) => result.level === level && result.dungeonId === dungeonId)) continue; const sample = bosses.find((boss) => boss.dungeonId === dungeonId)!; const result = await runBiteSizedTrashBenchmark(build, dungeonId, sample.seed, { partySeed: "bite-sized-balance-party", durationMs: 120_000, }); trashResults.push({ level, dungeonId, ...result, seats: [{ id: PLAYER_AGGRO_ID, label: "Player · Discipline Priest" }, ...result.party.map((label, index) => ({ id: result.partyActorIds[index], label: `Companion ${index + 1} · ${label}` }))] }); } } writeReport(); console.log(`Level ${level}: ${includeBosses ? bosses.length : 0} bosses and ${includeTrash ? selectedDungeons.length : 0} trash pulls measured.`); } writeReport(); console.log(output); } await main();