AI balance pass driven by arena telemetry
Two fair-map matches showed MRP annihilating Stream by ~5min. Build-order telemetry (new BALTYPES histogram in balance.lua) found the cause: Stream's podfarm arrived at 5.8min vs MRP's merchtable at 1.7min, so the cheap-swarm faction fielded 1 bufferbot against 17 djgrunts, and Stream spent 1500cr on the Algorithm superweapon before it had an army. Fixes: * BuildingFractions: barracks 1 -> 10, factories 1 -> 6 so army buildings come before tech. Result: podfarm 5.8min -> 2.5min, algorithm 3.3min -> 5.8min, and Stream survived to 9.5min with 13 buildings instead of being wiped at 5.2. * BuildingDelays on mastering/algorithm (4500) — no superweapon before an army. * UnitsToBuild shares normalised. Reading UnitBuilderBotModule showed these are PERCENTAGES of army composition, and mine summed to 233% per faction, which made unit selection collapse to iteration order and starved Stream of infantry. balance_report.py now emits a verdict (flatline / runaway / stalled production) and refuses to judge matches under 5 minutes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6847a47fea
commit
39ca9c78a5
@ -32,6 +32,26 @@ Tick = function()
|
|||||||
end
|
end
|
||||||
print(string.format("BAL t=%d %s cash=%d stored=%d units=%d buildings=%d",
|
print(string.format("BAL t=%d %s cash=%d stored=%d units=%d buildings=%d",
|
||||||
tick, p.InternalName, p.Cash, p.Resources, units, buildings))
|
tick, p.InternalName, p.Cash, p.Resources, units, buildings))
|
||||||
|
|
||||||
|
-- type histogram: which buildings/units does this bot actually have?
|
||||||
|
if tick % 1250 == 0 then
|
||||||
|
local kinds = {}
|
||||||
|
local order = {}
|
||||||
|
for _, a in ipairs(Map.ActorsInWorld) do
|
||||||
|
if a.Owner == p then
|
||||||
|
if kinds[a.Type] == nil then
|
||||||
|
kinds[a.Type] = 0
|
||||||
|
order[#order + 1] = a.Type
|
||||||
|
end
|
||||||
|
kinds[a.Type] = kinds[a.Type] + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local parts = ""
|
||||||
|
for _, k in ipairs(order) do
|
||||||
|
parts = parts .. k .. "=" .. kinds[k] .. " "
|
||||||
|
end
|
||||||
|
print(string.format("BALTYPES t=%d %s %s", tick, p.InternalName, parts))
|
||||||
|
end
|
||||||
if units + buildings == 0 and not dead[p.InternalName] then
|
if units + buildings == 0 and not dead[p.InternalName] then
|
||||||
dead[p.InternalName] = true
|
dead[p.InternalName] = true
|
||||||
print(string.format("BAL t=%d %s ELIMINATED", tick, p.InternalName))
|
print(string.format("BAL t=%d %s ELIMINATED", tick, p.InternalName))
|
||||||
|
|||||||
@ -41,35 +41,48 @@ player:
|
|||||||
droneport: 1
|
droneport: 1
|
||||||
BuildingFractions:
|
BuildingFractions:
|
||||||
pressplant: 30
|
pressplant: 30
|
||||||
|
transcoder: 30
|
||||||
hornpit: 8
|
hornpit: 8
|
||||||
adblocker: 8
|
adblocker: 8
|
||||||
transcoder: 30
|
# barracks first: infantry is the cheapest army and both sides need
|
||||||
merchtable: 1
|
# it early. Telemetry showed Stream getting its podfarm at 5.8min
|
||||||
podfarm: 1
|
# (1 bufferbot fielded) vs MRP's merchtable at 1.7min (17 djgrunts).
|
||||||
garage: 1
|
merchtable: 10
|
||||||
printfarm: 1
|
podfarm: 10
|
||||||
mastering: 1
|
garage: 6
|
||||||
algorithm: 1
|
printfarm: 6
|
||||||
launchramp: 2
|
launchramp: 2
|
||||||
droneport: 2
|
droneport: 2
|
||||||
|
mastering: 1
|
||||||
|
algorithm: 1
|
||||||
|
BuildingDelays:
|
||||||
|
# no superweapon building before there is an army to protect it
|
||||||
|
mastering: 4500
|
||||||
|
algorithm: 4500
|
||||||
UnitBuilderBotModule:
|
UnitBuilderBotModule:
|
||||||
RequiresCondition: enable-wax-ai
|
RequiresCondition: enable-wax-ai
|
||||||
UnitsToBuild:
|
UnitsToBuild:
|
||||||
djgrunt: 60
|
# these are PERCENTAGES of army composition, not weights: the AI
|
||||||
roadie: 5
|
# builds whatever sits furthest below its share. They previously
|
||||||
spinner: 30
|
# summed to 233% per faction, which made selection arbitrary and
|
||||||
stacktank: 45
|
# starved Stream of infantry entirely.
|
||||||
basscannon: 15
|
# MRP ~98
|
||||||
digger: 10
|
digger: 8
|
||||||
bufferbot: 60
|
djgrunt: 28
|
||||||
drmwalker: 45
|
roadie: 3
|
||||||
scraper: 10
|
spinner: 14
|
||||||
boombike: 20
|
stacktank: 20
|
||||||
crawler: 8
|
basscannon: 7
|
||||||
flyer45: 25
|
boombike: 6
|
||||||
compressor: 25
|
flyer45: 10
|
||||||
selector: 5
|
selector: 2
|
||||||
curator: 5
|
# Stream ~98
|
||||||
|
scraper: 8
|
||||||
|
bufferbot: 34
|
||||||
|
crawler: 4
|
||||||
|
drmwalker: 26
|
||||||
|
compressor: 24
|
||||||
|
curator: 2
|
||||||
UnitLimits:
|
UnitLimits:
|
||||||
digger: 4
|
digger: 4
|
||||||
scraper: 4
|
scraper: 4
|
||||||
|
|||||||
@ -1,34 +1,99 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Summarize a shellmap bot-war from lua.log BAL telemetry.
|
"""Summarize an arena bot-war from lua.log BAL telemetry, with a verdict.
|
||||||
|
|
||||||
Usage: balance_report.py [path-to-lua.log]
|
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
|
import os, re, sys
|
||||||
|
|
||||||
path = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser('~/Library/Application Support/OpenRA/Logs/lua.log')
|
path = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser('~/Library/Application Support/OpenRA/Logs/lua.log')
|
||||||
rows = []
|
|
||||||
|
rows, events = [], []
|
||||||
for line in open(path):
|
for line in open(path):
|
||||||
m = re.search(r'BAL t=(\d+) (\w+) cash=(\d+) stored=(\d+) units=(\d+) buildings=(\d+)', line)
|
m = re.search(r'BAL t=(\d+) (\w+) cash=(\d+) stored=(\d+) units=(\d+) buildings=(\d+)', line)
|
||||||
if m:
|
if m:
|
||||||
rows.append((int(m[1]), m[2], int(m[3]), int(m[4]), int(m[5]), int(m[6])))
|
rows.append(dict(t=int(m[1]), who=m[2], cash=int(m[3]), stored=int(m[4]),
|
||||||
m = re.search(r'BAL t=(\d+) (\w+) ELIMINATED', line)
|
units=int(m[5]), buildings=int(m[6])))
|
||||||
|
m = re.search(r'BAL (?:t=(\d+) )?(\w+) (ELIMINATED)', line)
|
||||||
if m:
|
if m:
|
||||||
print(f'*** {m[2]} ELIMINATED at tick {m[1]} (~{int(m[1]) // 25}s)')
|
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:
|
if not rows:
|
||||||
sys.exit('no BAL telemetry found')
|
sys.exit('no BAL telemetry found')
|
||||||
|
|
||||||
bots = sorted({r[1] for r in rows})
|
bots = sorted({r['who'] for r in rows})
|
||||||
print(f'{"tick":>6} ' + ''.join(f'{b + " $/store/u/b":>28}' for b in bots))
|
ticks = sorted({r['t'] for r in rows})
|
||||||
ticks = sorted({r[0] for r in rows})
|
|
||||||
for t in ticks[:: max(1, len(ticks) // 12)]:
|
|
||||||
line = f'{t:>6} '
|
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:
|
for b in bots:
|
||||||
r = next((x for x in rows if x[0] == t and x[1] == b), None)
|
r = at(t, b)
|
||||||
line += f'{f"{r[2]}/{r[3]}/{r[4]}/{r[5]}" if r else "-":>28}'
|
line += f'{f"{r['cash']}/{r['stored']}/{r['units']}/{r['buildings']}" if r else "-":>26}'
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
print('\nfinal:')
|
print('\nfinal state:')
|
||||||
for b in bots:
|
for b in bots:
|
||||||
r = [x for x in rows if x[1] == b][-1]
|
r = [x for x in rows if x['who'] == b][-1]
|
||||||
print(f' {b}: cash={r[2]} stored={r[3]} units={r[4]} buildings={r[5]}')
|
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.')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user