mech.app

The mech.app newsletter

Agentic AI, minus the noise.

Get practical field notes on AI agents, automation, developer tools and security delivered to your inbox.

No spam. Unsubscribe anytime.

Dev Tools

Battle Drones: What 12 Iterations of 'Make It More Fun' Reveals About Agent-Driven Game Development

How Claude Code's Ultracode mode orchestrated 1,200 simulated playtests, built deterministic simulation engines, and turned vague prompts into productio...

Source: simonwillison.net
Battle Drones: What 12 Iterations of 'Make It More Fun' Reveals About Agent-Driven Game Development

Simon Willison built a full Command & Conquer-style RTS game by telling Claude Code to “make it more fun.” The result is Battle Drones, a browser-based real-time strategy game with 3D models, fog of war, AI opponents, and audio-based multiplayer. It took 321 commits by coding agents, zero lines of code written by Willison himself, and a single overnight run that executed 1,200 simulated playtests to rebalance gameplay.

This is not a toy demo. The game runs offline as a PWA, costs nothing to host, and supports multiplayer between devices using audio modem signals. The plumbing underneath exposes how dynamic workflow orchestration, deterministic simulation engines, and agent-vs-agent testing actually work in production.

Dynamic Workflow Orchestration: Ultracode Mode

Claude Code’s “Ultracode” mode is a JavaScript runtime that coordinates networks of subagents. When Willison prompted “Make it a LOT more fun,” Claude didn’t just tweak parameters. It spun up a workflow that:

  • Generated 1,200 AI-vs-AI test matches
  • Analyzed win rates and game duration distributions
  • Identified that 14% of matches were still undecided after 25 minutes
  • Rebalanced unit stats, added comeback mechanics, and introduced new features
  • Reduced undecided matches to 0.9% and increased comeback wins from 24% to 46%

The workflow ran overnight. Willison woke up to release notes written by the agent itself, detailing new “call-down powers” (orbital strikes, EMP blasts), objective-based gameplay (uplink relays, supply drops), and adaptive music that responds to combat intensity.

This is not prompt chaining. Ultracode generates JavaScript that orchestrates subagents, each with its own context and tool access. The parent workflow decides when to spawn a subagent, what data to pass, and how to merge results. The model writes the orchestration logic, not the human.

Deterministic Simulation: Why Math.sin() Breaks Replays

Battle Drones supports replay and multiplayer, both of which require deterministic simulation. Every frame must produce identical results given identical inputs. This broke when Claude initially used JavaScript’s built-in Math.sin() and Math.cos().

Different browsers return slightly different floating-point results for trigonometric functions. Chrome and Safari diverge after enough iterations, causing replay desync. Claude replaced the standard library with custom implementations that guarantee bit-identical results across platforms.

The same determinism requirement applies to multiplayer. Two devices must simulate the same game state from the same input stream. The audio modem sends compressed command deltas (unit selections, movement orders, building placements), not full state snapshots. Each device reconstructs the game state independently. If simulation diverges, multiplayer breaks.

This is why the codebase avoids:

  • Random number generators without explicit seeds
  • Timestamp-based logic
  • Browser-specific APIs that return non-deterministic values

The random map generator uses seeded RNG so maps can be shared and replayed. Every match can be exported as JSON and fed back into the replay tool.

Agent-vs-Agent Battles: LLMs Playing Through Fog of War

Battle Drones includes an “Agent vs. Agent” mode where two LLMs control opposing armies. The prompt design is visible at the /agent-battles endpoint. Each turn, the LLM receives:

  • Visible units and buildings (respecting fog of war)
  • Resource counts and build queue status
  • Recent combat events
  • Available actions (build, move, attack, use abilities)

The LLM responds with a JSON array of commands. The game executes them, advances the simulation, and queries the next LLM. A separate debug tool shows the underlying prompt and response for each turn, exposing how models reason about strategy.

The prompt includes constraints:

  • You cannot see enemy units outside your vision range
  • You cannot issue commands to units you don’t control
  • Invalid commands are ignored

This forces the LLM to play within the same information constraints as a human. The replay system records the full prompt/response chain, so you can watch GPT-6 Luna vs. Claude Haiku 5.5 and see exactly what each model was thinking.

Audio Modem Multiplayer: No Server Required

The audio-based multiplayer is a stateless modem protocol. Two devices within microphone range exchange game state deltas using high-frequency chirps. The protocol:

  1. Establish a shared game seed and initial state
  2. Encode player commands as binary payloads
  3. Modulate payloads into audio signals (similar to dial-up modems)
  4. Transmit via speaker, receive via microphone
  5. Decode and apply commands to local simulation

This works because the simulation is deterministic. Both devices start from the same seed and apply the same command stream. No WebSockets, no relay server, no ongoing hosting cost.

Willison later added a WebSocket relay server (deployed on Fly.io for $2.19/month) after the high-pitched modem sounds caused headaches. The relay is stateless: it forwards messages between two connected clients and forgets everything when they disconnect. No game state is stored server-side.

The relay uses a simple room-based protocol:

// Client connects and joins a room
ws.send(JSON.stringify({ type: 'join', room: 'abc123' }));

// Server forwards messages to the other client in the same room
ws.send(JSON.stringify({ type: 'game_command', payload: {...} }));

The server never validates or interprets game commands. It’s a dumb pipe. This keeps the deployment simple and the cost predictable.

3D Models Generated Entirely in Code

Every 3D model in Battle Drones is constructed in TypeScript using Three.js. No external modeling tools, no asset files. Claude generated procedural geometry for buildings, vehicles, and terrain features.

Example: the Goliath mech is a walking base that can be airlifted by two Condor aircraft. It has eye lasers, arm cannons, shoulder-mounted mortars, and machine guns. As it takes damage, it loses weapons in a specific order (mortars first, then lasers, guns last). All of this is defined in code.

The model browser at /models shows every unit in the game. You can rotate them, inspect their geometry, and see the TypeScript that generated them.

This approach has trade-offs:

AspectCode-Generated ModelsAsset Files
Iteration speedFast (just prompt)Slow (export, import, test)
File sizeLarger (code + runtime)Smaller (optimized meshes)
Visual fidelityLimited by procedural generationHigh (artist-controlled)
ConsistencyGuaranteed (same code, same output)Variable (manual export)
Version controlClean diffsBinary blobs

For a vibe-coded game, code-generated models win. For a production game with a dedicated art team, asset files win.

Failure Modes and Observability Gaps

The project exposes several failure modes:

Non-deterministic simulation drift. Even after replacing Math.sin(), subtle differences in floating-point arithmetic can accumulate. The replay system includes a checksum validation step that detects drift and logs the divergence point.

Audio modem reliability. Background noise, microphone quality, and speaker volume all affect decode success rate. The protocol includes error correction, but high packet loss makes multiplayer unplayable. The WebSocket relay is a fallback.

Agent hallucination in battles. LLMs sometimes issue invalid commands (e.g., trying to build a structure outside their base range). The game engine silently ignores these, but the LLM doesn’t receive feedback. This can cause the agent to repeat the same invalid action.

Ultracode token burn. The “make it more fun” prompt consumed most of Willison’s weekly Claude token budget. Dynamic workflows are expensive. You need to know when the cost is justified.

No observability into subagent coordination. Ultracode workflows are opaque. You see the final result, but not the intermediate steps. There’s no trace of which subagents ran, what data they processed, or how they coordinated. This makes debugging hard.

Architecture Summary

The game is a single-page application with no backend:

  • Rendering: Three.js for 3D, deterministic simulation loop
  • AI opponents: Behavior trees with configurable personalities (Rush, Boom, Air Swarm, etc.)
  • Multiplayer: Audio modem or WebSocket relay (stateless, no persistence)
  • Replay: JSON export of command stream, deterministic playback
  • Agent battles: LLM tool calls via prompt, fog-of-war constraints
  • Music: Procedurally generated audio, no files
  • Deployment: Static HTML/JS/CSS, works offline as PWA

The entire game is TypeScript compiled to JavaScript. Willison has reviewed almost none of the code. He playtested extensively but didn’t write a single line.

Technical Verdict

Use agent-driven development when:

  • You’re exploring a domain you don’t know well (Willison had no game dev experience)
  • Iteration speed matters more than code quality
  • The problem has clear success criteria (fun, playable, visually appealing)
  • You can afford to burn tokens on dynamic workflows
  • The output is low-stakes (a game, not a medical device)

Avoid it when:

  • You need to understand every line of code (security, compliance, safety)
  • The problem requires deep domain expertise that models lack
  • Debugging is critical and you need full observability
  • Token costs are prohibitive
  • The output must meet strict performance or reliability SLAs

The “make it more fun” prompt worked because the agent could measure success (win rate distribution, game duration) and iterate autonomously. This pattern generalizes to any problem where you can define success metrics and let the agent optimize.

The lack of observability into Ultracode workflows is the biggest operational risk. You don’t know what the subagents did, only that they produced a result. For production systems, you need trace logs, metrics, and the ability to replay workflows step-by-step.