MRPCI/docs/CONTROL.md
m3ultra c83c90711d MRPCI v0.1 — the SCI-generation Monster Robot Party engine
Headless core (mrpci-core) + macroquad GUI (mrpci), inheriting MRPGI's
Command/Event bus architecture and jumping a Sierra generation:

- 256-color palettes with live median-cut quantization + palette cycling
- four screens: visual / priority / control / hotspot
- A* pathfinding with LOS-verified edges and line-exact path following
- perspective scale tables (actors shrink toward the horizon)
- point-and-click verb bar AND a text parser, one shared verb pipeline
- rhai room scripts (on_enter/on_verb/on_trigger + after() tick timers)
- deterministic 30Hz cycles, seeded RNG — byte-identical script replays
- checkpoint at room entry; death rewinds instead of restarting
- save-anywhere slots; multi-voice chip synth; JSONL/HTTP/MCP surfaces
- demo game: Neon Precinct (3 rooms, 25 points, one merciful fusebox)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 22:52:39 +10:00

5.3 KiB

Driving MRPCI programmatically

The engine core (mrpci-core) is fully headless: a GameState driven by JSON Commands in and Events 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:

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, --screens dir/.

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).

{"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)

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

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)

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:

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:<item>"/...
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.