"""Headless balance simulator: a greedy bot plays every deck against every encounter across many seeds. Outputs a win-rate matrix (markdown report + JSON table). The JSON table is what the director's validator consults when judging whether a deck edit pushes an encounter outside its difficulty band. Run: uv run python -m balance.sim """ from __future__ import annotations import json from collections import Counter from pathlib import Path from scrypt.data import Content, load_content from scrypt.engine.cards import Card # The bot lives in the shipped package (the encounter author sim-gates # against it at runtime); these re-exports keep balance/ callers working. from scrypt.engine.bots import ( # noqa: F401 bot_turn, choose_lane, choose_sacrifices, simulate, wants_fodder, ) from scrypt.engine.combat import Result REPORTS = Path(__file__).parent / "reports" # ------------------------------------------------------------ the matrix def winrate(content: Content, deck: list[Card], encounter_id: str, seeds: int = 100) -> dict: script = content.encounters[encounter_id]["script"] side = [content.card("bit")] * 20 outcomes = Counter() turns_to_win: list[int] = [] for seed in range(seeds): state = simulate(deck, side, script, seed) if state.result is Result.PLAYER_WIN: outcomes["win"] += 1 turns_to_win.append(state.turn + 1) elif state.result is Result.PLAYER_LOSS: outcomes["loss"] += 1 else: outcomes["stall"] += 1 n = sum(outcomes.values()) return { "winrate": outcomes["win"] / n, "stall": outcomes["stall"] / n, "avg_turns_to_win": (sum(turns_to_win) / len(turns_to_win)) if turns_to_win else None, } def standard_decks(content: Content) -> dict[str, list[Card]]: decks: dict[str, list[Card]] = { deck_id: list(deck["cards"]) for deck_id, deck in content.starter_decks.items() } decks["vanilla_drafted"] = decks["vanilla"] + [ content.card(c) for c in ("segfault", "kernel-panic", "root") ] return decks def run_matrix(content: Content | None = None, seeds: int = 100) -> dict: content = content or load_content() table: dict[str, dict[str, dict]] = {} for deck_name, deck in standard_decks(content).items(): table[deck_name] = {} for enc_id in content.encounters: table[deck_name][enc_id] = winrate(content, deck, enc_id, seeds=seeds) return table def render_report(table: dict) -> str: encounters = sorted(next(iter(table.values()))) lines = [ "# Balance report (greedy bot baseline)", "", "Win rate / stall rate / avg turns-to-win. The greedy bot is a floor —", "humans play better, so target bands sit above these numbers.", "", "| deck | " + " | ".join(encounters) + " |", "|---|" + "---|" * len(encounters), ] for deck_name, row in table.items(): cells = [] for enc in encounters: r = row[enc] t = f"{r['avg_turns_to_win']:.0f}t" if r["avg_turns_to_win"] else "—" cells.append(f"{r['winrate']:.0%} / {r['stall']:.0%} / {t}") lines.append(f"| {deck_name} | " + " | ".join(cells) + " |") return "\n".join(lines) + "\n" def main() -> None: table = run_matrix() REPORTS.mkdir(exist_ok=True) (REPORTS / "strength_table.json").write_text(json.dumps(table, indent=2)) report = render_report(table) (REPORTS / "baseline.md").write_text(report) print(report) if __name__ == "__main__": main()