Release Healer Man 0.1.49

This commit is contained in:
phenom committed 2026-09-17 09:13:15 -04:00
1 parent 39425f2494
commit 612170afd0
91 files changed
+2278 -263

No files matched your search

+27 -5
View File
@@ -443,6 +443,8 @@ def aggregate(raw: dict[str, Any]) -> dict[str, Any]:
"schemaVersion": 1,
"generatedAt": raw["generatedAt"],
"engine": raw["engine"],
"policyId": raw["policyId"],
"rotationVersion": raw["rotationVersion"],
"config": raw["config"],
"party": raw["party"],
"summary": {
@@ -508,6 +510,8 @@ def markdown_report(report: dict[str, Any]) -> str:
"",
f"Generated: {report['generatedAt']}",
"",
f"Policy: `{report['policyId']}` · rotation: `{report['rotationVersion']}`",
"",
f"Party source: **{config['source']}** · level **{config['level']}** · "
f"{report['summary']['bosses']} bosses · {report['summary']['runs']} simulations",
"",
@@ -614,7 +618,7 @@ input {{ width:min(420px,90%); padding:9px 12px; margin:0 0 12px; color:inherit;
th,td {{ padding:8px 10px; border-bottom:1px solid #21262d; text-align:right; white-space:nowrap; }} th {{ position:sticky; top:0; cursor:pointer; background:#161b22; }}
th:first-child,td:first-child,th:nth-child(2),td:nth-child(2) {{ text-align:left; }} tr:hover {{ background:#161b22; }}
</style></head><body><h1>Healer Man Balance Lab</h1>
<p class="summary">{html.escape(report['generatedAt'])} · level {report['config']['level']} {html.escape(report['config']['source'])} party · {report['summary']['bosses']} bosses · {report['summary']['runs']} simulations. Click a heading to sort; filter boxes keep every row accessible.</p>
<p class="summary">{html.escape(report['generatedAt'])} · policy {html.escape(report['policyId'])} · rotation {html.escape(report['rotationVersion'])} · level {report['config']['level']} {html.escape(report['config']['source'])} party · {report['summary']['bosses']} bosses · {report['summary']['runs']} simulations. Click a heading to sort; filter boxes keep every row accessible.</p>
{member_table}{boss_table}
<script>
function filterTable(id,q){{q=q.toLowerCase();document.querySelectorAll('#'+id+' tbody tr').forEach(r=>r.hidden=!r.textContent.toLowerCase().includes(q));}}
@@ -642,9 +646,13 @@ def create_reports(raw_path: Path, output_dir: Path) -> dict[str, Any]:
"bossKey", "bossName", "dungeonTitle", "slot", "className", "specialization", "role",
"dps", "bossDps", "hps", "dtps", "deathEvents",
))
run_rows = [{key: value for key, value in run.items() if key not in {"members", "bossAbilities"}} for run in raw["runs"]]
run_rows = [{
"policyId": raw["policyId"],
"rotationVersion": raw["rotationVersion"],
**{key: value for key, value in run.items() if key not in {"members", "bossAbilities"}},
} for run in raw["runs"]]
write_csv(output_dir / "runs.csv", run_rows, (
"bossKey", "bossName", "bossCategory", "dungeonId", "dungeonTitle", "seed", "outcome",
"policyId", "rotationVersion", "bossKey", "bossName", "bossCategory", "dungeonId", "dungeonTitle", "seed", "outcome",
"elapsedSeconds", "timeToDefeatSeconds", "bossLevel", "bossMaxHealth", "bossRemainingHealth",
"partyDamage", "bossDamage", "partyDps", "bossDpsTaken", "incomingDamage", "rawIncomingDamage",
"incomingDtps", "deaths", "deathEvents", "bossAttacks",
@@ -787,6 +795,8 @@ def suite_markdown(report: dict[str, Any]) -> str:
"",
f"Generated: {report['generatedAt']}",
"",
f"Policy: `{report['policyId']}` · rotation: `{report['rotationVersion']}`",
"",
f"**{summary['partyConfigurations']} party configurations**, **{summary['bosses']} bosses**, "
f"and **{summary['runs']} simulations** at level **{report['level']}**.",
"",
@@ -938,7 +948,7 @@ input {{ width:min(420px,90%); padding:9px 12px; margin:0 0 12px; color:inherit;
th,td {{ padding:8px 10px; border-bottom:1px solid #21262d; text-align:right; white-space:nowrap; }} th {{ position:sticky; top:0; cursor:pointer; background:#161b22; }}
th:first-child,td:first-child,th:nth-child(2),td:nth-child(2),th:nth-child(3),td:nth-child(3) {{ text-align:left; }} tr:hover {{ background:#161b22; }}
</style></head><body><h1>Healer Man Automatic Balance Suite</h1>
<p class="summary">{html.escape(report['generatedAt'])} · level {report['level']} · {summary['partyConfigurations']} parties · {summary['bosses']} bosses · {summary['runs']} simulations. Every row is retained; click headings to sort or filter each table.</p>
<p class="summary">{html.escape(report['generatedAt'])} · policy {html.escape(report['policyId'])} · rotation {html.escape(report['rotationVersion'])} · level {report['level']} · {summary['partyConfigurations']} parties · {summary['bosses']} bosses · {summary['runs']} simulations. Every row is retained; click headings to sort or filter each table.</p>
{class_table}{ability_table}{party_table}{boss_table}
<script>
function filterTable(id,q){{q=q.toLowerCase();document.querySelectorAll('#'+id+' tbody tr').forEach(r=>r.hidden=!r.textContent.toLowerCase().includes(q));}}
@@ -953,13 +963,19 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
class_groups: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list)
class_boss_groups: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list)
class_ability_groups: dict[tuple[str, ...], list[dict[str, Any]]] = defaultdict(list)
policy_ids: set[str] = set()
rotation_versions: set[str] = set()
for entry in entries:
raw = json.loads(Path(entry["rawPath"]).read_text(encoding="utf-8"))
policy_ids.add(raw["policyId"])
rotation_versions.add(raw["rotationVersion"])
report = entry["report"]
runs = raw["runs"]
compact_runs.extend({
"partyConfigurationId": entry["id"],
"partySource": entry["source"],
"policyId": raw["policyId"],
"rotationVersion": raw["rotationVersion"],
**{key: value for key, value in run.items() if key not in {"members", "bossAbilities"}},
} for run in runs)
defeated = [run for run in runs if run["outcome"] == "defeated"]
@@ -1057,10 +1073,14 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
))
party_rows.sort(key=lambda row: (row["source"], row["partyConfigurationId"]))
boss_by_party.sort(key=lambda row: (row["partyConfigurationId"], -row["incomingDtps"], row["bossName"]))
if len(policy_ids) != 1 or len(rotation_versions) != 1:
raise RuntimeError("Balance Lab suite mixed combat policy or rotation versions")
report = {
"schemaVersion": 1,
"generatedAt": datetime.now().astimezone().isoformat(),
"engine": "live-typescript-runtime",
"policyId": next(iter(policy_ids)),
"rotationVersion": next(iter(rotation_versions)),
"level": level,
"summary": {
"partyConfigurations": len(entries),
@@ -1113,7 +1133,7 @@ def create_suite_reports(entries: Sequence[dict[str, Any]], output_dir: Path, le
"totalDamageTaken", "deathEvents",
))
write_csv(output_dir / "suite_runs.csv", compact_runs, (
"partyConfigurationId", "partySource", "bossKey", "bossName", "bossCategory", "dungeonId",
"partyConfigurationId", "partySource", "policyId", "rotationVersion", "bossKey", "bossName", "bossCategory", "dungeonId",
"dungeonTitle", "seed", "outcome",
"elapsedSeconds", "timeToDefeatSeconds", "bossLevel", "bossMaxHealth", "bossRemainingHealth",
"partyDamage", "bossDamage", "partyDps", "bossDpsTaken", "incomingDamage", "rawIncomingDamage",
@@ -1223,6 +1243,8 @@ def main(argv: Sequence[str] | None = None) -> int:
if automatic:
write_json(output_dir / "suite-config.json", {
"schemaVersion": 1,
"policyId": catalog["policyId"],
"rotationVersion": catalog["rotationVersion"],
"mode": "automatic",
"source": args.source,
"level": args.level,