File size: 3,633 Bytes
9fca766
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""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()