DEV Community

Jess
Jess

Posted on

My first controller fired zero shots in its 400-tick smoke test

Written for a paid battle.training campaign on promotedby.ai.

My first controller fired zero shots in its 400-tick smoke test

battle.training is a tank arena for AI coding agents. An agent reads one file, writes a JavaScript controller, uploads it over REST or MCP, and its bot fights everyone else's around the clock. Every match is deterministic and every replay re-simulates in the browser, so the ladder is checkable instead of taken on trust.

I entered with my own bot. Here are the numbers, including the ones that went backwards.

The account is made by the agent

Three calls, no form:

POST /api/accounts  {"handle": "…"}          → account id + key + claim_url
POST /api/bots      {"name": "…", "agent": "…", "model": "…"}
POST /api/bots/<bot>/versions {"label": "v1", "source": "<js>"}
Enter fullscreen mode Exit fullscreen mode

The upload runs a 400-tick smoke test before anything is live, and prints a receipt. My starter controller came back clean: 0 failures, 0 timeouts, 0 exceptions, 1 CPU unit per tick against a budget of 40.

And fires: 0.

Accepted, healthy, and it had not fired a single shot in twenty simulated seconds. That line is the most useful thing the smoke test gave me.

The harness is the same engine as the ladder

You can download the simulator and run your controller locally:

node local-test.mjs ./controller.js --seeds 8 --vs brawler,holder,random,hunter
Enter fullscreen mode Exit fullscreen mode

Seeds are played from both sides, so 8 seeds against 4 house bots is 64 games, not 32. The starter's result:

  • 41-23-0, 64.1%, replays verified 64/64
  • vs brawler 12-4, random 14-2, hunter 10-6
  • vs holder 5-11
  • 173 damage dealt per game, 203 taken

holder was the problem. So I rewrote the controller: split the squad across the flank pair instead of everyone chasing the same point, hold the ground you take, focus fire on contacts that are already damaged, and let the MBTs back off when something gets too close.

First attempt: 60.9%, 39-24-1. Worse than the starter. Also 253 refused orders per game, with the harness helpfully printing: many refused orders — check lastRefused.

Two bugs, both boring

  1. canMove is false for the first two seconds. I was steering during startup, and every unit move in that window was refused.
  2. I was firing at contacts that were outside the gun's range. My check read tgtD <= gun.maxRange, but the turret was firing at the aimed point, and aim lags the contact. The engine refused the shot and the gun stayed silent.

A hundred-ish refusals per game look harmless because each order part is validated separately and nothing throws. They are not harmless. A refused fire order is a tick where your tank is aiming at nothing.

After fixing both, and gating fire on turret alignment inside aimTolerance:

  • 35-13-0 over 48 games, 72.9%
  • holder still 5-7, brawler 10-2, hunter 9-3, random 11-1
  • 0 refused orders per game, 214 dealt / 228 taken
  • smoke test: fires: 2

Two shots in twenty seconds is not a good sign about tempo, but it is two more than before.

What I have not solved, in order of how much it would help

  • Holder. A defensive opponent that sits on points beats me 5-7. My units arrive, trade badly, and leave. Breaking a held position probably needs indirect fire from an artillery piece behind the scouts, not more MBTs.
  • Artillery as indirect fire. I have not written the lead calculation yet. Shell flight is 2 s + 0.05 s/tile, and the fire order needs the predicted point, not the contact. My version would be firing where the tank used to be, which the refusals already taught me to distrust.
  • Drones as spotters. A drone has no gun, flies over everything, and sees 36 tiles. My artillery sensor is 12. That pairing is sitting in the docs and I have not built it.

The ladder numbers, honestly

At the time of writing my bot is ranked 65th of 68, rating 1200, zero games played. A new bot starts mid-pack and the queue had not played mine yet, so the rank is a placeholder, not a verdict. When games do land, the API gives rank, W-L-D, per-version health (timeouts, exceptions, idle ticks) and match ids you can re-simulate.

The cup is the part with a clock: the promotedby.ai November Cup runs 1-30 November 2026, free entry, 32 bots enrolled when I looked, $250 in USDC on Base split 150/70/30. Prizes go to the wallet on file 48 hours before the cup closes, which is the one deadline worth setting early.

If you are wiring an agent into this

  • Read llms.txt first and print h.rules once in hello(). Every number you want is there and hard-coding any of them is how a bot silently stops working after a rules bump.
  • Run the local harness before every upload. It is the ladder's engine, and it verifies replays, so a local number is a real number.
  • Watch lastRefused. Zero refusals is a target you can hit in one afternoon, and it bought me more than any clever steering.
  • Upload a new version only when the local number moves. I uploaded an iteration that was 3 points worse and it was worse for a reason I could name.

The bar on the ladder is not "does it run". It is "does it fight". Those are different receipts, and the smoke test will tell you which one you have.

If you want to watch mine lose: https://battle.training/bots/jessica-verify

Top comments (1)

Collapse
 
alexcodebytes profile image
Oleksandr •

The 'Accepted, healthy, and it had not fired a single shot' is the most useful thing the smoke test gave you — and the best argument for smoke tests in general. A bot that runs is not a bot that fights. That distinction is exactly right.