# simcore — the game sim as an embeddable, headless library

`simcore` wraps a game's deterministic simulation as a **C-ABI cdylib** so any host
language can drive a **fleet** of headless instances at full speed — no window, no
renderer, no netcode. It's the training/evaluation substrate: run thousands of sims in
parallel, observe them, snapshot/restore for search, and hash them for determinism
checks. The genetic-programming demo (`examples/evolve.c`) and the Python fleet driver
(`examples/simcore.py`) are built on it.

The same sim, observation, and action encoding that simcore exposes are the ones the
**live game** uses — so a bot's `decide(observation) → action` logic written against
simcore runs unchanged in the real game (see [Observation contract](#observation-contract)).

## Build

```sh
cargo build -p simcore --release        # → target/release/libsimcore.so
```

The C example links it dynamically (`-lsimcore -Wl,-rpath,<dir>`); the Python binding
`dlopen`s it (override the path with `SIMCORE_LIB=/path/to/libsimcore.so`).

## C-ABI

All functions return `int32` (`0` = ok, non-zero = error; call `sim_last_error()` for the
message). A `Handle` is an opaque `*mut` you own until `sim_free`.

| function | purpose |
|---|---|
| `sim_create(game, seed, ruleset_yaml, out*) ` | make one instance (`ruleset_yaml` "" = defaults) |
| `sim_clone(h, out*)` | independent copy of an instance |
| `sim_free(h)` | destroy an instance |
| `sim_step(h, input*)` | advance one instance one tick |
| `sim_step_many(handles*, inputs*, n)` | advance a batch one tick (serial) |
| `sim_step_many_parallel(handles*, inputs*, n)` | advance a batch across cores (GIL released) |
| `sim_observe(h, ptr*, len*)` | one flat observation record → `(ptr,len)` |
| `sim_observe_many(handles*, n, ptr*, len*)` | contiguous batch of flat records |
| `sim_observe_schema(game, ptr*, len*)` | the observation schema as JSON (decode records by walking it) |
| `sim_snapshot(h, ptr*, len*)` | serialize resumable state → `(ptr,len)` |
| `sim_restore(game, blob*, len, out*)` | mint a fresh instance from a snapshot blob |
| `sim_hash(h, out_u64*)` | determinism digest (`get_hash()`); equal ⇒ byte-identical state |
| `sim_current_tick(h, out_u64*)` | the instance's current tick |
| `sim_last_error()` | `const char*` message for the last failing call |

**Buffer lifetimes.** The `(ptr,len)` from `sim_observe*` and `sim_snapshot` points into a
scratch buffer that is **reused on the next call on that handle** — copy the bytes out
before the next call if you need to retain them.

**Input** is `InputFrame { u32 buttons; i16 axes[6]; }` — one per instance for the batch
step functions. Button bits match `GameButton::as_mask` (e.g. seaquest: `SOUTH=1<<0`,
`UP=1<<4`, `DOWN=1<<5`, `LEFT=1<<6`, `RIGHT=1<<7`).

## Python fleet driver (`examples/simcore.py`)

A thin, reusable ctypes binding — **stdlib only, no game-crate dependency**. It decodes
observations by walking the schema, so it stays in sync with the game automatically.

```python
from simcore import Fleet, BTN
fleet = Fleet("seaquest", seeds=range(64))     # 64 instances, one per seed

obs   = fleet.observe()                         # list of decoded state dicts (one per instance)
fleet.step([BTN.RIGHT] * len(fleet))            # advance all one tick; parallel=True fans across cores

snaps = fleet.snapshot()                         # save state (one blob per instance)
fleet.step([BTN.UP] * len(fleet))               # explore a branch
fleet.restore(snaps)                             # roll back — for search-based training (MCTS/rollout)
digests = fleet.hash()                           # per-instance determinism digest

fleet.free()
```

`Fleet` methods: `observe()` / `observe_raw()`, `step(buttons, parallel=False)`,
`snapshot()` / `restore(blobs)`, `hash()`, `len(fleet)`, `free()`. Because
`sim_restore` mints fresh handles, `restore()` rebuilds the fleet's instances from the
blobs; `len(blobs)` must equal the fleet size.

## Observation contract

The observation is defined **once** per game and feeds both hosts:

- the **flat** encoding simcore serves (packed little-endian, decoded by walking
  `sim_observe_schema`), and
- the **named-msgpack** encoding the live game serves to state bots.

For seaquest both come from one `BotStateView` in `games/seaquest/src/bot_state.rs`
(FlatObservation + serde derives). A model test,
`flat_and_msgpack_views_never_drift`, asserts every tick that the two encodings carry
identical data — so a `decide` function written against one host can't silently break on
the other. **Never let the two views diverge** (add fields to the shared view, not to one
encoding). The authoritative field list is `bots/README.md`.

## Determinism

Everything simcore exposes is deterministic given `(game, seed, ruleset, input stream)`:
same inputs ⇒ same `sim_hash`. This is what makes snapshot/restore search sound (a
restored branch reproduces exactly) and what lets a simcore-trained bot's run reproduce —
and verify — when replayed in the live game. `scripts/cross-determinism.sh` additionally
checks native-vs-wasm agreement of the hash stream.

## Watching runs (simviewer)

The sims are headless by design, but you don't have to fly blind. `simviewer` is a
standalone GPU window that renders a fleet with the real game's art, driven entirely by
your harness over a small text socket — point it at your training loop to watch a
population evolve, or a single champion play. `examples/evolve.c` streams its top runs to
it (`make simviewer-demo` shows the whole loop). See
[`../simviewer/README.md`](../simviewer/README.md) for the launch command and the control
protocol.

## Scope: an offline training/eval tool, outside the trust boundary

`simcore` is a repo-internal developer tool (`publish = false`) — it is **not** shipped to
players and is **not** a dependency of the game client or server. It runs the sim in your
own process for training and evaluation; it has no path to the leaderboard. A bot trained
here still has to reach a score the normal way: play through the real client, which records
a signed replay that the server **re-simulates from scratch** to derive the score. So the
convenience surface here — `sim_snapshot`/`sim_restore` (perfect save-states),
`sim_hash`, parallel fleets — is **not** a change to the trust boundary or the wire
protocol. It makes automated/TAS-style play easier to *develop*; it does not make a forged
or unearned score verifiable. Nothing here exposes a server secret: the seed is
caller-supplied, and the observation is the exact `BotStateView` already served to live
state-bots.

## Examples

- `examples/evolve.c` — genetic programming: evolves a seaquest controller over a fleet
  (parallel eval, snapshot-based death analysis, streams top runs to the simviewer).
- `examples/fleet_smoke.py` — drives a fleet from Python with nothing but ctypes + stdlib.
- `examples/evolve_seaquest.py`, `examples/bench.py`, `examples/native_bench.rs` — a
  Python GA sketch, a throughput benchmark, and a native benchmark.

See `docs/plans/sim-harness.md` for the design rationale.
