# Driving MRPCI programmatically The engine core (`mrpci-core`) is fully headless: a `GameState` driven by JSON `Command`s in and `Event`s out, over your choice of surface. The GUI is just one more consumer of the same bus. ``` GUI (macroquad) ─┐ CLI / stdio ─────┤ HTTP ────────────┼──▶ Command ▶ GameState ▶ Event MCP (Claude) ────┤ Python ──────────┘ ``` Everything below uses one binary: ```sh cargo build -p mrpci-core # builds target/debug/mrpci-headless mrpci-headless --help ``` `--game DIR` points any mode at a game folder. Useful boot flags: `--seed N` (deterministic RNG), `--room N`, `--ticks N`, `--render-room out.png` (annotated: room chip, score, dialogue/transcript text stamped with the built-in font; add `--raw` for the bare scene), `--screens dir/`. RoomDoc extras beyond the basics: procedural paint ops `fbm_fill` (ramp/scale/octaves/seed), `voronoi_fill` (cell/colors/edge_color/edge/seed), `scatter` (colors/density/seed) — all deterministic by seed — and a `weather` field (0 none, 1 rain, 2 snow, 3 embers) whose particles run on a separate RNG stream (visual-only; event replays unaffected). ## 1. JSONL stdio (the substrate) One JSON command per line on stdin, events stream back on stdout. Lines starting with `#` are comments (script files too). ```jsonl {"cmd":"verb_at","verb":"look","x":100,"y":120} {"cmd":"verb_at","verb":"do","x":268,"y":112} {"cmd":"use_item_at","item":"keycard","x":286,"y":138} {"cmd":"parse","text":"use keycard on locker"} {"cmd":"move_to","x":150,"y":150} {"cmd":"walk_to","x":316,"y":150} {"cmd":"tick","n":30} {"cmd":"choose","n":1} {"cmd":"end_dialogue"} {"cmd":"query"} {"cmd":"new_game"} {"cmd":"goto_room","n":2} {"cmd":"set_flag","name":"locker_open","value":true} {"cmd":"set_var","name":"drip_armed","value":0} {"cmd":"set_seed","seed":41} {"cmd":"save_game","slot":"street"} {"cmd":"restore_game","slot":"street"} {"cmd":"upsert_room","n":7,"doc":{"spawn":[160,170]},"persist":true} {"cmd":"upsert_script","file":"room7.rhai","source":"fn on_enter() { say(\"hi\"); }"} {"cmd":"save_room"} {"cmd":"load_game","dir":"games/other"} {"cmd":"reload_assets"} ``` Notes: - The engine only advances when told: `tick` runs fixed 1/30s cycles (movement, timers, triggers, NPC wander). `walk_to` ticks until arrival. - `verb_at` verbs: `walk`, `look`, `do`, `talk`. Distant do/talk/take targets are walked to first (the pending verb fires on arrival). - `parse` accepts the same things a player types: `look at sign`, `pick up the keycard`, `use keycard on locker`, `talk to sergeant`, `inventory`, `save`, `score`. - `upsert_room` validates before committing (walkable spawn, exits and door hotspots must point at rooms that exist) and rejects bad lore with an `error` event. - Events: `transcript`, `room_changed`, `inventory_changed`, `dialogue_open` / `dialogue_closed`, `score_changed`, `died` (checkpoint rewind follows), `won`, `audio` (cue name), `music` (mood), `ego_moved`, `state`, `saved_game` / `restored`, `room_upserted`, `room_saved`, `game_loaded`, `error`. ### Script replay (deterministic) ```sh mrpci-headless --game games/neon-precinct --script play.jsonl > events.log ``` Fixed cycles + seeded RNG + tick-based timers ⇒ the same script produces **byte-identical** events on any machine, any speed. That's the golden-test workflow for CI — and the reason none of the classic Sierra speed bugs can exist here. ## 2. HTTP ```sh mrpci-headless --game games/neon-precinct --serve 8093 ``` ``` POST /command one Command JSON → JSON array of Events GET /state StateSnapshot GET /frame.png current frame, headless-rendered (cycles applied) GET /screen/priority.png depth bands, visualized GET /screen/control.png walls red, water blue GET /screen/hotspot.png hotspot id map GET /events?since=N cursor-polled event log POST /rooms/{n}[?persist] body = RoomDoc JSON (the lore drop) ``` ## 3. MCP (Claude as player, tester, co-author) ```sh claude mcp add mrpci -- /path/to/mrpci-headless --mcp --game /path/to/games/neon-precinct ``` Tools: `mrpci_verb`, `mrpci_use_item`, `mrpci_parse`, `mrpci_walk_to`, `mrpci_choose`, `mrpci_tick`, `mrpci_state`, `mrpci_render_frame`, `mrpci_render_screen`, `mrpci_save`, `mrpci_restore`, `mrpci_new_game`, `mrpci_load_game`, `mrpci_upsert_room`, `mrpci_upsert_script`, `mrpci_save_room`, `mrpci_command` (escape hatch). `mrpci_render_frame` / `mrpci_render_screen` return real PNGs, so a model can look at the frame — or the invisible screens — it just authored. ## The rhai scripting surface Hooks a room script (or `main.rhai`, merged underneath) may define: ```rhai fn on_enter() // room became live (also after death-rewind!) fn on_exit() // leaving the room fn on_verb(verb, noun) // FIRST refusal on every verb; return true = handled // verb is "look"/"do"/"talk"/"take"/"use:"/... fn on_trigger(name) // ego stepped into a trigger hotspot fn on_tick() // every cycle (keep it light) fn any_name() // timer target for after(ticks, "any_name") ``` API: `say(t)`, `say_by(who,t)`, `flag(n)`, `set_flag(n,v)`, `val(n)`, `set_val(n,v)`, `has(item)`, `give(item)`, `remove_item(item)`, `room()`, `ego_x()`, `ego_y()`, `goto_room(n)`, `place_ego(x,y)`, `walk_ego(x,y)`, `freeze_ego(b)`, `npc_walk(name,x,y)`, `npc_place(name,x,y)`, `npc_freeze(name,b)`, `play(cue)`, `music(mood)`, `points(n)`, `win()`, `die(text)`, `after(ticks, "fn")`, `pal_cycle(i, on)`, `rand(n)`. Reads see a snapshot; writes queue as effects applied after the hook returns. A script can print nonsense but cannot corrupt the sim, and a runaway loop is killed at 50k ops. `rand` is the engine's seeded xorshift — scripts stay replay-deterministic.