Skip to content

Architecture

Marty McEnroe edited this page Aug 5, 2026 · 2 revisions

Architecture

The whole design follows from one decision: the expensive thinking happens offline, and the thing that runs during play does arithmetic.

Three programs, and only one of them runs during a game

Program When it runs Language What it does
The compiler Offline, once per scenario Python Turns public grid data into a scenario pack. All the modelling lives here.
The optimizer Offline, once per scenario Python, MILP via HiGHS Solves the pack for its exact optimal cost, which becomes the gold line.
The engine Every turn of every episode TypeScript Applies fixed rules to three numbers. No modelling, no solving, no randomness.

The compiler may fit curves, derive price slopes, take percentiles of an annual series, and build a scarcity adder. The engine may not. By the time a pack reaches a player it is a list of numbers, and resolving a turn is addition, multiplication, and a handful of caps.

That split is why the same engine runs in a browser, on a worker at the edge, and inside a test on your laptop, and produces identical results in all three.

  public grid data
        │
        ▼
  ┌───────────┐   pack.json    ┌───────────┐
  │ compiler  │ ─────────────► │  the pack │  (data: prices, campus, events, scoring)
  └───────────┘                └─────┬─────┘
                                     │
                    ┌────────────────┴────────────────┐
                    ▼                                 ▼
             ┌────────────┐                    ┌────────────┐
             │ optimizer  │  optimal cost      │   engine   │  turn resolutions
             │  (offline) │ ─────────────────► │ (runtime)  │
             └────────────┘   the gold line    └────────────┘
                                                      │
                                                      ▼
                                                 episode log

Scenarios are data, never code

A scenario is one JSON file, valid against a published schema. It carries the day's clock, the price series, the campus and its assets, the events, the scoring declaration, and its own provenance. Nothing in it executes.

This is the boundary that lets the library grow without touching the platform, and it is the reason a contributor needs a text editor rather than a development environment. It is also what makes the gate possible: a thing that is only data can be checked completely before it is trusted.

See The Scenario Pack.

The competitor contract is the crown jewel

Between the player and the platform sits a documented interface: what may be observed, what may be done, how a turn resolves, what an episode looks like.

The user interface is a client of that contract with no privileges. It cannot see anything an agent could not see, and it cannot do anything an agent could not do. That is what makes phase two a swap rather than a rewrite.

See The Competitor Contract.

The server is authoritative, and state is folded, not stored

At the venue, an episode's state is not kept as a mutable row that the client updates. Actions are appended to a log, and the current state is reconstructed by folding those actions through the engine from the beginning.

Two consequences matter. A client cannot assert a state it did not earn, because its assertions are not what the server reads. And the episode log is not a report about what happened; it is the thing that happened. Replay is not a feature bolted on afterward. It is the primary mechanism.

Determinism is contractual

Same pack, same action sequence, same resolutions, same score. Always.

The runtime contains no randomness at all. A forecast that misleads you is data compiled with a documented error model, not a die roll.

The strongest form of this is enforced by a test that ships with the code:

score(replay(episode_log)) === stored_score

Any change to the engine has to keep that true, and has to keep packs that use no optional features resolving bit-identically to the previous version. That constraint is why engine changes are careful and why this repository does not let you edit the engine locally.

Versioning

Three independent version numbers, all pinned into an episode when it is created:

  • pack_version for the scenario file
  • engine_version for the resolution rules
  • score_version for the scoring function

An episode created today is immune to a pack edited tomorrow. A breaking change to the observation or action shape bumps the schema from v0 to v1 and gets a new section in the contract, never a silent edit.

The wire

Field names crossing the wire are snake_case, and the JSON Schemas are normative. Where this documentation and a schema disagree, the schema is right.

What runs at the venue, and what runs here

The venue is a single Cloudflare Worker serving a React application, with D1 for episodes and R2 for packs. None of that is needed to build a scenario, so none of it is here.

Exedra ships the compiler, the optimizer, the engine, the scoring function, the schema, the acceptance gate, two packs, and a local runner that keeps an episode in memory. Same rules, no infrastructure.

See What Exedra Withholds for the full boundary, and Hydration for how the shared parts get here.

Clone this wiki locally