Round 4: balance-arena harness with lua telemetry, minimap radar, damage smoke, harvester-attack warning, Needle Drop + Compression maps

Fixes a critical bug: undeployed MCV vans now count for short-game survival —
previously every match ended at tick 1 (all players 'defeated', local player
'won'). The bot-vs-bot shellmap claim from earlier rounds was invalid for the
same reason: shellmaps stall with Bot: players and matches insta-ended. The
arena map + tools/balance_report.py is the real, verified harness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-21 02:09:20 +10:00
parent 4761245c61
commit 975f305fa1
23 changed files with 324 additions and 22 deletions

View File

@ -198,7 +198,7 @@ faction, or it reads as clip-art soup. Fix the Blender camera rig once, reuse.
Learned the hard way on 2026-07-20 (phases AF):
- **Smoke-test any change by launching straight into a game**: `./launch-game.sh Game.Mod=vinylgod Launch.Map=vinylgod``Launch.Map` accepts the map's *folder name*, no UID needed. Run ~45s, then check `~/Library/Application Support/OpenRA/Logs/` for `exception-*.log` (renamed with a timestamp on crash — absence of `exception.log` alone proves nothing).
- **The shellmap is the integration test**: it runs a bot-vs-bot match (BotA MRP vs BotB Stream, `Bot: wax` in map.yaml) behind the main menu. If a trait wiring is broken, the menu crashes within seconds.
- **Shellmaps FREEZE with bot players** (learned 2026-07-21): a `Bot:` player on a shellmap stalls the order manager after one world tick — the menu *looks* fine but nothing simulates. Keep the shellmap a static diorama. The real integration test is `maps/arena` (Lobby-hidden map with two `Bot: wax` players + `balance.lua` telemetry): launch with `Launch.Map=arena`, wait, then `python3 tools/balance_report.py` reads economy/army/eliminations from lua.log. A map validation error (e.g. bad `Enemies:` reference) can also silently stall a match at tick 1 — always get check-yaml to zero before trusting a run.
- Chrome image collections must be **power-of-two** textures (a 48×48 png crashes the renderer).
- Every rules file must use the same case for shared top-level actors: `player:` not `Player:` — mismatched case = "duplicate values" manifest error.
- Speech notification `Sounds`/`Speech` blocks: set `DefaultVariant: .wav` (engine default is `.aud`).

Binary file not shown.

View File

@ -16,6 +16,18 @@ Container@PLAYER_WIDGETS:
Height: 23
Align: Right
Font: TinyBold
Container@RADAR_BOX:
X: WINDOW_WIDTH - 240
Y: WINDOW_HEIGHT - 240
Width: 232
Height: 232
Children:
Radar@RADAR_MINIMAP:
X: 4
Y: 4
Width: 224
Height: 224
WorldInteractionController: INTERACTION_CONTROLLER
Container@SUPPORT_POWERS:
X: 10
Y: 45

View File

@ -0,0 +1,40 @@
WorldLoaded = function()
print("BAL script loaded")
for _, p in ipairs(Player.GetPlayers(nil)) do
Trigger.OnPlayerWon(p, function(pl) print("BAL WON: "..pl.InternalName) end)
Trigger.OnPlayerLost(p, function(pl) print("BAL LOST: "..pl.InternalName) end)
end
end
-- Balance telemetry: logs bot economy/army every ~10s to lua.log.
-- Read with: grep BAL ~/Library/Application Support/OpenRA/Logs/lua.log
local tick = 0
local dead = {}
Tick = function()
tick = tick + 1
if tick <= 3 then print("BAL tick "..tick) end
if tick == 251 then print("BAL reached 251") end
if tick % 250 ~= 0 then
return
end
local bots = Player.GetPlayers(function(p) return p.InternalName == "BotA" or p.InternalName == "BotB" end)
for _, p in ipairs(bots) do
local units, buildings = 0, 0
for _, a in ipairs(Map.ActorsInWorld) do
if a.Owner == p then
if a.HasProperty("Move") then
units = units + 1
else
buildings = buildings + 1
end
end
end
print(string.format("BAL t=%d %s cash=%d stored=%d units=%d buildings=%d",
tick, p.InternalName, p.Cash, p.Resources, units, buildings))
if units + buildings == 0 and not dead[p.InternalName] then
dead[p.InternalName] = true
print(string.format("BAL t=%d %s ELIMINATED", tick, p.InternalName))
end
end
end

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

View File

@ -0,0 +1,62 @@
MapFormat: 12
RequiresMod: vinylgod
Title: Balance Arena
Author: Monster Robot Party
Tileset: VINYLGOD
MapSize: 34,34
Bounds: 1,1,32,32
Visibility: MissionSelector
Categories: Tests
Players:
PlayerReference@Neutral:
Name: Neutral
OwnsWorld: True
NonCombatant: True
Faction: Random
PlayerReference@Multi0:
Name: Multi0
Playable: True
LockFaction: True
Faction: vinylgod
Enemies: BotA, BotB
PlayerReference@BotA:
Name: BotA
Faction: vinylgod
Bot: wax
Enemies: BotB, Multi0
PlayerReference@BotB:
Name: BotB
Faction: stream
Bot: wax
Enemies: BotA, Multi0
Actors:
Actor0: mpspawn
Owner: Multi0
Location: 16,3
Actor9: vault
Owner: Multi0
Location: 16,2
Actor1: popupvan
Owner: BotA
Location: 7,7
Actor2: digger
Owner: BotA
Location: 9,8
Actor3: streamvan
Owner: BotB
Location: 26,26
Actor4: scraper
Owner: BotB
Location: 24,25
Rules: rules.yaml

View File

@ -0,0 +1,3 @@
world:
LuaScript:
Scripts: balance.lua

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,78 @@
MapFormat: 12
RequiresMod: vinylgod
Title: The Compression
Author: Monster Robot Party
Tileset: VINYLGOD
MapSize: 92,92
Bounds: 1,1,90,90
Visibility: Lobby
Categories: Maps
Players:
PlayerReference@Neutral:
Name: Neutral
OwnsWorld: True
NonCombatant: True
Faction: Random
PlayerReference@Creeps:
Name: Creeps
NonCombatant: True
Faction: Random
PlayerReference@Multi0:
Name: Multi0
Playable: True
Faction: Random
Enemies: Creeps
PlayerReference@Multi1:
Name: Multi1
Playable: True
Faction: Random
Enemies: Creeps
PlayerReference@Multi2:
Name: Multi2
Playable: True
Faction: Random
Enemies: Creeps
PlayerReference@Multi3:
Name: Multi3
Playable: True
Faction: Random
Enemies: Creeps
PlayerReference@Multi4:
Name: Multi4
Playable: True
Faction: Random
Enemies: Creeps
PlayerReference@Multi5:
Name: Multi5
Playable: True
Faction: Random
Enemies: Creeps
Actors:
Actor0: mpspawn
Owner: Multi0
Location: 9,9
Actor1: mpspawn
Owner: Multi1
Location: 82,9
Actor2: mpspawn
Owner: Multi2
Location: 9,82
Actor3: mpspawn
Owner: Multi3
Location: 82,82
Actor4: mpspawn
Owner: Multi4
Location: 45,7
Actor5: mpspawn
Owner: Multi5
Location: 45,84

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

View File

@ -0,0 +1,46 @@
MapFormat: 12
RequiresMod: vinylgod
Title: Needle Drop
Author: Monster Robot Party
Tileset: VINYLGOD
MapSize: 42,42
Bounds: 1,1,40,40
Visibility: Lobby
Categories: Maps
Players:
PlayerReference@Neutral:
Name: Neutral
OwnsWorld: True
NonCombatant: True
Faction: Random
PlayerReference@Creeps:
Name: Creeps
NonCombatant: True
Faction: Random
PlayerReference@Multi0:
Name: Multi0
Playable: True
Faction: Random
Enemies: Creeps
PlayerReference@Multi1:
Name: Multi1
Playable: True
Faction: Random
Enemies: Creeps
Actors:
Actor0: mpspawn
Owner: Multi0
Location: 7,20
Actor1: mpspawn
Owner: Multi1
Location: 34,20

View File

@ -26,27 +26,29 @@ Players:
Name: Creeps
NonCombatant: True
Faction: Random
PlayerReference@BotA:
Name: BotA
Faction: vinylgod
Bot: wax
Enemies: BotB
PlayerReference@BotB:
Name: BotB
Faction: stream
Bot: wax
Enemies: BotA
Actors:
Actor1: popupvan
Owner: BotA
Location: 7,7
Actor1: pressplant
Owner: Creeps
Location: 12,12
Actor2: digger
Owner: BotA
Location: 9,8
Actor3: streamvan
Owner: BotB
Location: 26,26
Actor4: scraper
Owner: BotB
Location: 24,25
Owner: Creeps
Location: 15,14
Actor3: ampstack
Owner: Creeps
Location: 10,14
Actor4: popupshop
Owner: Creeps
Location: 18,12
Actor5: hornpit
Owner: Creeps
Location: 16,16
Actor6: streamhub
Owner: Creeps
Location: 24,24
Actor7: transcoder
Owner: Creeps
Location: 21,25
Actor8: adblocker
Owner: Creeps
Location: 25,22

View File

@ -18,3 +18,4 @@ Speech:
ShuffleReady: audio/mc_shuffleready
SelectTarget: audio/mc_selecttarget
Incoming: audio/mc_incoming
HarvesterAttack: audio/mc_harvattack

View File

@ -55,6 +55,8 @@ digger:
popupvan:
Inherits: ^Unit
MustBeDestroyed:
RequiredForShortGame: true
Voiced:
VoiceSet: MrpVoice
-WithSpriteBody:
@ -103,6 +105,7 @@ djgrunt:
^EconBuilding:
Inherits: ^EconSprite
WithDamageOverlay:
FireWarheadsOnDeath:
Weapon: BuildingExplode
Targetable:
@ -150,6 +153,7 @@ djgrunt:
popupshop:
Inherits: ^EconBuilding
Inherits@shape: ^Buildable2x2
ProvidesRadar:
Tooltip:
Name: actor-popupshop.name
Health:

View File

@ -30,7 +30,10 @@ player:
WinNotification: Win
LoseNotification: Lose
BaseAttackNotifier:
HarvesterAttackNotifier:
Notification: HarvesterAttack
SupportPowerManager:
ScriptTriggers:
editorplayer:
Inherits: ^baseplayer

View File

@ -2,6 +2,8 @@
streamvan:
Inherits: ^Unit
MustBeDestroyed:
RequiredForShortGame: true
Voiced:
VoiceSet: StreamVoice
-WithSpriteBody:
@ -29,6 +31,7 @@ streamvan:
streamhub:
Inherits: ^EconBuilding
Inherits@shape: ^Buildable2x2
ProvidesRadar:
Tooltip:
Name: actor-streamhub.name
Health:

View File

@ -45,6 +45,7 @@ world:
BuildingInfluence:
PathFinder:
MapOptions:
ScriptTriggers:
Locomotor@WHEELED:
Name: wheeled
TerrainSpeeds:

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

View File

@ -200,3 +200,16 @@ algorithm:
Filename: sequences/assets/algorithm.png
icon:
Filename: sequences/assets/icon_algorithm.png
smoke_m:
idle:
Filename: sequences/assets/smoke.png
Length: 2
loop:
Filename: sequences/assets/smoke.png
Start: 2
Length: 4
end:
Filename: sequences/assets/smoke.png
Start: 6
Length: 2

34
tools/balance_report.py Normal file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Summarize a shellmap bot-war from lua.log BAL telemetry.
Usage: balance_report.py [path-to-lua.log]
"""
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 = []
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((int(m[1]), m[2], int(m[3]), int(m[4]), int(m[5]), int(m[6])))
m = re.search(r'BAL t=(\d+) (\w+) ELIMINATED', line)
if m:
print(f'*** {m[2]} ELIMINATED at tick {m[1]} (~{int(m[1]) // 25}s)')
if not rows:
sys.exit('no BAL telemetry found')
bots = sorted({r[1] for r in rows})
print(f'{"tick":>6} ' + ''.join(f'{b + " $/store/u/b":>28}' for b in bots))
ticks = sorted({r[0] for r in rows})
for t in ticks[:: max(1, len(ticks) // 12)]:
line = f'{t:>6} '
for b in bots:
r = next((x for x in rows if x[0] == t and x[1] == b), None)
line += f'{f"{r[2]}/{r[3]}/{r[4]}/{r[5]}" if r else "-":>28}'
print(line)
print('\nfinal:')
for b in bots:
r = [x for x in rows if x[1] == b][-1]
print(f' {b}: cash={r[2]} stored={r[3]} units={r[4]} buildings={r[5]}')