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.