#!/usr/bin/env python3 """Summarize an arena bot-war from lua.log BAL telemetry, with a verdict. Usage: balance_report.py [path-to-lua.log] Reports the economy/army curve, who led when, and flags the specific failure modes we have actually hit before: * economy flatline (income cannot sustain spending — wax seep rate too low) * runaway (one side >2x the other for a sustained stretch) * stalled army (both sides sitting on cash without building units) """ import os, re, sys path = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser('~/Library/Application Support/OpenRA/Logs/lua.log') rows, events = [], [] for line in open(path): m = re.search(r'BAL t=(\d+) (\w+) cash=(\d+) stored=(\d+) units=(\d+) buildings=(\d+)', line) if m: rows.append(dict(t=int(m[1]), who=m[2], cash=int(m[3]), stored=int(m[4]), units=int(m[5]), buildings=int(m[6]))) m = re.search(r'BAL (?:t=(\d+) )?(\w+) (ELIMINATED)', line) if m: events.append((int(m[1] or 0), m[2], m[3])) m = re.search(r'BAL (WON|LOST): (\w+)', line) if m: events.append((0, m[2], m[1])) if not rows: sys.exit('no BAL telemetry found') bots = sorted({r['who'] for r in rows}) ticks = sorted({r['t'] for r in rows}) def at(t, who): return next((r for r in rows if r['t'] == t and r['who'] == who), None) print(f'{"tick":>6} {"~min":>5} ' + ''.join(f'{b + " cash/stored/u/b":>26}' for b in bots)) step = max(1, len(ticks) // 14) for t in ticks[::step]: line = f'{t:>6} {t / 1500:>5.1f} ' for b in bots: r = at(t, b) line += f'{f"{r['cash']}/{r['stored']}/{r['units']}/{r['buildings']}" if r else "-":>26}' print(line) print('\nfinal state:') for b in bots: r = [x for x in rows if x['who'] == b][-1] print(f' {b}: cash={r["cash"]} stored={r["stored"]} units={r["units"]} buildings={r["buildings"]}') for t, who, what in events: print(f' *** {who} {what}' + (f' at tick {t} (~{t / 1500:.1f} min)' if t else '')) # ---- verdict ------------------------------------------------------------- print('\nverdict:') issues = [] # Don't draw conclusions from an opening. Both bots are still teching at 3 min # and every heuristic below misfires on that. if ticks[-1] < 7500: print(f' DATA TOO THIN — match only reached {ticks[-1] / 1500:.1f} min. ' 'Let it run past 5 min before trusting any verdict.') sys.exit(0) # economy flatline: both sides broke for a sustained late stretch late = [r for r in rows if r['t'] >= ticks[len(ticks) // 2]] broke = [r for r in late if r['cash'] + r['stored'] < 200] if len(broke) >= max(4, len(late) // 3): issues.append(f'ECONOMY FLATLINE — {len(broke)}/{len(late)} late samples under 200 banked. ' 'Raise wax seep rate (SeedsResource Interval) or add seeps.') # runaway: one side dominating on army for a sustained stretch if len(bots) == 2: a, b = bots lead = 0 for t in ticks: ra, rb = at(t, a), at(t, b) if ra and rb: hi, lo = max(ra['units'], rb['units']), min(ra['units'], rb['units']) if hi >= 6 and lo * 2 < hi: lead += 1 if lead >= len(ticks) // 3: issues.append(f'RUNAWAY — one side held >2x army on {lead}/{len(ticks)} samples. ' 'Check unit cost/HP and the AI UnitsToBuild weights.') # stalled army: lots of money, nobody building rich_idle = [r for r in late if r['cash'] + r['stored'] > 3000 and r['units'] < 4] if len(rich_idle) >= max(3, len(late) // 4): issues.append(f'STALLED PRODUCTION — {len(rich_idle)} samples rich but under 4 units. ' 'AI is banking instead of spending; check UnitLimits / factory count.') if issues: for i in issues: print(' ! ' + i) else: print(' no flatline, no runaway, no production stall detected.')