In development

Project Solarpunk

A mobile farming game where the server owns the farm — Nakama storage the client cannot write, crops timed on the server's clock, and every rule proved by breaking it.

A casual, multiplayer farming simulation for phones: a Unity 6 client on the Universal Render Pipeline, a Nakama game server behind it, and the same working contract as everything else on this site — a written agent contract, milestones with a definition of done, and a queue of record. Three of eight milestones are closed; the art direction is on its second pass.

It is a game, but the interesting decisions are backend ones. The client is a view. It cannot write the farm, cannot decide a harvest, and does not know what time it is.

The client asks, it does not decide

The farm is one JSON document in Nakama storage, owned by a server-side Lua module with permission_write = 0. The client physically cannot write it. A tap on a tile sends an RPC — farm_plant, farm_harvest — and nothing appears on screen until the server has validated bounds, occupancy and ripeness and answered. Every answer carries the whole farm and the server’s clock, so a returning player’s crops are at the right stage on the first frame they exist.

The property is pinned by breaking it: flip the permission to 1 and exactly one test goes red, AClientCannotWriteTheFarmDocument, and nothing else.

The device clock is not an input

A wound-back phone clock is the oldest cheat in the genre, so DateTime.UtcNow is gone from every gameplay path — an EditMode scan of the source refuses it. One server-time RPC anchors a monotonic reading, and the client extrapolates from that. A failed sync retries with backoff and the farm stays inert; it never guesses. The DoD test stages server time months away from the machine’s own clock, so a leak presents as a crop ripe on the frame it was planted.

Getting there found a real defect in the stack: Nakama’s refresh-token expiry defaults to 3600 seconds, shorter than the 7200-second session token, which makes the refresh leg unreachable and silently degrades “stays signed in” to re-authenticating on every launch. Proved the same way — revert the flag and one test goes red.

A refusal is not an error

Three runtime behaviours were captured from the running server before the client was written, and each changed the design. The one that mattered most: a Lua error() becomes an HTTP 500, which the Unity SDK retries as transient and then reports as a bare cancelled task. So a player-causable refusal — tile occupied, crop not ripe — is a successful response carrying ok: false and a reason. The server’s grid and schedule constants exist in Lua as well as C#, and a contract test parses them out of the module and fails the build if they drift.

What the render cannot measure

The art milestone has a budget — one material, 150 draw calls, 67,000 triangles — and it holds: 89 draw calls, 63,960 triangles, 341 tests green. It was also rejected twice with every gate green. Measuring a render catches a regression against an intent and says nothing about whether the intent was any good, so the milestone does not close on a number. It closes when someone looks at it.

All work