You say what. The compiler decides how.
Imperative code tells the machine the steps: loop, check, cache, invalidate, redraw. Declarative code states the result you want and leaves the steps to a compiler that knows your whole game. The difference is not style — it is who owns the hard decisions, and whether a change costs one line or four files.
Show the 10 cheapest offers the player can afford. Keep it current.
public sealed class ShopOffers(Wallet wallet, OfferCatalog catalog, OfferListView view){readonly List<Offer> cache = new();③bool dirty = true;③int lastGold = -1;public void Start(){wallet.OnGoldChanged += _ => dirty = true;catalog.OnPriceChanged += _ => dirty = true;// (no OnOfferRemoved handler)④}public void Update()①{if (!dirty && wallet.Gold == lastGold) return;Rebuild(wallet.Gold);}void Rebuild(int gold){var result = new List<Offer>();②foreach (var o in catalog.All)if (o.Price <= gold) result.Add(o);result.Sort(ByPrice);cache.Clear();for (int i = 0; i < result.Count && i < 10; i++)cache.Add(result[i]);lastGold = gold;dirty = false;view.Refresh(cache);}static int ByPrice(Offer a, Offer b) => a.Price.CompareTo(b.Price);}
- ①runs every frame
- ②allocates a list per run
- ③manual cache + dirty flag
- ④forgot: offer removed → stale row
reactive query AffordableOffers(gold: int) -> Offer[] {from Offer o| filter o.Price <= @gold| sort o.Price| take 10}
imperative · ≈ 40 lines · 4 decisions that are yours · 1 bug already indeclarative · 6 lines · 0 of those decisions are yours
The same requirement, written as steps on the left and as a result on the right. The four marked places on the left are decisions you own — and one is already a bug.
Why this is the door for assistants
Small surface
The whole language fits in a few pages. An assistant can hold all of it, and so can you.
The compiler reviews every draft
A draft is bound, typed and planned before it exists. Wrong shapes come back as diagnostics, not as bugs in production.
Humans review intent
You read six lines of what, not forty lines of how.
- 01Closed worldYou declare the whole game: state, commands, the questions your UI views ask, modules, frame rules. There is no query the compiler has not seen and no command it has not typed.
- 02CompilerIt binds, type-checks, plans with a cost-based optimizer and emits plain C#. No parser, no planner, no reflection on the device. A shape it cannot guarantee is rejected with a diagnostic, never degraded silently.
- 03DeterminismCommands are pure transitions over declared state: a fixed-point math library (Fixed64) instead of floats, seeded random declared in the schema, a single writer per state owner. Same inputs, same state — on x86-64, ARM64 and IL2CPP.
- 04ReplaySo the same generated command can run on the device and in a per-player grain. The device applies it immediately and journals it; the server replays the journal and compares the state. That is the whole trust model.
One command. Two places. One answer.
The command you declare is compiled once. The device runs it to move the game forward; the server runs the very same code to check. Here is BuyItem(sword) doing both.
- input state
- #91c0
- seed
- 0x5A3C
- output state
- —
- time
- — µs
- input state
- #91c0
- seed
- 0x5A3C
- output state
- —
- time
- — µs
same result verified on x86-64, ARM64 and IL2CPP
Both sides start from the same verified state and the same seed — the only inputs the command can read.
public static class BuyItemExecutor{public static BuyItemOutcome ExecuteCore(DbContext context, BuyItemCommand command, CommandExecutionFrame frame){if (context is null) throw new global::System.ArgumentNullException(nameof(context));var db = (GameDbContext)context;if (!(command.cost > 0)){return BuyItemOutcome.Failure(BuyItemErrorCode.InvalidCost);}var __mutation1Key = command.playerId;Wallet __mutation1Row;var __mutation1Matched = db.Wallets.TryFindById(__mutation1Key, out __mutation1Row);if (!__mutation1Matched){return BuyItemOutcome.Failure(BuyItemErrorCode.NoWallet);}__mutation1Row = new Wallet{PlayerId = __mutation1Row.PlayerId,Gold = __mutation1Row.Gold,};__mutation1Row.Gold = __mutation1Row.Gold - command.cost;db.Wallets.Update(__mutation1Row);var wallet = new { gold = __mutation1Row.Gold };// … upsert Inventory: same shape — key, TryQuery on its unique index, Update or Addreturn BuyItemOutcome.Success(new BuyItemResult { gold = wallet.gold });}// + Execute(...) wrapper and error mappers}
Not a protocol. Not a second implementation. The same compiled command, with a fixed math library and a seeded random, so both sides can only reach the same state.
Five stages, all at build time
Nothing on this path runs on the player's device. The output is plain C# (or a portable artifact) — there is no query parser on the hot path.
A glass box, not a black box
A Cascades-style, cost-based optimizer makes the choices a senior engineer makes by hand — which index, which join strategy, what order, when to stop scanning, whether to eliminate a sort — but deterministically and exhaustively, using your schema, your indexes, cardinality estimates and profile data. The difference: every choice is inspectable. The plan is output you can read; the strategy names are real and finite; nothing is hidden.
The optimizer can also read a profile of real sessions — including deterministic replays — so cardinality estimates, plan choices and buffer sizes come from how the game is actually played, not from guesses. The profile is governed and fail-closed: a stale, unapproved or schema-mismatched profile is rejected, never silently used. Profile-guided optimization →
// illustrative: the plan ConjureDB picks for GetTopScorersTopN (limit=count, order=Score desc)└── Scan (Player,strategy=SortedSetScan,index=Player_ByScore)
Ask for the plan and read what it picked — an index walk, bounded, no sort, no allocations.
Not an event bus. A maintained result.
Rebuilding every frame pays for the whole table even when nothing changed. Rx moves events and every subscriber re-runs its query. Incremental view maintenance moves the change itself through the query's own operators and keeps the result as a table. Same event, three mechanisms — step through it.
- sourcePlayers · 10 000 rows1 row changed
- maintainersjoin · sort · take 10probe 1 key · insert at #7, evict #10
- resultTopPlayers · 10 rows2 rows changed
frame budget · ≈ 3 rows × 5 changes = 15 rows per frame
The level-up travels as one delta: probe one guild, compare with the cut-off, insert at #7. Two rows of the result change; nothing else is touched.
reactive query TopPlayers() -> LeaderboardRow[] {from Players| join Guilds g (GuildId == g.Id)| sort { -Level, Name }| take 10| select { … }}
IVM is not a subscription trick. The compiler lowers the query into maintainers — filter, join arrangements, grouped accumulators, bounded Top-K state — and each commit pushes only the delta through them. The result is a table: consistent once per commit, proportional in cost to the change, and indexable like any other table. A reactive query is how a UI view subscribes to that view model. A shape the maintainers cannot keep does not compile.
And it is honest about its limits: a query shape it cannot maintain incrementally is rejected at compile time, never silently degraded to a hidden full rescan.
What the closed world rules out
Queries nobody declared
Designers cannot type new queries that run on players' devices. Changes after the build ship as compiled artifacts through the liveops lane, verified like everything else.
Many writers on one state
Each piece of state has one owner and one writer. Shared state such as clans and leaderboards gets its own owner on the server; there is no multi-writer contention to resolve.
Rendering and feel
Conjure is the logic and the data. Unity still draws the frame, plays the sound and reads the touch.
A schema to maintain
There is a schema and a build step. For a prototype you will delete in two weeks, a dictionary is still the right answer.
If you don't trust it, read it
The generated code is plain source you can open. There is no reflection, no runtime code generation, nothing dynamic on the query path — which is exactly why it survives IL2CPP and a tight mobile frame. Conjure earns trust by removing mystery, not by asserting quality.