Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shweetandClaude Opus 5.5 committed 2026-11-08 01:30:33 -05:00
commit 2007fb7edc
200 files changed
+27710

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
# Plays many seeded bot matches at once (the game's --balance runner, one process per seed) and adds up their tallies,
# flagging any weapon outside 45-55% win rate. Seeds start at SEED_BASE (default 100), so a run is repeatable and a
# fresh base checks results don't just fit one set of seeds.
#
# python tools/balance_runs.py [matches per process] [processes] [-- extra game arguments, like race]
import math
import os
import pathlib
import re
import subprocess
import sys
import tempfile
from concurrent.futures import ThreadPoolExecutor
ROOT = pathlib.Path(__file__).resolve().parent.parent
GAME = ROOT / "build" / "smack-a-brother"
TARGET = 0.05
def run(seed, matches, extra, prefs):
env = dict(os.environ, RPG_SEED=str(seed), SDL_VIDEO_DRIVER="dummy", SDL_AUDIO_DRIVER="none", XDG_DATA_HOME=prefs)
result = subprocess.run([str(GAME), "--balance", str(matches)] + extra, env=env, capture_output=True, text=True)
return result.stdout + result.stderr
def main():
args = sys.argv[1:]
extra = args[args.index("--") + 1:] if "--" in args else []
args = args[:args.index("--")] if "--" in args else args
matches = int(args[0]) if args else 2500
processes = int(args[1]) if len(args) > 1 else os.cpu_count()
base = int(os.environ.get("SEED_BASE", "100"))
with tempfile.TemporaryDirectory() as prefs, ThreadPoolExecutor(processes) as pool:
outputs = list(pool.map(lambda i: run(base + i, matches, extra, prefs), range(processes)))
tallies = {"Weapons": {}, "Trinkets": {}}
total = stalls = 0
for output in outputs:
section = None
for line in output.splitlines():
if line.startswith("Weapons"):
section = "Weapons"
elif line.startswith("Trinkets"):
section = "Trinkets"
row = re.match(r"\s+(.+?)\s+[\d.]+% \+-\s*[\d.]+\s+\((\d+) wins / (\d+) picks, (\d+) stalls\)", line)
if row and section:
tally = tallies[section].setdefault(row.group(1), [0, 0, 0])
for i in range(3):
tally[i] += int(row.group(2 + i))
summary = re.search(r"(\d+) matches in .*?(\d+) hit the", line)
if summary:
total += int(summary.group(1))
stalls += int(summary.group(2))
print(f"{total} matches, {stalls} stalled ({100 * stalls / max(total, 1):.1f}%), seeds {base}-{base + processes - 1}")
for section, tally in tallies.items():
print(section)
for name, (wins, picks, stalled) in sorted(tally.items(), key=lambda item: -item[1][0] / max(item[1][1], 1)):
rate = wins / max(picks, 1)
margin = 1.96 * math.sqrt(rate * (1 - rate) / max(picks, 1))
flag = " <--" if section == "Weapons" and abs(rate - 0.5) > TARGET else ""
print(f" {name:16} {100 * rate:5.1f}% +-{100 * margin:4.1f} ({picks} picks, {stalled} stalls){flag}")
if __name__ == "__main__":
main()