How I built Redline Wars
I built Redline Wars in my spare time with a laptop, an army of AI agents, and a stubborn memory of the strategy games I loved growing up. This is the route I took, including the turns that did not work.
I am Danillo Felixdaal, an Amsterdam-based developer who spent years building web and mobile products before turning much of my attention to AI architecture. I co-founded ProofOfWork, now becoming Twenty+One, where I work as a Build Lead: one human responsible for the decisions and the result while specialist agents do much of the making. Redline Wars is mine, built after hours under Jolyn Studios. It gave me a personal place to test the same question I face at work: how much can a small team do when the human keeps direction and the agents have real tools, shared context, and proof that their work holds up?
I have a habit of building an idea far enough to let it argue back. In our concept lab, ideas are sparred, prototyped and given a verdict, including the ones that fail. In other projects I have explored visual AI workflows and ways for agents to coordinate. This game was the less sensible and more personal experiment: what if that way of working could build the strategy game I still wanted to play?
The game I wanted to play again
I grew up with Command & Conquer, Red Alert, and Yuri's Revenge. I remember the music as clearly as the units. Later, after work in Amsterdam, I played Generals with friends in internet cafés. We sat at separate computers, ate beside the keyboards, shouted warnings across the room, and stayed for one more match. A win came from playing better. That feeling was the reason I decided to build this game.
I wanted a modern 3D battlefield that still felt like those evenings: a clear set of rules, a battle you can read, and no purchase between a player and the outcome. The browser mattered because I wanted someone to open a link and play. The desktop app came later, when I needed a practical way for players to host their own matches. Both open the same game.
I kept asking what Red Alert felt like rather than how to copy its pixels. For me it was readable armies, industrial machines, factions you could recognise at a glance, dramatic light, and music that told you a battle was coming. I wanted the 3D world to look like the battlefield I imagined while staring at those old maps. That gave the agents an art direction to work toward, not permission to reuse somebody else's models or recordings.
Redline Wars is currently free to play, made with respect for Westwood Studios and Electronic Arts. I used OpenRA's open-source engine and made new art and sound for Redline Wars. No EA or Westwood assets or recordings ship with it. The playable Alpha offers skirmish and multiplayer; the Fractured Order story gives those battles their setting, rather than promising a finished campaign.
One developer, many hands
My laptop was the development machine: an Apple M3 Pro MacBook on which I built, played, measured frames, and watched things fail. It was not a server that players depend on. I made the product decisions and judged the result; AI agents helped me carry out the work.
I built a private coordination layer called ContextRelay for our work at Twenty+One and connected Claude Code and the Codex CLI through it. They could hand off a bounded task, inspect the same repository, review one another's work, and return a disagreement to me with evidence. One agent could be busy implementing while the other checked the architecture. When they disagreed, I wanted the argument and its evidence, not a quiet compromise in two separate chats. The bridge made a small team possible, but it did not make decisions for me. I chose the next problem and what counted as finished.
| Job | Model or platform | Why it belonged here |
|---|---|---|
| Architecture and broad review | Claude Opus 4.8, later Opus 5, in Claude Code | Keep the engine, art, network, and release rules coherent. |
| Focused code and second opinion | Codex on GPT-5.6 in the Codex CLI | Implement bounded changes and challenge another agent's assumptions. |
| OpenRA fork and browser port | Fable 5.1, under my direction | Fable implemented most of the WebAssembly port while I guided the work and made the product decisions. |
| Models, animation, and repair | Astra 6 in Blender, then GLM 5.3 Flash for the wider asset pipeline | Build authored assets and make their export repeatable. |
| Multiplayer systems | GLM 5.3 Flash | Run long, broad implementation tasks against the service plan and network gates. |
| Independent reviews | Grok 4.6 | Challenge code and asset decisions before they became expensive to change. |
| Multiplayer and measured finish | Grok 4.7 alongside GLM 5.3 Flash | Help bring the network work across the line, then chase large-battle frame cost and live defects. |
| In-game voice and video | Freehop, our own open-source SDK | The calling layer had to exist without per-minute bills or streams through my relay. |
| Film, music, voice, model detail | Higgsfield AI, Suno 5.5, ElevenLabs, Meshy | Give the game characters, a score, faction and character voices, effects, and a consistent in-game likeness. |
The crucial skill was matching the harness and model to a bounded job, then asking another agent or a gate to challenge the result. A confident answer and a clean compile were only a start. The code, replay, pixels, and live page had to agree. With those checks, I could move between a rig, a sound bank, a shader, and a server without hiring a department for each one.
The first hard choice: keep one truth
I chose OpenRA because its mature, deterministic simulation already knew how to run a real RTS. My rule was simple: OpenRA decides the battle. The 3D client shows it. A renderer must never quietly move a unit, decide a hit, or invent an explosion. That choice shaped everything from the browser port to multiplayer.
Getting OpenRA into a browser was the hardest single piece of engineering. A desktop engine assumes sockets and APIs a browser cannot give it. I worked through a WebSocket transport, a browser-driven simulation pump, and a trimmed WebAssembly build. Then I built a determinism check: run the same seeded battle in desktop .NET and browser WebAssembly and compare their state and sync hashes. Until those results matched, a beautiful browser battle would have been an imitation, not the same game.
Fable 5.1 and I opened the openra-web fork from the upstream OpenRA tree. I guided the port and set the boundaries; Fable did most of the WebAssembly implementation. The fork let us keep the engine's battle rules while replacing desktop assumptions at the edges. The work gave OpenRA a browser platform and connection factory, an in-memory local connection, and cooperative steps for work that had expected its own thread. JavaScript supplied browser storage, input, audio, graphics and WebSocket interop; the mod rules and required files went into the browser filesystem, with persistent settings in IndexedDB. The result was a trimmed .NET 8 AOT build. OpenRA still advanced at 25 simulation ticks a second; the screen could draw at its own rate.
The TCP decision was more specific than “TCP does not work on the web.” A browser WebSocket itself runs over TCP, but a normal page cannot open OpenRA's arbitrary raw TCP socket. The first bridge translated browser WebSocket traffic to the original server's TCP connection. Later, the room node and public relay made that path usable for player-hosted matches without asking a player to expose the server port. The browser port changed the transport and host plumbing, not the combat rules.
The determinism oracle mattered more than a screenshot. It replayed one seeded battle in desktop .NET and browser WebAssembly, compared the state over time, and caught drift before it could become a multiplayer desync. Codex and GLM later helped build and maintain that proof. It gave me a precise answer to the question I kept asking the agents: is this still the same game?
The boundary became a compact message each simulation tick. OpenRA emits a packed snapshot; TypeScript reads it and feeds the image, sound, and interface. Player orders travel back to OpenRA for validation. This is why the browser, desktop app, and dedicated host can share the same engine rather than growing separate versions of the rules.
The snapshot is packed binary rather than JSON because it can describe hundreds of actors without repeatedly spelling out field names. Its sections carry identifiers, lengths, and offsets. The TypeScript decoder reads views of one buffer, and unknown event kinds can be skipped. I made a strict rule at this seam: the simulation advances on its own schedule; the renderer consumes the latest snapshot and may smooth motion between ticks, but it does not tick the engine or decide any gameplay. When I later moved the simulation into a Web Worker, that boundary let the interface remain responsive while pathfinding and world generation ran elsewhere.
I wrote the renderer directly in WebGPU because it receives a packed stream of actors, not a scene graph it owns. That gave me control over batching, shadows, lighting, and the frame budget. The site's single-model showroom uses three.js; a model on a pedestal is a different problem from an RTS battle.
In the renderer, that decision meant custom WGSL shaders, instanced draws, clustered lighting for many simultaneous flashes and searchlights, cascaded shadows, temporal smoothing, and tone mapping. Those are names for the work the graphics card performs to turn a battle state into a picture. The business choice underneath is simpler: I owned the visual pipeline so I could spend detail where the player sees it and prove how much a frame costs. The price was that every shader bug was ours to find.
Giving the picture character
The first playable battlefield made the architecture visible, but every early unit and building was generated from code: cylinders, boxes, and later procedural hulls meshed from shapes. Even the battlefield sound began as waveforms and filters from our own synthesizer. That let me prove movement, orders, terrain and combat before an art pipeline existed, yet from the normal camera height the armies had too little identity. I changed course and brought authored Blender models into the build. Astra 6 laid roughly 95 percent of the 3D foundation. GLM 5.3 Flash finished the export and integration pipeline so the models could be rebuilt rather than copied by hand. Grok 4.6 and Codex challenged asset decisions in review. The picture changed from a technology demonstration into a place where I wanted to command an army.
This was where “inspired by Red Alert” became a set of visual decisions: strong silhouettes before tiny details, faction colour visible at battle distance, machinery that looks heavy enough to move, weather and a day-night cycle that change mood without hiding orders, and damage you can read without opening a panel. The industrial material set added paint, metal, rust, and grime. I compared results from the game's normal camera height, because a beautiful close-up of a tank is worthless if ten tanks look identical when you are commanding them.
I had to break my own procedural-only rule. The original shapes were cheap to generate, but they could not tell a player what kind of tank or building they were looking at from a normal camera distance. I allowed authored Blender geometry and carefully sourced CC0 or CC-BY material instead. The files became a small factory of their own: more than 650 Blender primary sources, materials with wear and grime, model-detail levels for distance, and damage states that move a building from a scorch to a ruin. Every imported source needed a licence and a pinned origin. Every export had to agree with the game's scale and actor list.
The MCV, the mobile construction vehicle, was the showpiece and the hardest art handoff. It had to read first as a vehicle and then as a construction yard in one continuous motion. Astra built the deploying rig with 256 bones and 246 animation channels, aligning the moving parts rather than hiding the transformation in a model swap. There were five authored damage stages too, so the result still had to make sense when the building was hit. GLM helped turn Blender sources, rig data, animation, materials, and damage meshes into repeatable game packs. Hash and visual checks guarded the trip from source file to match. That is a lot of work for one object; it is also the moment the synthetic-looking battlefield began to feel built.
Generated work still needed a ruler. Meshy helped develop Riki's in-game model so the commando in the roster could resemble the woman in the film. One export came back with a deformed rig and a rifle that bent like rubber. My attempts to fix it by eye made it worse. Astra repaired the constraints, shoulder, hair and sleeve weights, UVs, and rifle grip, then checked 223 sampled frames and 147 action switches. The repair stayed only when the moving model held together. That episode taught me that an AI-generated asset is a draft until the animation and export survive measurement.
The films were another production loop. Higgsfield AI gave me a camera, characters, and a way to stage briefings without a film crew. I directed Allied command, Kirkov, Riki, and Jackson as four scenes from the same war, cut to the game's own score. Most generations did not survive comparison with the game's costumes, faces, and visual language. I rejected and regenerated them until the film characters belonged beside their in-game models. Riki needed the Meshy and Astra detour precisely because the film had made that mismatch obvious.
The faces were not invented at random. Jerome is based on Jermaine, my cousin and long-time development and business partner. Kirkov carries the likeness of my friend Sebastiaan. Riki is named for my wife Rikie, and Jackson carries the likeness of Anthony, a long-time friend with a Dutch military background. They agreed to be part of this world. That made the films more personal to me, and it made consistency between the screen and the playable roster matter even more.
Making room for a second player
Once the same battle could run in a browser, the next question was who would host it. I did not want every match to depend on a game server I owned. The desktop app became a thin Electron shell around the very same AppBundle the browser opens. It gives a player a landing screen and can start the bundled hosting node when they choose to host. The browser remains a permanent way to play, and its join path remains in the code behind a runtime switch. The public browser switch is now set to join: browser players can join hosted rooms; hosting remains in the desktop app.
Claude and Codex helped me argue through the architecture before I let the wide build begin. The choices had to work together: who owns a room, what the relay may know, whether a volunteer server is a different product, and whether a browser player would still have a path later. We settled on player hosting first. My infrastructure is a meeting point and, if needed, a last source of capacity. It is not the machine every match must run on. The relay may know that a room exists, but it must not interpret an order or decide who won. A dedicated server comes from the same OpenRA engine build as the clients; it runs headless as an order authority, while the clients simulate the battle. The desktop app must wrap the browser game, not grow another game inside it. Those decisions became written boundaries the agents could implement and test.
GLM 5.3 Flash was the workhorse for the broad multiplayer build: room nodes, relay, client flow, packaging, and the network gates. Grok 4.7 worked with that pass on review and the problems that appeared when the pieces met. Grok 4.6 had been an earlier independent reviewer. I could afford to let GLM work across a wide service plan because the decisions were pinned, Codex and Claude could inspect the seams through ContextRelay, and the gates could reject a room that only looked connected.
I built the hosting path around players' machines. A host's node dials out to the public spine relay, so a player does not have to open an inbound router port. The relay lists rooms and forwards match bytes without interpreting the battle. A dedicated OpenRA server from the same engine handles each room, forwards orders and watches for sync disagreement; the clients run the simulation and draw it. The community node package uses the same source, manifest, assembler, and start logic as the node in the desktop app. A player's app starts one room on demand; a volunteer can install the package and keep standing rooms available. There is one game build and one node build, with different packaging around them.
Here is one internet match in plain language. I choose Host in the desktop app. It starts the node already packaged with the app. The node starts one dedicated OpenRA room and opens an outbound encrypted connection to wss://spine.redlinewars.online/node. That is the node's entrance to the spine. The public room directory and player-facing socket live at play.redlinewars.online: a second player receives a room URL shaped like wss://play.redlinewars.online/g/<room-id>. The relay gives that socket a channel on the node's existing connection. Its control messages say which room and channel; its binary frames carry the players' game bytes. The node forwards them to the room's loopback-only server. Both clients run the same simulation and exchange orders; the dedicated server detects sync disagreement. Neither player needs the other's home address. On a local network, the desktop apps can discover the room and talk directly to the host's node, skipping the public relay. The public browser can play skirmish or join a hosted internet room through that same player-facing route.
The node and relay have distinct jobs. The node manages rooms and starts the dedicated process. A community installation can run several standing rooms from the same package. The relay supplies the room directory and a tunnel to nodes that dialed out. It may reject an incompatible build, cap traffic, or disconnect a broken socket; it does not parse the battle. The dedicated engine forwards orders and checks the tick at which two clients disagree. That separation makes a volunteer node useful without turning the central service into a second simulation.
Open player-hosted matches and trusted Ranked results are deliberately different problems. A player-hosted room is unranked: neither the host nor a browser can send the account service a victory claim and have it accepted. For Ranked, the separate path I built and verified locally uses an owner-controlled room to record the order stream. An isolated verifier replays those orders with the matching OpenRA simulation, reads the actual end state, and signs a result or a void reason before the account service can settle it once. It reuses the game rules instead of inventing a second referee. That was a separate qualification step before the public rollout.
This architecture was tested by awkward failures, not delivered by a perfect first plan. Two browsers could lockstep while keeping separate shroud state. Lobby buttons once waited on a tick nobody called. A long model-processing task blocked a join past its time window, so that work had to yield. An older network contract lingered after the client had moved to /v2, and I removed the legacy paths rather than maintain two meanings for a room. A browser-map experiment relied on filesystem enumeration WebAssembly could not provide; it broke boot, so I reverted it. The later Web Worker and asynchronous bridge kept the page responsive while the engine ran. The desktop shell stayed glue; it never gained its own copy of the game.
There are limits I chose deliberately. Ranked keeps its human-opponent requirements. An unranked room can start with one seated human and AI opponents when the map and readiness checks pass. The table has at most five player seats; observers do not take one, and optional empty seats do not block Start. Rooms that are still booting stay out of the public list. Public browser joining remains controlled by the off / join / full runtime switch, now set to join.
I tested that promise without opening the public switch. Two browsers first joined one room through a relay running on my M3 Pro, started a match, and kept the shared frame counter moving without a desync. Another test put two nodes behind the relay, routed players through their tunnels, and reached matching frames without an out-of-sync flag; a separate check confirmed that each player kept a different view of the unexplored map. After the public page could boot, I repeated the shape through the owner's gated browser route: one browser hosted, the directory listed the room, and the other joined. Both stayed connected while the sync check remained clean. These were local and gated network proofs, not a claim that two consumer machines on different networks had completed a public release match. The public browser setting remained off during those early tests; the later release opened joining.
Finding the sound of this war
The engine and network could now run a battle, but the first sounds still felt like a prototype. I remembered how the music in Red Alert changed the mood before the first tank moved. With Suno 5.5 I searched for a theme with that drive, judged the results against the game's own identity, and chose a 142-second piece that could loop from the boot screen into a match. I compressed the selected track to about 1.7 MB so it would not dominate the first download. It evokes the games I loved without shipping their recordings.
That first theme grew into a twelve-track score. Cold Start now loops through loading, setup and the lobby. Once the battle begins, ten tracks shuffle without repeating the one just played; Hard-Won Victory and Quiet Burial belong to the result screen. Reverse Order, Still Standing, Mechanical Groove, Mechanized Expansion and Dusk Over The Field give the battle different moods. Solo Mission is Riki's theme; I chose Beyond The Enemy Lines for the trailer and We Hold The Line for the capture scene. All twelve recordings are in the shared game bundle and on the soundtrack page.
I kept playback under the player's control. Sound and volume settings persist, the browser waits for a gesture when autoplay is blocked, and music stays off while the microphone is live so the score does not spill into the call. On 7 October I checked the actual served game: every soundtrack file was present and matched the website's recording, including the complete Reverse Order track.
Sound made the same trip as the models. At first the guns, engines and impacts came from the game's own procedural audio engine. The earliest announcer tests included locally rendered text-to-speech, followed by recorded faction banks and character voices. As the library grew, I moved every voice bank to ElevenLabs: faction announcers, characters such as Riki, Spy and Jackson, and death shouts. One provider now voices the whole battlefield, and each bank keeps its own country's timbre. I wrote lines by faction and situation, rendered them before the game was built, and listened for pronunciation, character, and timing. The Allied bank speaks English; other banks speak French, German, Russian, and Ukrainian. I transcribed generated clips back to text and compared them with the intended words, because a file named “construction complete” can still say the wrong thing. One later fix made building completions say exactly that while units still say “unit ready.”
ElevenLabs sound generation then gave the effects banks distinct weapons, impacts, movement, and battlefield cues. An earlier inventory counted hundreds of banked voice clips and 190 rendered effects across five faction banks; the working banks later grew beyond that checkpoint. The recorded clips take priority, and the procedural engine still covers a missing effect instead of leaving a battle mute. A shout gets a voice budget so an army cannot queue a wall of speech, and sounds hidden by fog stay inaudible as well as invisible. I baked the chosen audio into the shared game bundle at build time. A match never calls a voice or music provider over the network. The creative platforms helped make the sound; the finished game owns its playback.
The assistant beside the commander
In those internet cafés the loudest player was sometimes the one without a keyboard. Someone watched the screen over a shoulder, called out movement, and argued about the next target. That memory shaped the last big feature I built: JOA, the in-game assistant. JOA is the voice beside you in every battle. It greets you, reports what the factories are producing, warns when the base is under attack, and calls every support weapon as it fires. The companion is one part of JOA. It pairs a phone or tablet to a live match and gives the person on the couch their own screen: the tactical map, command groups, support weapons, and a statistics sheet. The game itself answers a finger too: a tap selects, two fingers pan and pinch, and the lobby fits a tablet.
The phone must never become a second god-view. I set the boundaries before any code was written. The commander stays in control of every order. The companion sees only what the commander's own vision of the field reveals, so a paired phone leaks nothing through the fog. Permission is granted in tiers: spectator, technician, or commander. Pairing needs a one-time code and the commander's approval, the host of a match can refuse companions entirely, and the link can be ended at any moment.
The architecture follows the one rule that shaped everything else in this project: OpenRA decides the battle, and every screen shows it. The game already emits its packed snapshot each tick, so the companion renders from a projection derived from that snapshot, filtered to what the local player may know. Orders travel the other way as intents over a WebSocket, and the main game checks every one: the link session, the sequence window, the permission tier, the map bounds, the group revision, aircraft availability, and whether an attack target is actually visible. Only an intent that passes becomes the same order the commander's own interface would issue. If the check fails, nothing happens, and the phone is told why.
The sound followed the banks, not a new voice. The new lines, production reports, the base-under-attack warning, and the support calls, were rendered with ElevenLabs into the same per-country voices the shout and death rows already use, each bank speaking its own language. Nothing is synthesised during a match; a line either exists as a banked clip or stays silent. The companion has its own acknowledgment and alert sounds, and a switch to turn them off.
Claude and Codex built it under my direction, trading implementation and review through ContextRelay while I made the calls on scope and safety. The hard discussions were not about drawing a map on a phone. They were about which ten facts a second screen may know, what happens when a group is disbanded mid-order, and how a rejected order should be explained on a small display. The statistics sheet shows credits, ore, power, harvesters, the enemy count and the roster because the game tracks those. Where the engine does not track a number, the sheet says so instead of inventing one.
The companion is now enabled in the public game's configuration. It opens from a pairing link or QR code on a phone or tablet, with approval and permissions still controlled by the commander. That is separate from Freehop: pairing a second screen does not join a voice call, and joining a call does not grant game controls. Ranked rooms keep companions disabled. The couch gets its observer while the commander's screen stays in command; checking the experience across physical phones and networks is still part of Alpha testing.
The call across the room
The cafés had one more sound I wanted back. Friends shouted warnings across the room, argued about the next target, laughed at a failed rush. Text chat in a lobby is useful, but it is not the same room. I wanted voices and faces inside the match itself: join a call from the room, watch the roster light up when someone speaks, mute the one friend who narrates every harvester. Nothing between the players and the game; no external meeting app, no invite link.
The browser already knows how to place a call; WebRTC is a mature stack. The problem is what happens when the direct connection fails. Two strict home routers or a campus network that blocks UDP may need a relay, and the relayed traffic becomes a running cost. I wanted the room service to introduce players without also paying to carry every conversation. The game relay lists matches and forwards game traffic; the media takes its own path.
So we built the calling layer as its own product: Freehop, our open-source voice and video SDK for browsers and desktop apps, Apache-2.0, with its own documentation and a live demo that places a real call between two browser tabs. It splits the work across two planes. A small gate introduces the participants and carries sealed envelopes between them, each one encrypted with HKDF-derived AES-256-GCM keys and bound to the room, the sender and the recipient, so a gate cannot read, forge or re-route what it carries. Gates are bounded to kilobytes so media cannot fit through them, and being kicked from a room rotates its secrets. The media plane stays with the session itself.
When the networks block the direct path, the call climbs a ladder of machines that already belong to it: a desktop participant's own gateway, which asks its router for a port through PCP, NAT-PMP or UPnP; the match host's node; another member's gateway; and at the end of the ladder, one participant bridging the others. If no route exists inside the session, Freehop reports unreachable instead of quietly routing the call through a paid relay. Once peers are linked, the call even keeps running when every gate is down, because the signalling moves onto their own data channels.
Freehop was tested the way this whole project tests things. I built a home lab of Linux kernel NAT profiles, symmetric NATs, UDP-blocking firewalls, IPv6-only paths and UPnP routers, and drove real Chromium, Firefox and WebKit engines through them with fake cameras and microphones: 40 of 40 network scenarios placed their calls, including the pairs the early research had proven impossible to connect directly. The gate traffic for a whole scenario stayed between 30 and 210 KB. coturn's own test client exchanged 800 messages over UDP and another 800 over TCP against Freehop's TURN gateway, none lost. Those are simulated-network results from my lab, not field evidence; the SDK is alpha software and says so on its own page.
Redline Wars consumes Freehop through its public SDK. The deployed version is 0.1.0-alpha.3. The game's relay issues admission tickets, a separate Freehop gate handles signalling and STUN, and the session's participants and host supply the media routes. Alpha v0.2.0 includes the call interface, speaking indicators, per-player mute and a room policy that tells a joiner whether audio, video or both are allowed.
On 7 October I enabled the public Freehop service for the existing game and downloads. I then found a smaller switch still closed: Community #1 had no explicit call setting, which safely defaulted to off. I changed that room to allow voice and video after checking it was empty and draining it cleanly. Its public listing now advertises both, and the template for new community rooms carries the same choice. Each player still chooses whether to join and whether to use a microphone or camera; allowing calls never opens those devices automatically.
The earlier browser-to-desktop direct-route test and four physical-Mac frame-time profiles remain useful evidence. They do not complete physical acceptance across Windows hardware, phones and different home networks. Freehop is available as an opt-in Alpha feature while that work continues.
Making it open source was not charity; it was the same rule as the engine seam. The problem is not game-specific, and a calling layer only a game could use would be a worse calling layer. Forcing the SDK to stand on its own made both sides honest: Freehop survives without the game's assumptions, and the game integrates without private hooks. It grew from the protocol research in this repository into its own project with its own evidence file and its own limits page, built under the codename Peerlane; the envelope salts still answer to that name, and the calls they seal answer to nothing but the session they belong to.
The gate that says no
Agents made it possible to change several parts of the project quickly. They also made it easy for one part to assume that another had changed with it. The packed snapshot is the clearest example. If C# adds a field and TypeScript reads the old width, a battle can look fine for a moment and then desync. I gave that boundary a release gate that calculates the layout independently, compares producer and decoder, and deliberately feeds it bad lengths and bytes. A mismatch stops the release.
The Snapshot ABI gate:
This simplified exchange shows how I used ContextRelay and the gate when a field changed. The transcript illustrates the review pattern; the measured bytes come from the check, not from an agent's confidence:
ContextRelay · private agent-to-agent bridge
A compact map of the other release gates:
The larger gate inventory gave me a less flattering truth. A full historical pass ran 201 commands and found 54 failures. Some were product defects; others were fixtures and source pins that no longer described the game. I spent days reconciling both. We fixed boot fixtures, refreshed asset hashes, caught missing engine API members, rebuilt audio coverage, and made deployment checks fail when their evidence was stale. The result was not a dramatic feature. It was permission to believe the next green build.
That work also changed how I used agents. I could ask GLM to build a wide system, have Claude and Codex argue about its boundary, ask Grok to review the risky seam, and still let an independent replay or pixel gate refuse all of us. One agent's output became another agent's input, but the repository and the checks were the common record. A gate nobody runs is only a reassuring file.
A battle that fits the machine
The new models made the game feel right and made each frame more expensive. There were two different bills to pay: how much model data lived in GPU memory, and how much work each displayed frame required. A smaller download, a smaller GPU footprint, and a faster frame are three different measurements. I kept them separate.
The memory numbers come from an offline audit of the assets the renderer uploads, including their three model-detail levels. It is a model of GPU allocations, not a reading from a graphics card. The earlier all-float vertex layout modeled 743.9 MiB of geometry. Grok 4.7 did the quantization work as part of our push to keep the battle at 60 fps: packing normals, tangents, and texture coordinates took modeled geometry to 437.5 MiB in the first measured pass, a 41.2 percent reduction. The current 28-byte static and 36-byte animated layout models 393.7 MiB for the same asset inventory. Textures add another 94.0 MiB in that audit. The stored asset pack is about 151.9 MiB; it is compressed on disk and expands when used. None of these memory numbers alone proves an FPS gain.
LOD means level of detail. A nearby tank needs its full shape; a distant one occupies fewer pixels and can use fewer triangles. The audit counts three levels, roughly full, half, and quarter detail. I kept the silhouettes and authored motion where players can see them, and used simpler meshes farther away. The renderer adds a sticky band around switching distances so a unit does not flicker between levels as the camera moves. That costs a little bookkeeping and, in an earlier pass, some extra draw calls; it buys a steadier picture.
The optimization had more than one round, with Claude Opus 5 and Grok 4.7 helping to interrogate the expensive paths. Early on, shadow passes were drawing tens of millions of triangles and the meadow layer alone spent 13.8 milliseconds of CPU on updates. Splitting shadow detail from camera detail cut roughly a quarter of the shadow triangles. Sticky LOD selection stopped visible popping even though it cost 82 more draw calls in that test. Moving meadow updates onto a cadence brought that CPU cost to 2.75 milliseconds. Those are measurements from that earlier tuning pass; I do not mix its old frame rate with the later Europe battle as if they were one before-and-after test.
I measured frame time separately on my M3 Pro laptop in Chrome, using the Europe skirmish map with roughly 1,135 actors. Dynamic and High, the two settings I timed after the frame cuts, showed a typical frame of about 8.3 milliseconds at full internal resolution. My 60-frame target was 16.7 milliseconds. The laptop display refreshes near 120 Hz, so a typical 8.3 millisecond interval can also mean the picture is waiting for the screen. This is a result for that machine and battle, not a before-and-after speedup claim for the quantization pass or a promise for every graphics mode.
The actual frame cuts were selective. Shadow maps became smaller and their reach shorter; the sun and contact-shadow shaders sampled less; water skipped expensive land lighting; animated soldiers stopped appearing in every shadow cascade. Scenery scanning and the simulation moved off the page's main thread so they stopped delaying mouse input and the picture. I kept full resolution, contact shading, four shadow cascades, and the actors in the measured Dynamic and High battle. I spent less work on shadows and distant detail that were hard to see during play.
| Choice | What became cheaper | What I gave up or kept |
|---|---|---|
| Pack vertex data and use smaller index types where they fit | Less geometry uploaded and held in the GPU | More careful encoding and shader decoding; the cutout and shadow pixel gates had to stay equal. |
| Use three LODs with a sticky switching band | Fewer distant triangles without flickering | Some extra draw calls and selection bookkeeping; nearby silhouettes stay detailed. |
| Shorten and shrink shadow work | Fewer shadow pixels, samples, and repeated soldier draws | Less distant shadow precision; contact shading and four cascades remained in the measured modes. |
| Move simulation and scenery scans into workers | Fewer long stalls on the page's main thread | A more careful asynchronous bridge; the engine still owns every game rule. |
| Let Dynamic shed extras when needed | More room inside a slow frame | Nearby grass and wind go first, before core actors or rules change. |
The quality governor is the final trade: on a strong machine Dynamic may add nearby grass and wind after several quiet measurement windows. If frames get expensive, it drops those extras first, then can lower internal resolution or other effects. High starts from the full picture and may shed cost; Ultra+ keeps its richer look and asks more of the machine. I also tried to correct the hard square where natural ground types blended. Roads stayed sharp because a wandering road edge would look broken; sand could look like sand. That first correction was not the last word on those squares; the next chapter tells how they actually went away.
My laptop was a measuring instrument I could inspect, not a universal benchmark. GPU timestamps on that machine returned zeros at first, so I used time between displayed frames and kept the test map, actor count, browser, and graphics mode attached to the result. Later, once the renderer asked the adapter for its timestamp feature, individual passes could be timed; that is how the restored soldier depth pass in the next chapter got its 1.7 milliseconds. I left the other graphics modes without a new performance claim until I could clock them separately.
What a faster frame cost
The day after those frame cuts I sat down to play and saw what the frame clock had not measured. The shadow under a parked truck vibrated. Walking soldiers had holes in their legs. Light on buildings looked flat, distant trees shimmered, damaged trees carried health bars, and wherever a mountain met open ground the terrain broke into square plates. The cuts had been written for my 120 Hz screen: the code comments behind them said "missed 120 Hz" almost everywhere. My target was 60 frames per second, which leaves twice the time per frame, and the cuts had been applied to every graphics mode, Ultra included.
So the second pass measured the picture, not only the clock. Codex ran the experiments and Claude coordinated them: a fixed camera, a fixed sun, the simulation paused, 24 frames, and the variation of every pixel from frame to frame. Each suspect was switched off, on, and off again, so a change that came back when it was restored was a cause and not a coincidence.
The biggest amplifier had been there all along. A sharpening pass at strength 0.28 enlarged every leftover wobble of the anti-aliasing. Lowering it to 0.10 and letting still pixels keep a longer history cut the shimmer on a building from 1.82 to 0.73 on that measure. Screen-space contact shadows were the vibration under the truck: they read a depth image that shifts slightly every frame. Turning them off halved the worst shimmer beside a vehicle and made a building edge ten times calmer, while the full picture changed by 0.13 of 255 at normal zoom. The shadow maps, their filtering, and the baked ambient occlusion stayed, so units still stand on the ground.
The soldiers had two faults stacked on top of each other. The speed pass had taken them out of the depth pass, so anti-aliasing reused the old background through moving legs; putting them back costs 1.7 milliseconds with 217 actors on screen. On snowy maps a second effect remained: the weather layer painted snow on every surface facing the sky, including the tops of their trousers, which on white ground read as holes. Animated units no longer hold snow; a test with the same rifleman in clear and snowy weather went from 35.8 percent pale uniform pixels to 0.2 percent.
The square plates came from a limit in the terrain itself. Every map cell could show at most three materials, so where rock, snow, a road, and a snow variant met, one of them stopped dead at a cell edge. Now every pixel mixes the four cells around it with a gently wandering boundary; roads and concrete keep straight edges without the staircase. The first version of that change pushed a heavy battle from 17.6 to 25 milliseconds at the slowest five percent, so it was rewritten to sample only the materials that actually carry weight.
The humbling result: the build from the morning before the cuts measured 1.90 on the same shimmer scale, no calmer than the cut build. The flicker had always been there; the cuts made it visible. The new build is calmer than both, and Ultra and Ultra+ have their pre-cut shadows back.
| Lesson | What I do now |
|---|---|
| A speed number hides what it costs | Every frame-time claim sits beside a picture-stability measurement. |
| Global cuts punish the modes that should shine | Quality is budgeted per graphics mode; Ultra never pays for Dynamic's headroom. |
| One setting can outweigh a whole quality table | Toggle one suspect at a time, off-on-off, before touching the next. |
| Per-cell decisions show up as squares | The terrain decides per pixel; a cell only names its material. |
| An effect meant for one object lands on all of them | Weather, shading, and health bars are checked per kind of object, with a gate. |
From my laptop to a player's screen
For a while the site and services built automatically while the game bundle still came from my development Mac. That was a fragile way to ship. I moved the release through one shared build lane: build the browser game once, verify it, then let the desktop installers wrap that exact AppBundle. Desktop packaging refuses a missing or stale bundle instead of silently building another game.
The Mac visual gates exist because a cloud Linux software GPU failed when the tests read pixels back. Fresh release runners exposed other ordinary problems: a .NET pin, a missing mod library, Blender setup, and shell scripts that assumed a newer macOS than the runner had. Each fix made the factory less dependent on what happened to be installed on my laptop. A separate deploy can update the site, account service, relay, node, game, or downloads, but the browser game still comes from the single verified AppBundle.
A push can run tests; publishing is now an explicit choice from the consolidated release branch. The workflow checks that all approved work is included and that the selected commit is still the current release source. A full release waits for the shared AppBundle, visual gates, desktop packages and Mac node packaging before changing production services. Frame-time qualification happens on the physical Mac, and the bundle that ships must match the measured content digest. The community node zip comes from the same node assembly used by the app. I can still update the website or an individual service through its own guarded route.
The last lesson arrived after a build passed. The public page could still fail to open because the server did not allow one script the game imported. Earlier, a folder mirror had deleted a worker script that really was part of the bundle. In both cases the file existed in the engineering story and disappeared in the player's story. I fixed the serving rule in the service that owned it, then checked the actual page. “Built” was not the same as “playable.”
The safeguards now follow the files further. A browser release switches to a versioned AppBundle and preserves the operator's Freehop setting. Download checks compare package hashes, build identities and source references. Website checks cover the version, privacy and credits pages, all twelve soundtrack recordings, and the model catalogue's delivery format. The account and node installers retain recovery checkpoints. A full service rollout still needs an idle or drained room: an ordinary node restart can end a match, so a green build is not permission to interrupt players.
There is also a source boundary I have to keep explicit. The public source edition includes the OpenRA engine changes for that edition, the WebAssembly host, WebGPU renderer, procedural stand-ins and a minimal HUD. It excludes the production HUD, JOA, Freehop integration and production creative assets. Freehop has its own Apache-2.0 repository. A reduced edition does not supply the corresponding source of the complete distributed game. The production release gate currently blocks a new game publication until that correspondence is resolved and verified; a website update or a working live call does not clear it.
Other defects waited until the game met its real surroundings. Civilian houses spilled outside the lots they replaced, so I fitted only those meshes rather than shrinking every actor. Damaged infantry needed smoke that followed the figure. A phone ran out of room loading the full authored roster and kept restarting near the same point; narrow screens now take a lighter silhouette path. The shared rule was the same in each case: inspect what a player actually receives, fix the owner of that behavior, and keep browser play working throughout the rollout. Browser joining is now enabled; hosting remains a desktop feature.
The identity finally had to feel as deliberate as the battlefield. Isa Ozler designed the Redline Wars game logo. I carried the cut shape behind his wordmark into the site's buttons; their lower front piece now slides back and returns like a loading mechanism when you hover. It was a small finishing choice, but it made the site and the game feel like one product. Thank you, Isa.
The numbers behind the story
These numbers come from different kinds of evidence. An asset count is an inventory, GPU memory below is an offline allocation model, and frame time is a Chrome measurement on one M3 Pro laptop. I keep their conditions beside them so one impressive number does not borrow credibility from another. Some inventory totals describe the audited build at that point in the road; they are not a claim that every local copy or later export has the same file count.
| What I counted or measured | Result and scope |
|---|---|
| First playable stretch | About six weeks of work, including two fully dedicated weeks and evenings around the day job; shipping continued afterward. |
| Simulation and picture | OpenRA at 25 ticks per second in WebAssembly; the WebGPU renderer can use more than 200 dynamic lights, with a 24-minute day-night cycle. The industrial material set has 29 layers. |
| Art sources | 652 Blender primary sources in the structural audit; the earlier workstation inventory counted 3,327 .blend files when library copies were included. The audit found 1,862,511 triangles in those source files before runtime selection. |
| Resolved game catalog | 310 audited actors, of which 283 were renderable and 95 buildable. These categories overlap. |
| Native models and movement | 277 delivered native models, 1,503 moving joints, and 1,368,266 triangles at full detail in the model delivery. The wider LOD0 tally was 1,481,162 triangles. |
| Damage | 59 current damage ladders in the model delivery; the MCV deploy alone uses 256 bones and 246 animation channels. |
| Character films | Four Higgsfield scenes: Allied command, Kirkov, Riki, and Jackson. Riki's repaired rig was checked on 223 frames and 147 action switches. |
| Voices | 35 planned lines across each of eight faction banks, plus Riki and Spy personas: 328 banked clips in the audio inventory, including 76 death lines. |
| Effects and music | An earlier checkpoint counted 190 effects across five faction banks, or 38 per bank; the working banks later expanded. The current score has twelve recordings: ten battle tracks, including the menu's Cold Start, plus separate victory and defeat songs. All twelve live game files matched the soundtrack page on 7 October. |
| In-game calling | Freehop 0.1.0-alpha.3, Apache-2.0, enabled publicly on 7 October. Community #1 allows opt-in voice and video. The earlier 40-of-40 kernel-NAT matrix and coturn interop are simulated-network lab evidence; physical acceptance across supported devices and networks remains pending. |
| Geometry memory model | Earlier 743.9 MiB; first packed layout 437.5 MiB (41.2% less); current local layout 393.7 MiB. With 94.0 MiB of textures, the respective modeled totals are 837.9, 531.5, and 487.7 MiB. Three LOD levels and smaller 16-bit indices where possible are counted. These are modeled GPU allocations, not live VRAM readings. |
| Large-battle frame time | On the M3 Pro in Chrome, Europe with about 1,135 actors: Dynamic and High each showed about 8.3 ms for a typical frame at full internal scale. The 60 fps budget is 16.7 ms. Other modes were not retimed in that test. |
| Picture pass, 23 September | On a 218–221 actor skirmish with the Mac also running other heavy work: Dynamic p50 16.4 ms and p95 17.9 ms, 60 fps with 1024-pixel shadow maps and contact shading off; Ultra and Ultra+ back on 2048-pixel maps with 9-sample filtering. |
| Picture stability | Frame-to-frame variation on a building at a fixed camera: 1.90 before the cuts, 1.82 after them, 0.73 after the picture pass (High). Worst-case Ultra shimmer beside a vehicle: 4.67 with contact shadows, 1.46 without. |
| Browser start | A cold local start reaches the lobby in 21–25 s and a playable match in about 36 s; building 374 model variants takes 14.5 s of that. |
| Earlier frame pass | Classic held about 48–52 fps before the later cuts. The meadow update cost changed from 13.8 ms to 2.75 ms in that earlier pass. These are not directly comparable with the later Europe run. |
| Release checks | A historical full inventory ran 201 commands and initially found 54 failures. A later audit listed 199 live commands and five owner-disabled gates; those are separate checkpoints, not one subtraction. One acceptance pass ran 48 executable gates and 67 map starts. |
| First download | An earlier desktop-path first visit was about 152 MB, with near-zero repeat transfer when the content-addressed cache was warm. The asset pack audit separately counted 151.9 MiB stored. The lighter phone path and its cache policy differ, so the repeat number is not a universal mobile claim. |
Where the road stands
Redline Wars is the game I set out to make from those Amsterdam evenings: one rules engine, an authored 3D battlefield, and a browser skirmish you can open directly, currently free to play. The desktop app gives players the route to host or join their own matches. Accounts and the live leaderboard are open; the outcome screen explains whether a match counted. The public browser setting is join, so browser players can join hosted rooms. JOA's companion is enabled, the twelve-track score is installed, and Freehop is available in rooms that allow it. The current public game version is shown at the top of this story. The public source edition has a reduced feature set and does not reproduce this full distributed game.
That September release was about the room around the battle: opening as an observer without occupying a seat, leaving the previous lobby before hosting another, removing human players or AI, keeping the chosen capacity through map changes, and letting a promoted admin operate the room. Readiness comes back from the engine. An idle admin yields after ten minutes when another human is waiting, with exceptions for a lone admin or a match already running. Lobby text chat is temporary: at most 100 visible messages, no history for a new arrival, no saved chat or replay chat, and a clear on departure, disconnect or Start. Returning from multiplayer to skirmish was tested in the packaged Mac app.
The release also taught me to test the factory itself. An earlier rejected deployment briefly left nodes on a different simulation build from the relay. I rolled it back, checked the restored services, and fixed the dependency gates before publishing the qualified version. Clean-checkout builds, Windows archive handling and signed Mac package manifests all had to pass. The existing four frame-time profiles passed with unchanged thresholds; that is evidence for those measured profiles, not a promise about every machine.
On 1 October a player found a hole in those checks: the public short URL showed an empty map and forces panel. The engine initialized, but the server still permitted the old inline startup-script hash, so the browser blocked the script that selected game mode. A server rewrite also left relative configuration requests pointing outside the game directory. I corrected the hash, redirected the short URL to the actual game document with its query preserved, and added a release check derived from the script itself. A build passing locally had not proved that every public entry URL worked.
The same player then reported units and a Construction Yard on different sides of River Crossing. Repeating a player-versus-AI match showed why: four bridge-test patrol actors were still authored into the public Conquest map and always belonged to player slot one, while normal starting units followed the selected spawn. The correction removes those actors from the playable map. The bridge movement, repair, combat and fog checks keep their patrols in a private in-memory test map built from that same source. A new regression gate starts both player slots at both spawn positions with each starting-unit preset, checks both armies and deploys the MCV. The browser check also clicks through setup and witnesses the base on screen. A second report caught a detached upper fence rail: its posts ended below it. Extending the three saved Blender posts connects both rails without adding triangles; the other scenery assets remain byte-identical. Scenery export and freshness now run in the shared build lane too.
The next screenshot exposed a crowded multiplayer lobby. Its clearer layout keeps participants and Ready/Start separate from three collapsible settings groups, and gives temporary chat a visible panel. The real lobby controls passed, including three window widths, keyboard use, read-only guest controls and admin handover. An initial strict performance comparison held the candidate: it reported a small frame-time shift on the physical Mac. Follow-up showed that the same runtime bytes were already live, while the old 0.2 ms CPU injection was an unsuitable positive control for delivered frames. The release check now uses a calibrated skipped-frame control and a 2% margin. After the candidate passed the physical Mac profiles, the 3 October full deployment shipped the clearer lobby, the River Crossing spawn repair and the fence fix in the browser game and desktop downloads.
On 4 October I gave the game one public product version, Alpha v0.1.0, across the landing page, this story, the credits, downloads and the desktop app. The five refreshed desktop installers use the same measured game runtime as the 3 October release; the new source tag identifies the packaging and version-label changes. I checked every live browser runtime file against that build, and the released Mac app opened a local hosted multiplayer match with WebGPU rendering. Browser players can join hosted rooms; hosting stays in the desktop app.
On 6 October I qualified the Alpha v0.2.0 candidate; the full deployment completed on 7 October. Two server-side defects had to be fixed first: the public room list had dropped each room's call policy, so joiners never saw the call bar, and an outdated ticket service on the relay rejected the peer-id that mutes and roster cameras depend on. The release carried those repairs and the twelve-track score. The version rose across site, desktop and relay, the ranked gate was re-pinned through a real end-to-end match, and the four frame-time profiles held on the physical Mac within the same margin. Calls were verified between a browser and desktop on a direct route. A Windows-only test expectation surfaced in packaging and taught the lesson again: derive path expectations, never type them.
Later on 7 October I opened the public Freehop switch and corrected Community #1's room policy. The browser, both server nodes and relay agreed on the same simulation build; the installed Freehop package matched the version pinned in the project. Those checks establish what was deployed and enabled. They do not claim that the next full release is already published or that the remaining physical acceptance work has passed.
The protocol research did not stay in the worktree. The controlled direct-connection cases and the deliberately blocked routes that failed in the prototype became the 40-of-40 home-lab matrix the Freehop SDK now publishes beside its code; what once had to be kept out of the game became the call inside it. What is still honest to say: those are simulated-network results from my own lab, the SDK is alpha, and media performance beside gameplay on real home networks is the next phase, not a measured claim.
The road taught me that AI multiplies what a developer can attempt, but only when the work has a direction. Fable and I could tackle a browser port; Astra could give hundreds of models a visual language; GLM could carry a broad multiplayer build; Grok could interrogate a frame budget; Claude and Codex could challenge each other through ContextRelay. The harnesses, models, and platforms made that range possible. I still had to choose the rules, reject the wrong film, demand a working rig, and decide when a number was honest enough to publish.
I started with a memory of friends at computers in Amsterdam. Now a player can open a browser link and command the battlefield I used to imagine. The next seat can be a hosted match through the desktop app or a browser joining that room. I am still the one who has to listen when a replay, a byte count, a frame clock, or a player says I got something wrong. That is how this game became more than a generated demo.
The likenesses behind Jerome, Kirkov, Riki, and Jackson belong to family and friends who agreed to be part of the project. The full list of people and tools is on the credits page.
Redline Wars is a fan reinterpretation and is not affiliated with or endorsed by Electronic Arts, Westwood Studios or OpenRA. OpenRA and its contributors retain their rights under GPLv3-or-later. Command & Conquer, Red Alert, and related marks belong to their owners. The game's notices describe the software under GPLv3-or-later, including the WebGPU client, separately from artwork, recordings and trademarks. Copyright in Jolyn Studios' contributions does not replace those software licence terms. The credits and source notice explain the public edition's scope and the separately sourced creative material.