We chose Rust for a narrow reason. Jellygon and Jelly48 share a 2D soft-body solver, and we wanted that code to run in a browser, inside a native iPhone app, and in a desktop development build. Maintaining a separate engine for each target would have defeated the point of being a small team.
That part worked about as expected. The surprise arrived later, as more of the day-to-day work involved AI coding agents. Rust did not make the agent infallible. It made a large class of its mistakes loud, specific, and cheap to hand back before a person had to review them.
The same game code reached three runtimes
The shared stack uses wgpu for graphics, winit for windows and events, and our own Rust engine crate. Jellygon currently runs in three places:
- Browser: Rust compiles to WebAssembly, with WebGPU as the primary renderer and WebGL2 available for broader browser support.
- iPhone: Xcode links the Rust code as a static library and renders through Metal. The game is available on the App Store.
- macOS: the development build runs through the same windowing and graphics layers using Metal.
When we first pointed Jellygon at the iOS simulator, the engine and game code ran without changes. The problems were build plumbing: selecting the simulator SDK, producing the right static library, and removing an environment variable that sent Apple's linker in the wrong direction. Irritating, certainly. A second implementation of the game, no.
The boundary is deliberately plain. Rust owns the game state, physics, render loop, and most interface behavior. A Swift bridge handles the services that belong to iOS: advertising consent and presentation, StoreKit, haptics, iCloud, device motion, audio-session behavior, and Firebase events. At publication the bridge is roughly 1,100 lines. Game code calls a small platform function and does not need to know whether the result is a Taptic Engine response, a browser fallback, or nothing at all.
There are real costs. Web graphics support remains the least uniform target. WebAssembly and Xcode need build glue, and a framework that owns those details would hide more of them. We accepted that work in exchange for keeping the simulation and game behavior in one place.
The compiler became the agent's first reviewer
The question that determines whether an AI coding session is useful is not whether the agent can produce code. It is how much human attention the code consumes before it can be trusted.
In a permissive environment, review often starts with mechanical suspicion: did this value become null, did the refactor miss a call site, is that API from the installed version or from last year's documentation? Rust moves many of those questions into the build.
When an agent uses a value after moving it, forgets to handle an Option, or breaks a type at one of six callers, the compiler returns a bounded list of failures. The agent can work through that list before asking for review. The human still has to judge the design, but less of that judgment is spent acting as a tired type checker.
This is a change in the division of labor, not proof of correctness. Compiling code can still contain the wrong algorithm, a poor interface, or a beautifully typed misunderstanding of the requirement. Those are exactly the problems we would rather save human attention for.
Explicit code is easier to navigate by search
Agents learn an unfamiliar repository by searching it. Rust rewards that habit. A module's public functions are marked. Missing values appear as Option at the call site. Platform-specific branches are visible in conditional compilation. Cargo features and target sections describe why one build differs from another.
That does not make every codebase tidy. It does mean fewer important behaviors depend on runtime registration or an unmarked value appearing somewhere else. When the agent asks “where can this state change?”, text search usually returns a useful answer.
The same applies to project shape. Build with cargo build, test with cargo test, and put runnable tools in examples/. Our deterministic replays, automated playtesters, and graphics checks all fit the same command structure. The agent spends less time guessing which private build ritual the repository expects.
Determinism closes the useful part of the loop
Compilation catches structural mistakes. It does not tell us whether a physics change altered a shipped game. That is where deterministic simulation matters more than the language.
Given the same starting state and inputs, our test builds can replay the same game. We used that to compare the last deployed Jelly48 engine with current work. Identical inputs produced identical output, which let us show that an observed velocity spike was already present rather than introduced by the new changes.
The same property supports longer checks: physics soaks, full-game automated play, fixed sequences for comparing strategies, and browser probes against the WebAssembly build. An agent can make a change, run the relevant check, inspect the failure, and try again without asking a person to reproduce every step.
That is the collaboration payoff we value most. “The change looks safe” is not useful evidence. A replay, a test result, or a measured before-and-after gives a reviewer something concrete to challenge.
Rust is not required for deterministic tools. Its constraints do help us keep state ownership explicit, and Cargo gives those tools one predictable home. The language makes the verification loop easier to maintain; the verification is what earns the trust.
Rust gives agents some bad habits too
The trade is not one-sided.
Incremental builds in this project commonly take 5–15 seconds. That is reasonable here, but a larger workspace would slow every edit-and-check loop. An agent also learns quickly that cloning a value can satisfy the borrow checker. It may stop there even when a better ownership design would remove the clone. The compiler accepts both; a human has to care which one belongs in the codebase.
Fast-moving libraries create another recognizable failure. wgpu and winit APIs change, while model training data retains old versions. The agent may reach for yesterday's method and let the compiler guide it toward the installed API. That is preferable to a latent failure, but it is still churn.
Most importantly, Rust cannot decide whether a feature should exist, whether a menu feels awkward, or whether a technically elegant abstraction makes the game harder to change. Type safety protects attention. It does not replace taste.
The platform and collaboration payoffs compound
Keeping one game core means an agent has one simulation to understand and one set of rules to preserve. A physics fix can be exercised against desktop tools, the browser build, and the iOS host without translating the behavior into another language first. The platform bridges still need their own testing, but the game does not split into three interpretations of the same mechanic.
We did not choose Rust because it was a good language to share with an AI. We chose it because a browser and an iPhone needed to share a physics game. After building this way, the collaboration benefit belongs on the same decision sheet: not because the agent writes perfect Rust, but because the language and tooling make imperfect work easier to inspect, rerun, and reject.