A deterministic expedition game built on an authoritative Go world simulation.
A layered agentic world simulator.
Start with the reviewer-ready walkthrough in DEMO.md.
Strata is a layered world simulator turned expedition game. The default route opens a three-contract campaign where one player-controlled scout must charge, mine, and survive regional failures before returning to a physical extraction site:
- Verdant Recovery establishes the route through a western food collapse
- Iron Scar Haul raises the ore quota during a haul-road outage
- Stormbreak closes the campaign inside a cascading grid failure
- Reserve Cell and Ore Sieve are available immediately; the faster Drift Rig unlocks after the first successful extraction
- every contract offers two deterministic routes with distinct spawn points, extraction windows, hazards, collision obstacles, and payout bonuses; Verdant's two routes additionally use hashed 32×18 authoritative heightfields whose slopes and blocked cells change movement and energy in Go
- Canopy Circuit's Canopy Mire and Spore Bloom overlap at the authored Living Causeway gate. Go replaces their ordinary combined traversal penalty with a content-hashed 82% pace / 3.4 energy-per-second interaction while both fields are occupied; HUD, minimap, and braided ground marks only present that result
- the three rigs carry authored non-weapon abilities: Reserve Pulse, Sieve Overdrive, and Drift Stabilizer; their 16/20/16-unit holds retain expedition ore outside the ambient market, and cargo mass now trades pace and energy for a viable extraction payload
- the isolated Go session is the sole progression authority and now issues the
canonical
strata.save-envelope.v1at field-station and debrief safe points. The save contains the exact authoritative campaign profile, compiled-content SHA-256 identity,strata.campaign-state.v2selections and Ironman flag, structured migration history, and replay provenance. Session-scoped run IDs make debrief application idempotent, browser autosaves rotate an opaque recovery copy, and Settings can export or import the engine save without an account - every terminal run opens a campaign debrief with the authoritative objective ledger, score, grade, payout, route/rig context, and newly earned unlocks; continuation is gated on the rotating autosave or an explicit manual recovery export. Replay export is the basis-bound session archive, verified by fresh authoritative command/fixed-step reconstruction; debrief review remains a bounded presentation history over that evidence
The same deterministic world remains available as a real-time survival sandbox:
- agents move in continuous 2D space
- food sources regenerate and deplete
- ore fields create a second resource pressure and a simple credit economy
- scarcity drives migration, clustering, deaths, reproduction, and wealth accumulation
- named scenarios apply timed regional shocks and log world events
- a browser viewer renders agents, food density, active pressure fields, and the event feed live
The campaign is built on a single-agent hero mode:
- the world now starts with
1focus agent - reproduction is disabled by default so the graphics stay isolated while you work on presentation
- the
Ecosystempreset restores a1000agent reproductive world for population-scale runs
The frontend now has two surfaces:
/serves the builtVite + TypeScript + Three.jsexpedition campaign/classicserves the built TypeScript Canvas viewer fromweb/classic/?showcase=1preserves the guided cinematic simulator walkthrough
The cinematic viewer is backed by the versioned strata.world.v1 manifest. It
describes an authored 8192×4608 fictional world on a 16×9 grid of 512-unit
chunks, four regions, and twelve landmarks. Simulation and replay coordinates
remain authoritative in Go; the renderer uses the manifest only for world
presentation and streaming. The manifest is available at
GET /api/world-manifest, and snapshots include its manifestVersion.
The bin/strata CLI wraps every command this repo runs. Put it
on your PATH once with bin/strata link (symlinks into ~/.local/bin), then:
strata dev
strata test
strata lint
strata coverage
strata web dev
strata site dev
strata verifystrata help lists everything, including the web/site build and lint
tasks, the desktop host gate, and the dependency audit. The development
commands run the same invocations as CI — the canonical, CI-verified forms
stay in AGENTS.md.
The browser viewers are built artifacts and are not committed, so build them once before the first run:
npm --prefix web ci
npm --prefix web run buildThen start the server:
go run ./cmd/strataThen open http://localhost:8080.
Strata can also start behind Flask without replacing its authoritative Go
simulation. The Flask process serves the Vite bundle, starts the Go server when
needed, and reverse-proxies /api/* (including the event stream):
python3 -m venv .venv-flask
source .venv-flask/bin/activate
pip install -r requirements-flask.txt
npm --prefix web ci
npm --prefix web run build
python flask_app.pyOpen http://127.0.0.1:5102. Set PORT for the Flask
port or STRATA_BACKEND_URL to reuse an already-running Go backend. If port
8080 belongs to an incompatible older service, the local launcher safely
starts the current backend on 58080 instead.
The server binds to loopback by default. Set STRATA_ADDR=127.0.0.1:8081 when
another local process already owns the default port. Binding another interface
is an explicit deployment choice.
Rebuild with npm --prefix web run build whenever you change anything under
web/. Without a build, the Go server still serves the API but has no viewer
assets to hand back.
| Variable | Consumer | Default | Purpose |
|---|---|---|---|
STRATA_ADDR |
cmd/strata |
127.0.0.1:8080 |
Listen address for the Go server. Loopback by default; binding another interface is an explicit deployment choice that changes the trust boundary below. |
STRATA_TOKEN |
cmd/strata |
unset | Shared bearer token that re-enables mutating requests when STRATA_ADDR is not loopback. Ignored on a loopback bind. |
PORT |
flask_app.py |
5102 |
Port for the Flask companion host. |
FLASK_HOST |
flask_app.py |
127.0.0.1 |
Bind host for Flask; loopback addresses only, anything else is refused. |
STRATA_BACKEND_URL |
flask_app.py |
http://127.0.0.1:8080 |
Go backend that Flask proxies /api/* to; set it to reuse an already-running server. |
STRATA_FALLBACK_BACKEND_URL |
flask_app.py |
http://127.0.0.1:58080 |
Where the launcher starts Strata when the preferred port is held by an incompatible service. |
STRATA_STATIC_DIR |
flask_app.py |
web/dist |
Built viewer bundle that Flask serves. |
VITE_STRATA_DESKTOP |
web/vite.config.ts |
unset | true routes the web build into dist-desktop for the desktop embed pipeline. |
Summarized here; SECURITY.md is the full statement, including what the code already enforces and how to report a vulnerability.
Strata's HTTP API is a local, single-user control plane. It has no accounts,
no roles, and no per-request authorization: any caller that can reach the port
can drive the simulation. That is deliberate, and it is safe only because the
server binds 127.0.0.1 by default, so the operating system limits callers to
this machine.
Setting STRATA_ADDR to anything else publishes those endpoints to the network,
so the server fails closed rather than trusting the new audience:
STRATA_ADDR |
STRATA_TOKEN |
Mutating requests (POST/PUT/PATCH/DELETE) |
Read-only requests |
|---|---|---|---|
| Loopback (default) | ignored | Served, unauthenticated | Served |
| Non-loopback | unset | Refused with 403, and a warning is logged at startup |
Served |
| Non-loopback | set | Require Authorization: Bearer $STRATA_TOKEN, else 401 |
Served |
The guard is enforced centrally by HTTP method, so a mutation route added later inherits it automatically. Read-only endpoints stay unauthenticated in every mode: the token narrows who can change the world, not who can watch it. If you need confidentiality as well, terminate TLS in front of Strata — it speaks plain HTTP and the bearer token is only as private as the transport carrying it.
The primary web client creates an isolated simulation with POST /api/sessions
and includes its sessionId on snapshot, control, operations, intervention,
contract, replay, and SSE requests. Separate browser tabs therefore run
separate authoritative expeditions. GET /api/sessions/{id} describes a live
session and DELETE /api/sessions/{id} closes it; server-side expiry remains a
fallback for abandoned clients. Every state endpoint requires a sessionId:
requests without one are rejected with 400 missing sessionId rather than
falling back to a shared world. Mutation requests require same-origin
application/json; this keeps the local browser API unavailable to cross-origin
form/fetch traffic while preserving CLI and native-adapter access.
Every authoritative snapshot carries an {epoch, sequence} cursor. The browser
rejects duplicate, stale, or malformed snapshots across control responses and
both live transports before presentation state can change. GET /api/campaign-profile?sessionId={id} reads the session-owned progression
ledger; PUT replaces an exact-schema progression profile only while the
expedition is idle or terminal. Restore rejects host-mode changes, unknown
fields, unearned unlock closure, oversized input, and active-run replacement.
GET /api/campaign-state?sessionId={id} returns the owned v2 field-station
selection state; PUT atomically validates and replaces its complete mutable
selection/route map and Ironman intent. Campaign-state updates are replay
neutral, unavailable during active expeditions, and cannot disable a
consequence-locked Ironman campaign. Progression hosts also reject start,
restart, or reset commands that would erase an active expedition.
GET /api/replay?sessionId={id} returns a verified
strata.replay-archive.v2. Its replay basis binds the exact game and engine
versions, current GameContent schema and SHA-256, normalized deterministic
Config SHA-256, and effective initial seed. Each successful command and each
fixed Step call consumes one monotonic invocation ordinal in addition to
epoch/tick, so paused steps, same-tick commands, speed changes, and resets have
one reconstructable order. Command-record hashes bind the basis hash;
checkpoint proofs bind the complete basis, cursor, command boundary, reason,
and public state fingerprint. The archive final proof binds its basis, exact
final cursor/state, command chain, checkpoint proof list, and truncation state.
Before HTTP or the desktop bridge returns an archive, VerifyReplayArchive
rejects incompatible engine/content/config, noncanonical commands, broken
chains or proofs, malformed or over-budget schedules, missing or divergent
checkpoints, truncation, and final-state drift. It then instantiates a fresh
simulation, reproduces every inferred fixed step and recorded command, and
matches the complete periodic/epoch/terminal checkpoint schedule plus final
snapshot. Checkpoints remain inspection evidence with resumable: false until
private simulation and PRNG state have a versioned codec. Active expeditions
checkpoint every 6,000 fixed steps (five minutes at 20 Hz), with 128 retained
records; the conservative three-contract 0.25x campaign bound is 43 records.
Command count/bytes and total reconstruction work are independently bounded; a
cutoff advances monotonically when evidence is evicted, and a truncated archive
deliberately cannot pass full reconstruction. Snapshot storage is deep-owned on
capture and export. The paused expedition menu exports this canonical archive
directly.
GET /api/save-envelope?sessionId={id} issues the session's canonical campaign
save at the field station or debrief. PUT accepts the unchanged raw JSON body
at the same safe points and returns the accepted canonical form. Imports are
limited to 4 MiB and fail closed on duplicate or unknown keys, trailing JSON,
incompatible engine/content identity, invalid owned payloads, migration-chain
errors, or a campaign access-mode mismatch. A successful import atomically
replaces the authoritative CampaignProfile, CampaignState, and
session-owned migration history; it never partially restores any of them.
Ironman imports must preserve monotonic consequences and prove the exact
overlap of the bounded debrief ledger; once the entire retained overlap has
expired, a direct leap is rejected until a cumulative chain anchor exists.
The additive strata.game.v1 → strata.game.v2 migration accepts only the
exact pre-v2 compiled-content SHA-256 at a safe point, appends the structured
content/add-expedition-cargo-v1 record, and returns the current canonical
save. This unshipped v2 lands retained cargo and public route
hazard/obstacle/interaction geometry as one atomic content version; no
intermediate v2 hash is supported. Unknown hashes and active-run imports remain
rejected atomically. Exact strata.campaign-state.v1 payloads migrate to v2
through the authored campaign-state-v1-to-v2 record; unknown, duplicate, or
contradictory migration lineage fails closed.
V1 intentionally omits runCheckpoint, and save issue/import/export is blocked
during an active expedition until private simulation and PRNG state have a
versioned resumable codec. replayHead.checkpointHash is the latest retained
v2 checkpoint proof, which transitively binds basis, invocation cursor,
command boundary, reason, and state fingerprint. It records provenance, but
does not restore a run and cannot locate its archive unless a caller persists
that archive as a sidecar. An exact pre-cargo save may preserve its historical
state-hash pointer during safe-point migration; the next engine reissue
replaces it with the current v2 proof because the old envelope cannot recreate
a missing legacy sidecar.
GET /api/game-content publishes strata.game.v2: each contract's retained
cargo target, each rig's cargo capacity, immutable route hazards, obstacles and
linked interactions, and each Verdant route's terrain schema, ID, and SHA-256
hash plus the quantized height lattice and boolean walkability mask once. The
20 Hz snapshot carries matching live terrain contact and authoritative active
interaction state. Replay state now identifies strata.public-snapshot.v3;
consumers must resolve the referenced terrain and route geometry from the
matching content catalog.
internal/desktopbridge exposes the same authoritative session, command,
snapshot, campaign-profile restore, campaign-state read/update, inspection,
save, and replay contracts through a dependency-free binding surface.
web/src/transport/desktop-simulation-transport.ts
consumes those bindings and the versioned strata:simulation-snapshot event
stream. The primary UI selects that adapter when a packaged host injects
window.__STRATA_DESKTOP__; otherwise it retains the HTTP/SSE browser-demo
path.
cmd/strata-desktop is the native host and pins Wails v3.0.0-beta.3. It is a
separate Go module with its own go.mod/go.sum, which is what keeps the
shipped server honest: the root strata module requires nothing outside the
standard library, so go build ./cmd/strata never resolves the Wails runtime or
its ~137-package transitive toolchain. CI enforces the boundary by building and
testing the server with no desktop system libraries installed; the separate
desktop job installs GTK/WebKit and runs make desktop. Because Go evaluates
the internal rule on import paths, the module path stays strata/cmd/strata-desktop
so the host can still import strata/internal/.... Production tasks generate
typed bindings, build the desktop-mode
Vite entry into web/dist-desktop, stage it for embedding, compile the host,
and create an ad-hoc-signed macOS technical-proof bundle at
artifacts/Strata.app. The browser demo remains separately buildable in
web/dist, so desktop packaging cannot replace its HTTP entry with one that
expects the Wails runtime. See
docs/DESKTOP_BRIDGE.md for the binding/event contract,
reproducible task sequence, and explicit release-work boundaries.
The dependency-free public strata/contracts Go package defines strict,
versioned SaveEnvelope, ReplayHead, and data-only ContentPackManifest
formats. The save envelope and v2 campaign state are now wired through the
simulation session, HTTP/SSE adapter, desktop bridge proof, and browser
recovery slots. Its
contentHash is the canonical SHA-256 of the current compiled GameContent,
including authoritative terrain, cargo limits, and route interaction geometry. The same package provides exact-decimal
canonical JSON, SemVer engine/dependency ranges, dependency-cycle checks,
portable-path and payload-budget validation, and engine-owned schema
validation. Content packs cannot declare scripts, plugins, native libraries,
shaders, or WASM.
Content-pack activation remains foundation work rather than a claim that the
hard-coded catalog has migrated. See
docs/PRODUCT_CONTRACTS.md for the activation and
validation sequence.
- choose a contract, extraction route, and scout rig at the field station
WASD/ arrow keys or a standard controller steer the expedition scout; analog input has a configurable dead zone and is quantized before it becomes an authoritative command- click the terrain to set a route target
Space/ the controller south button activates the fitted rig abilityEscape/ the controller menu button pauses or resumes, with keyboard focus contained in the pause menu and rapid pause edges coalescedAbandon expeditionin the pause menu uses a deliberate two-press confirmation and returns through the same authoritative, saved debrief flowSettingsis available from the field station and pause menu, with interface scaling, system-aware reduced motion, a color-safe palette, graphics presets, authored audio-cue captions, persistent conflict-free keyboard/controller rebinding, analog axis/dead-zone controls, rotating save recovery, and manual save export / safe-point importExport replayin the pause menu writes the authoritative hash-chained session command/checkpoint archive rather than the renderer's interpolation buffer; the engine reconstructs and verifies it before HTTP or desktop export- win, failure, and abort each open the full-screen expedition debrief exactly
once per run;
Review runpresents the terminal world and evidence deck,Return to debriefrestores the outcome, andContinue to field stationremains unavailable until campaign authority has accepted the outcome and a local autosave or manual recovery copy exists. Review mode cannot issue authoritative commands - complete charge and hold objectives, retain the required cargo in the rig, then physically bring that payload into the marked extraction site before the 15–20 minute contract window closes
Pause/Resumeis also available in the inspector controls1x,2x,4xsimulation speed- seed input plus
Reset Seed/Load Preset - presets for
Focus Agent,Competition Demo, andEcosystem - scenario picker with
Start Scenario/Clear Scenario - one-click demo buttons for
Food CollapseandBloom Shift Autoplay Demofor a staged in-browser presentation- guided demo briefing with stage progress, live metric readouts, and mobile-safe presentation overlays
- replay scrubber with event bookmarks and live/replay switching
- presentation mode for a cleaner showcase surface
- pilot takeover with click-to-move and
WASD/ arrow key steering - metrics for credits, ore reserves, and ore price
- event feed showing phase transitions and scenario completion
For the cinematic or hands-free presentation launch, open:
- http://localhost:8080?showcase=1
- http://localhost:8080?showcase=1&autodemo=1
- http://localhost:8080?intro=skip to enter spectator mode immediately
- http://localhost:8080?graphics=legacy to use the procedural legacy renderer
- http://localhost:8080/classic/ for the Canvas fallback
The guided demo runs through baseline competition, a western food collapse, a migration corridor cue, an eastern bloom reset, and replay review. The sidebar briefing explains what to watch, while presentation mode keeps the same stage, progress, and metric readouts over the 3D viewport.
The full local quality gate matches CI:
make testIt verifies Go formatting, Go tests, the deterministic 90+ simulator eval, and the Vite build for both the 3D and classic viewers.
Manifest and deterministic checks can be run independently:
make manifest-check determinism-checkThese checks prove all 144 chunk coordinates are covered, landmark and LOD references validate, presentation presets retain the manifest dimensions, and same-seed snapshots remain byte-identical. Go replay tests additionally prove fresh full-schedule reconstruction and command/checkpoint/final-proof tamper rejection through the engine, HTTP, and desktop boundaries.
Run the simulator eval directly when you want the scored evidence:
go run ./cmd/strata-eval -min-score 90Individual checks are still available:
go test ./...npm --prefix web run buildcmd/strata: HTTP server and streaming endpointscmd/strata-eval: deterministic 90+ quality gate for simulator behavior and demo evidencecontracts: public dependency-free save, replay-head, canonical hashing, and data-only content-pack formatsinternal/sim: deterministic simulation coreinternal/sim/campaign.go: versioned contract, route, hazard, obstacle, loadout, and ability cataloginternal/sim/terrain.go: versioned quantized route terrain, public hashes, bilinear contact, swept walkability/grade collision, and reachability checksinternal/sim/campaign_profile.go: exact-schema, session-owned progression, unlock closure, bounded debrief history, and safe-point restore validationinternal/sim/session.go: isolated simulation-session ownership, lifecycle, cleanup, and serialized stepping/commandsinternal/sim/command.goandinternal/sim/replay.go: canonical command application plus basis-bound, hash-chained, full-schedule replay verificationinternal/desktopbridge: Wails-neutral single-session binding and snapshot event proof with deterministic parity testsinternal/sim/world_manifest.go: authoritative world manifest and validationweb: Vite/TypeScript source and built assets for both browser viewersweb/src/campaign/campaign-progress.tsandweb/src/campaign/campaign-authority.ts: rotating opaque engine-save recovery slots, separate campaign preferences, legacy one-way migration, and the read-only projection of Go campaign authority into field-station presentation stateweb/src/campaign/campaign-authority-queue.tsandweb/src/session/snapshot-cursor.ts: serialized authority synchronization and stale-snapshot rejection shared by the browser and desktop runtime pathsweb/src/expedition/expedition-setup.ts: contract, route, rig, and campaign field-station boardweb/src/expedition/expedition-debrief.ts,web/src/expedition/expedition-debrief-lifecycle.ts, andweb/src/expedition/expedition-debrief-view.ts: pure authoritative outcome model, once-per-run review/sync lifecycle, and accessible responsive debrief surfaceweb/src/renderer/terrain-surface.ts: strict route/catalog/contact identity checks and presentation-only height/contact interpolation; it never decides traversalweb/src/input/input-context.ts: explicit campaign, gameplay, inspection, replay, pause, and modal input policyweb/src/input/input-bindings.tsandweb/src/input/pilot-input.ts: device-local, versioned input profiles plus the single quantized/coalesced authoritative steering sinkweb/src/input/camera-mode.ts: deterministic mapping from experience/input state to cinematic, chase, spectator, and inspection camerasweb/src/transport/simulation-transport.tsandweb/src/transport/http-sse-transport.ts: transport-neutral simulation calls and the isolated HTTP/SSE adapterweb/src/transport/desktop-simulation-transport.tsandweb/src/transport/runtime-simulation-transport.ts: validated desktop event/binding adapter plus automatic packaged-host/browser selectionweb/src/hud/player-settings.ts: validated accessibility and presentation preferences stored separately from campaign progress asstrata.settings.v1web/classic: classic Canvas viewer HTML entry served at/classicafternpm --prefix web run buildevals: MVP capability and regression checksdocs/CINEMATIC_ACCEPTANCE.md: browser, performance, and reduced-motion acceptance checklistdocs/DESKTOP_BRIDGE.md: versioned desktop binding/event contract and the pinned Wails packaging proof and remaining storefront release boundarydocs/PRODUCT_CONTRACTS.md: SaveEnvelope and data-only content-pack contract semantics and activation orderdocs/WORLD_PIPELINE.md: source-art and runtime asset pipeline
- work from short-lived branches off
main - open pull requests back into
main - let GitHub Actions run the full
make testgate: Go formatting, Go tests,go run ./cmd/strata-eval -min-score 90, and the Vite build for both browser viewers - use the PR template and issue templates to keep changes reviewable
- The 3D viewer combines validated authored GLB assets with generated terrain, simulation-driven agents, and procedural atmospheric effects.
- The Go simulation remains authoritative; the frontend adapts live snapshots and replay frames without adding backend pose hints.
- The server still uses server-sent events for snapshot streaming. That keeps the engine/viewer split intact across both viewers.
MIT — see LICENSE.