MRPGI/web/manual.html
type-two e9cf00e686 THE MRPGI BOOK: deep combined user/dev manual, linked from the live menus
- web/manual.html — 17 chapters: playing, the editor deeply, the
  authoring cookbook (full JSON reference + the five load-bearing
  puzzle patterns), dialogue trees, flags, the art pipeline with
  sprite-generation prompts, sound, driving the engine with code
  (real JSONL transcripts), AIs + MCP, prompt patterns with actual
  outputs (the roof room is the real shipped expansion), the web
  build & cloud saves, engine internals, extension recipes,
  troubleshooting, and full command/event/palette reference.
- Game menu gains "Manual": window.open on the web (via a JS plugin
  import), the default browser natively.
- README tab links the Book; repo README points at it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 11:49:24 +10:00

850 lines
51 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The MRPGI Book — user &amp; developer manual</title>
<meta name="description" content="The complete manual for MRPGI, the Monster Robot Party Game Interpreter: playing, painting, authoring games as JSON, driving the engine from terminals and AIs, and the engine internals.">
<style>
:root {
--bg: #0b0d11; --panel: #12151c; --ink: #cdd5e1; --dim: #7d8798;
--pink: #ff87d7; --cyan: #5fffd7; --yellow: #ffe95f; --border: #2a3140;
}
html { scroll-behavior: smooth; }
body {
margin: 0; background: var(--bg); color: var(--ink);
font: 15px/1.65 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
main { max-width: 880px; margin: 0 auto; padding: 24px 20px 120px; }
h1 { color: var(--pink); font-size: 30px; letter-spacing: 2px; margin: 18px 0 4px; }
.subtitle { color: var(--dim); margin: 0 0 28px; }
h2 {
color: var(--pink); font-size: 21px; letter-spacing: 1px;
margin: 56px 0 10px; padding-top: 18px; border-top: 1px solid var(--border);
}
h3 { color: var(--cyan); font-size: 16px; margin: 30px 0 6px; }
h4 { color: var(--yellow); font-size: 14px; margin: 22px 0 4px; }
p, li { margin: 8px 0; }
a { color: var(--cyan); }
b { color: #fff; }
i { color: var(--yellow); font-style: normal; }
kbd {
background: #1e232b; border: 1px solid #48505f; border-radius: 4px;
padding: 0 6px; color: #fff; font: inherit; font-size: 13px;
}
code { color: var(--yellow); font-size: 13.5px; }
pre {
background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
padding: 14px 16px; overflow-x: auto; font-size: 13px; line-height: 1.5;
}
pre code { color: var(--ink); }
pre .c { color: var(--dim); } /* comment */
pre .k { color: var(--pink); } /* key / verb */
pre .s { color: var(--cyan); } /* string / value */
pre .y { color: var(--yellow); }
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 13.5px; }
th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; vertical-align: top; }
th { color: var(--cyan); background: var(--panel); }
.toc {
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
padding: 16px 22px; columns: 2; column-gap: 40px; font-size: 14px;
}
.toc a { text-decoration: none; display: block; padding: 2px 0; }
.toc a:hover { color: var(--pink); }
.note {
border-left: 3px solid var(--cyan); background: var(--panel);
padding: 10px 14px; border-radius: 0 8px 8px 0; margin: 14px 0;
}
.warn { border-left-color: var(--pink); }
.swatch {
display: inline-block; width: 30px; height: 18px; border-radius: 3px;
border: 1px solid #000; vertical-align: middle;
}
.top { position: fixed; right: 18px; bottom: 18px; background: var(--pink); color: #0b0d11;
padding: 8px 12px; border-radius: 8px; text-decoration: none; font-weight: bold; }
.pill { background:#1e232b; border:1px solid var(--border); border-radius:20px; padding:1px 10px; font-size:12px; color:var(--cyan); }
@media (max-width: 700px) { .toc { columns: 1; } }
</style>
</head>
<body>
<main>
<h1>THE MRPGI BOOK</h1>
<p class="subtitle">Monster Robot Party Game Interpreter — the complete user &amp; developer manual.<br>
Play it · paint it · write worlds as JSON · drive it from terminals, Python and AIs · read the engine's soul.</p>
<p><span class="pill">play now: <a href="./">monsterrobot.games/engine</a></span>
<span class="pill">engine version 0.1</span>
<span class="pill">this page is long on purpose</span></p>
<nav class="toc">
<a href="#what">1. What MRPGI is</a>
<a href="#quick">2. Three quick starts</a>
<a href="#playing">3. Playing</a>
<a href="#editor">4. The editor, deeply</a>
<a href="#cookbook">5. The authoring cookbook</a>
<a href="#dialogue">6. Dialogue trees</a>
<a href="#flags">7. Flags: real puzzles</a>
<a href="#art">8. The art pipeline</a>
<a href="#audio">9. Sound &amp; music</a>
<a href="#headless">10. Driving it with code</a>
<a href="#ai">11. AIs and MRPGI</a>
<a href="#prompts">12. Prompt patterns that work</a>
<a href="#web">13. The web build &amp; cloud saves</a>
<a href="#internals">14. Engine internals (for devs)</a>
<a href="#extend">15. Extending the engine</a>
<a href="#trouble">16. Troubleshooting</a>
<a href="#appendix">17. Appendices &amp; reference</a>
</nav>
<!-- ================================================================ -->
<h2 id="what">1. What MRPGI is</h2>
<p>In the 1980s Sierra built AGI, the engine behind <i>King's Quest</i>,
<i>Space Quest</i> and <i>Police Quest</i>. Its genius was the
<b>dual-buffer room</b>: every screen is drawn twice — once as the picture you
see, once as an invisible map of <b>meaning</b> (what's walkable, what's a
wall, what's near the camera). One trick gives you walk-behind depth,
collision, and doors, with zero per-object bookkeeping.</p>
<p>MRPGI keeps that soul and rebuilds everything around it, new-school:</p>
<ul>
<li><b>An in-engine paint app.</b> You draw rooms — look and meaning together — and walk them seconds later.</li>
<li><b>Games are folders of JSON + PNGs.</b> Hand-editable, git-friendly, zip-and-share-able, LLM-writable.</li>
<li><b>A forgiving verb-noun parser</b> plus an optional <b>local AI lane</b> that maps free English onto commands the game already allows — the AI can bend language, never the rules.</li>
<li><b>A fully headless core.</b> The same engine runs with no window: pipe JSON commands in a terminal, call it over HTTP from Python, or hand the controls to Claude over MCP — including rendering real PNG frames so the AI can <i>see</i> what it built.</li>
<li><b>Deterministic replays.</b> Fixed 20-cycles-per-second simulation; the same command script always produces byte-identical results. Your quest's win path can be a CI test.</li>
</ul>
<p>The engine is Rust. The window is <a href="https://macroquad.rs">macroquad</a>.
The web version you're probably reading this next to is the whole engine —
editor included — compiled to a ~1&nbsp;MB WASM file.</p>
<!-- ================================================================ -->
<h2 id="quick">2. Three quick starts</h2>
<h3>2a. Play in the browser (zero install)</h3>
<p>Open <a href="./">monsterrobot.games/engine</a>. You're in
<i>The DJ's Lost Fuse</i>. Walk with arrows or click. Type
<code>look</code>, <code>take fuse</code>, <code>talk scrap-bot</code>.
Press <kbd>Tab</kbd> to open the paint tool. Hit <b>SIGN IN</b> (top right) to
make an account so <b>Save world</b> keeps your edits between visits.</p>
<h3>2b. Run it native</h3>
<pre><code><span class="c"># requires Rust — https://rustup.rs</span>
git clone &lt;the repo&gt; &amp;&amp; cd MRPGI
cargo run -p mrpgi -- --game games/lost-fuse <span class="c"># the GUI</span>
cargo run -p mrpgi -- --sample <span class="c"># write + open the 3-room robot demo</span>
cargo run -p mrpgi -- --kit <span class="c"># the 4-room fantasy starter world</span></code></pre>
<h3>2c. Make your first game in ten minutes</h3>
<ol>
<li><code>mkdir -p games/mygame &amp;&amp; cargo run -p mrpgi -- --sample --game games/mygame</code> — a working skeleton.</li>
<li>Open it: <code>cargo run -p mrpgi -- --game games/mygame</code>. You boot into the editor.</li>
<li>Paint over room 0. Pick a colour, pick a <b>meaning</b> (floor = walkable, wall = blocked), draw. <kbd>Tab</kbd> to walk it. <kbd>Tab</kbd> back. <kbd>K</kbd> saves.</li>
<li>Press <kbd>0</kbd> (object tool), click somewhere, and fill in the inspector fields: name, look text, use text. Toggle <b>takeable</b> or <b>wins</b>.</li>
<li>Set an exit on the top strip (<b>E: 1</b>), press <kbd>&gt;</kbd>, paint room 1.</li>
<li>Edit <code>games/mygame/game.json</code> — name your game, write the intro line.</li>
</ol>
<p>That's a game. Everything past this point is depth.</p>
<!-- ================================================================ -->
<h2 id="playing">3. Playing</h2>
<h3>Moving</h3>
<p>Arrow keys walk in eight directions; click walks to the point
(greedy steering — it stops at walls rather than pathfinding around them, like
the originals). Walk off a screen edge that has an <b>exit</b> and you enter
the next room from the opposite side.</p>
<h3>The parser</h3>
<p>Type a sentence, press <kbd>Enter</kbd>. Filler words
(<i>the, a, at, please…</i>) are dropped, the first word maps to a canonical
verb, the rest becomes the noun and is matched loosely against object names
and their synonyms.</p>
<table>
<tr><th>Verb</th><th>Also answers to</th><th>Does</th></tr>
<tr><td><code>look</code></td><td>l, examine, x, inspect, check, read, view, see, study</td><td>describe the room, or one thing</td></tr>
<tr><td><code>take</code></td><td>get, grab, pick, snatch, steal, pocket, collect, nab</td><td>put a takeable object in your inventory</td></tr>
<tr><td><code>use</code></td><td>open, unlock, activate, push, pull, turn, operate</td><td>the puzzle verb — locks, levers, wins</td></tr>
<tr><td><code>talk</code></td><td>speak, ask, chat, greet, say</td><td>start a conversation</td></tr>
<tr><td><code>drop</code></td><td>discard, leave, put</td><td>remove from inventory</td></tr>
<tr><td><code>inventory</code></td><td>inv, i, items, bag</td><td>what you're carrying</td></tr>
<tr><td><code>help</code></td><td>?, commands, verbs</td><td>list the verbs</td></tr>
</table>
<p>Games can add their own synonyms (see <a href="#cookbook">the manifest</a>) —
<i>The DJ's Lost Fuse</i> accepts <code>fix amp</code> because its manifest
maps <i>fix</i> to <i>use</i>.</p>
<h3>The menu bar</h3>
<p>Along the top, like the classics: <b>Game</b> (New Game, Edit Mode, CRT,
Sound, Manual, About) and <b>Action</b> (Look Around, Inventory, Help). Slide
along the bar with a menu open to switch menus. Every item just injects the
same command a typed sentence would — menus, keyboards, terminals and AIs all
speak one language to the engine.</p>
<h3>Dialogue</h3>
<p><code>talk &lt;name&gt;</code> opens a conversation. Press
<kbd>1</kbd><kbd>6</kbd> to pick a reply, <kbd>Esc</kbd> to leave. The world
pauses while you talk. Some replies only appear once you've done something —
if an NPC seems cagey, come back after you've changed the world.</p>
<h3>The AI lane (native, optional)</h3>
<p>When the parser doesn't understand a sentence and a local LLM is
configured, the sentence is translated into <i>one allowed command</i> and
re-run through the real parser. You'll see the translation as
<code>(≈ open crate)</code>. It's the difference between
"I don't understand 'pry the lid off'" and the game just working — while your
puzzle logic stays fully deterministic. Setup in <a href="#appendix">the appendix</a>.</p>
<!-- ================================================================ -->
<h2 id="editor">4. The editor, deeply</h2>
<p>Press <kbd>Tab</kbd> anywhere. The left panel holds tools; the top strip
holds the world (room number, exits, music); the canvas is the room.</p>
<h3>The one idea that matters: LOOK and MEANING</h3>
<p>Every stroke paints up to two layers at once:</p>
<ul>
<li><b>LOOK</b> — one of 16 EGA colours. What players see.</li>
<li><b>MEANING</b> — what the pixels <i>are</i>: <b>floor</b> (walkable, auto-depth),
<b>wall</b> (blocked), <b>water</b> / <b>trigger</b> (reserved control lines),
or <b>none</b> (change the look only, leave behavior alone).</li>
</ul>
<p>Colour never implies meaning. A brown floor and a brown wall are identical
to the eye and opposites to the engine. Press <kbd>L</kbd> to cycle the view:
<b>look</b> (the picture), <b>both</b> (walls tinted red, water blue,
triggers green), <b>ctrl</b> (the raw meaning map). If you're confused about
why the robot won't walk somewhere, look at <b>both</b>.</p>
<div class="note">A fresh room is <b>entirely walkable</b>. You paint walls
where you <i>don't</i> want walking — the opposite of most tile editors.</div>
<h3>Tools</h3>
<table>
<tr><th>Key</th><th>Tool</th><th>Notes</th></tr>
<tr><td><kbd>1</kbd></td><td>brush</td><td>freehand; size with <kbd>,</kbd> <kbd>.</kbd></td></tr>
<tr><td><kbd>2</kbd></td><td>line</td><td>click points; <b>right-click or Enter</b> commits the polyline</td></tr>
<tr><td><kbd>3</kbd></td><td>rect</td><td>drag a filled rectangle — your workhorse for rooms</td></tr>
<tr><td><kbd>4</kbd></td><td>fill</td><td>flood-fill a sealed region; a huge-spill warning means your outline leaks</td></tr>
<tr><td><kbd>5</kbd></td><td>pick</td><td>eyedropper</td></tr>
<tr><td><kbd>6</kbd></td><td>erase</td><td>back to blank + walkable</td></tr>
<tr><td><kbd>7</kbd></td><td>image</td><td>stamp a sprite from the game's <code>sprites/</code>; <kbd>[</kbd> <kbd>]</kbd> choose, <kbd>R</kbd> reload — stamped pixels bake the current meaning underneath (a tree stamped with <b>wall</b> is solid)</td></tr>
<tr><td><kbd>8</kbd></td><td>oval</td><td>filled ellipse</td></tr>
<tr><td><kbd>9</kbd></td><td>spawn</td><td>where the robot appears (cyan circle)</td></tr>
<tr><td><kbd>0</kbd></td><td>object</td><td>live objects — the game-logic tool (below)</td></tr>
</table>
<p><kbd>Z</kbd> undoes (<b>just Z — no Ctrl</b>). <kbd>C</kbd> clears the room.
<kbd>K</kbd> saves, <kbd>O</kbd> reloads from disk. Rooms are stroke lists, so
undo works by replaying every stroke but the last — nothing is ever
destructively merged.</p>
<h3>The object tool &amp; the kit browser</h3>
<p>With <kbd>0</kbd> active, <kbd>[</kbd> <kbd>]</kbd> flips through 60+
ready-made archetypes (fantasy, sci-fi, monsters, nightlife, horror — all in
<code>kit/</code>). Click empty canvas to stamp one, fully written. Click an
existing object to select it and edit in the inspector: name, look text, use
text, needs-item, synonyms, takeable, wins, delete. <kbd>Enter</kbd> hops to
the next field, <kbd>Esc</kbd> stops typing.</p>
<h3>Rooms and exits</h3>
<p>The strip's <b>N E S W</b> buttons cycle each edge's destination room.
<kbd>-</kbd> <kbd>=</kbd> (or <b>&lt; &gt;</b>) move between rooms — moving
<b>saves the room you're leaving</b>. Going past your highest room number
creates a fresh blank one. In play, arriving at a walled edge falls back to
the room's spawn point, so you can't get bricked in.</p>
<!-- ================================================================ -->
<h2 id="cookbook">5. The authoring cookbook</h2>
<p>Everything the editor does is readable, diffable JSON. This section is the
full data reference — learn it and you can write entire games in a text
editor, or get an AI to write them for you (<a href="#prompts">§12</a>).</p>
<h3>A game is a folder</h3>
<pre><code>games/mygame/
game.json <span class="c"># the manifest — optional, every field has a default</span>
rooms/room0.json <span class="c"># one RoomDoc per room</span>
sprites/*.png <span class="c"># auto-quantized to EGA; filename = sprite name</span>
sfx/ music/ <span class="c"># optional audio overrides (§9)</span></code></pre>
<h3>game.json — the manifest</h3>
<pre><code>{
<span class="k">"name"</span>: <span class="s">"The DJ's Lost Fuse"</span>,
<span class="k">"start_room"</span>: 0,
<span class="k">"intro_text"</span>: <span class="s">"Night. A scrapyard. Somewhere above, a party has gone horribly silent. Type 'help'."</span>,
<span class="k">"verbs"</span>: [ { <span class="k">"name"</span>: <span class="s">"use"</span>, <span class="k">"words"</span>: [<span class="s">"plug"</span>, <span class="s">"fix"</span>, <span class="s">"repair"</span>] } ],
<span class="k">"flags"</span>: {}
}</code></pre>
<p><code>verbs</code> merges extra synonyms into the built-in table by
canonical name. The AI lane's whitelist is generated <i>from the same
table</i>, so the parser and the AI can never drift apart. <code>flags</code>
presets flag values for each new game.</p>
<h3>RoomDoc — one room</h3>
<pre><code>{
<span class="k">"strokes"</span>: [ ...paint operations, replayed in order on load... ],
<span class="k">"spawn"</span>: [30, 140],
<span class="k">"exits"</span>: [null, 1, null, null], <span class="c">// N, E, S, W → room number or null</span>
<span class="k">"objects"</span>: [ ...ObjDefs... ],
<span class="k">"music"</span>: 2 <span class="c">// 0 off · 1 calm · 2 eerie · 3 tense · 4 jolly · 5 spooky</span>
}</code></pre>
<h3>Strokes — the paint ops</h3>
<p>Externally tagged: each stroke is an object with exactly one key.
<code>meaning</code> is <code>"None" | "Floor" | "Wall" | "Water" | "Trigger"</code>;
<code>color</code> is an EGA index 015 (<a href="#appendix">palette table</a>).
The room grid is <b>160 × 168</b>; y 015 is traditionally the "sky/wall"
band and the playable floor usually starts around y 16.</p>
<pre><code>{ <span class="k">"Rect"</span>: { "x":0, "y":0, "w":160, "h":16, "color":0, "meaning":"Wall" } }
{ <span class="k">"Ellipse"</span>: { "x":22, "y":104, "w":34, "h":18, "color":6, "meaning":"None" } }
{ <span class="k">"Fill"</span>: { "x":80, "y":100, "color":7, "meaning":"Floor" } }
{ <span class="k">"Line"</span>: { "pts":[[0,80],[159,80]], "color":15 } }
{ <span class="k">"Brush"</span>: { "pts":[[50,60],[52,61],[54,62]], "color":4, "meaning":"None", "size":2 } }
{ <span class="k">"Image"</span>: { "name":"tree", "x":60, "y":80, "meaning":"Wall" } }</code></pre>
<h4>The standard walled room (copy this)</h4>
<pre><code>[
{ "Rect": { "x":0, "y":0, "w":160, "h":168, "color":1, "meaning":"None" } }, <span class="c">// backdrop</span>
{ "Rect": { "x":6, "y":16, "w":148, "h":146, "color":8, "meaning":"Floor" } }, <span class="c">// floor</span>
{ "Rect": { "x":0, "y":0, "w":160, "h":16, "color":0, "meaning":"Wall" } }, <span class="c">// N wall</span>
{ "Rect": { "x":0, "y":160, "w":160, "h":8, "color":0, "meaning":"Wall" } }, <span class="c">// S wall</span>
{ "Rect": { "x":0, "y":0, "w":6, "h":168, "color":0, "meaning":"Wall" } }, <span class="c">// W wall</span>
{ "Rect": { "x":154, "y":0, "w":6, "h":70, "color":0, "meaning":"Wall" } }, <span class="c">// E wall, top half…</span>
{ "Rect": { "x":154, "y":100, "w":6, "h":68, "color":0, "meaning":"Wall" } } <span class="c">// …leaving a door gap y 70100</span>
]</code></pre>
<p>For a door on the N/S edge, split those walls at x 6595 instead. The
exit trigger zone is the outer ~9 pixels of each edge, so gaps must reach the
border.</p>
<h3>ObjDef — every field</h3>
<pre><code>{
<span class="k">"name"</span>: <span class="s">"amp"</span>, <span class="c">// what the parser calls it (required)</span>
<span class="k">"sprite"</span>: <span class="s">"chest"</span>, <span class="c">// a PNG in sprites/ (required)</span>
<span class="k">"x"</span>: 122, <span class="k">"y"</span>: 132, <span class="c">// its FEET — bottom-centre, drives depth (required)</span>
<span class="k">"look"</span>: <span class="s">"The club's outdoor amp. Its fuse slot gapes empty and scorched."</span>,
<span class="k">"takeable"</span>: false,
<span class="k">"synonyms"</span>: <span class="s">"amplifier speaker stack"</span>, <span class="c">// space-separated extra nouns</span>
<span class="k">"use_text"</span>: <span class="s">"You slot the fuse home. The amp THUMPS alive."</span>,
<span class="k">"needs"</span>: <span class="s">"fuse"</span>, <span class="c">// inventory item required to use (else: "It's locked…")</span>
<span class="k">"wins"</span>: false, <span class="c">// a successful use ends the game with the victory banner</span>
<span class="k">"requires_flag"</span>: <span class="s">""</span>, <span class="c">// flag that must be true to use (else: "Nothing happens…")</span>
<span class="k">"sets_flag"</span>: <span class="s">"music_on"</span>, <span class="c">// flag set true on a successful use</span>
<span class="k">"dialogue"</span>: [] <span class="c">// §6 — non-empty makes it talkable</span>
}</code></pre>
<p>Everything except <code>name / sprite / x / y</code> defaults, so minimal
objects stay minimal. Objects never block movement — solidity is painted, not
attached to objects (stamp their footprint with the <b>wall</b> meaning if
they should block).</p>
<h4>The five load-bearing patterns</h4>
<table>
<tr><th>Pattern</th><th>Recipe</th></tr>
<tr><td>Key &amp; lock</td><td>takeable <code>key</code> anywhere + a door/chest with <code>"needs":"key"</code></td></tr>
<tr><td>Lever &amp; door</td><td>lever with <code>"sets_flag":"power_on"</code> + door with <code>"requires_flag":"power_on"</code></td></tr>
<tr><td>Earn the ending</td><td>the finale object stacks <code>needs</code> + <code>requires_flag</code> + <code>"wins":true</code></td></tr>
<tr><td>Gossip gate</td><td>a dialogue choice with <code>requires_flag</code> appears only after the deed; its <code>sets_flag</code> unlocks elsewhere (§6)</td></tr>
<tr><td>Fetch chain</td><td>A needs B, B needs C — each <code>use_text</code> should hint the next link</td></tr>
</table>
<h3>Case study: <i>The DJ's Lost Fuse</i></h3>
<p>The shipped example (<code>games/lost-fuse/</code>, ~200 lines of JSON)
uses every mechanic exactly once:</p>
<pre><code>take fuse <span class="c">an item → inventory (room 0, scrapyard)</span>
use amp <span class="c">needs: fuse → sets_flag music_on (room 1, back street)</span>
talk bouncer <span class="c">a choice hidden behind requires_flag music_on…</span>
"I'm with the band." <span class="c">→ sets_flag on_the_list</span>
use turntable <span class="c">requires_flag on_the_list → wins: true (room 2, the club)</span></code></pre>
<p>Play it before reading its JSON — then read its JSON. It's the fastest way
to internalize the whole model.</p>
<!-- ================================================================ -->
<h2 id="dialogue">6. Dialogue trees</h2>
<p>An object with a non-empty <code>dialogue</code> array is talkable. Each
entry is a <b>node</b>; node 0 is where <code>talk</code> starts. A node says
a line and offers numbered choices; each choice jumps to another node
(<code>goto</code>) or ends the conversation (<code>goto: -1</code>). A node
with no visible choices is a terminal line.</p>
<p>The bouncer from <i>Lost Fuse</i>, in full — a real, working tree:</p>
<pre><code><span class="k">"dialogue"</span>: [
{ <span class="k">"says"</span>: <span class="s">"List. Name. No name, no party."</span>,
<span class="k">"choices"</span>: [
{ <span class="k">"text"</span>: <span class="s">"C'mon, one dance?"</span>, <span class="k">"goto"</span>: 1 },
{ <span class="k">"text"</span>: <span class="s">"Hear that bass? I fixed your amp. I'm with the band."</span>,
<span class="k">"goto"</span>: 2,
<span class="k">"requires_flag"</span>: <span class="s">"music_on"</span>, <span class="c">// hidden until the amp works</span>
<span class="k">"sets_flag"</span>: <span class="s">"on_the_list"</span> }, <span class="c">// saying it unlocks the finale</span>
{ <span class="k">"text"</span>: <span class="s">"Fine."</span>, <span class="k">"goto"</span>: -1 }
] },
{ <span class="k">"says"</span>: <span class="s">"No list, no dance. The list is life."</span>,
<span class="k">"choices"</span>: [ { <span class="k">"text"</span>: <span class="s">"Harsh."</span>, <span class="k">"goto"</span>: -1 } ] },
{ <span class="k">"says"</span>: <span class="s">"...That WAS you? Respect, fixer. You're on the list."</span>,
<span class="k">"choices"</span>: [ { <span class="k">"text"</span>: <span class="s">"Let's go."</span>, <span class="k">"goto"</span>: -1 } ] }
]</code></pre>
<p>Choices gated by <code>requires_flag</code> are <b>invisible</b> until the
flag is set — the numbers the player sees always match what they can pick.
This is how NPCs "notice" what you've done. Design note: give gated choices a
line that makes the causality delicious (<i>"Hear that bass?"</i> only exists
while bass is audible).</p>
<div class="note">Writing tip: nodes are cheap. Give wrong turns a little
personality ("The list is life.") — dead ends carry more character-per-byte
than the golden path.</div>
<!-- ================================================================ -->
<h2 id="flags">7. Flags: real puzzles</h2>
<p><b>Flags</b> are named booleans living for one play session (the manifest
can preset them). Three things touch them:</p>
<ul>
<li>an object's successful <code>use</code><code>sets_flag</code></li>
<li>a picked dialogue choice → <code>sets_flag</code></li>
<li>gates: object <code>requires_flag</code> (blocks <code>use</code> with
<i>"Nothing happens. Something else must come first."</i>) and choice
<code>requires_flag</code> (hides the choice)</li>
</ul>
<p>Combined with <code>needs</code> (items), you can build arbitrarily long
chains: item → machine → flag → gossip → flag → finale. State lives in the
engine, not in your prose — you never have to trust the player read
anything.</p>
<p>Choose flag names like <code>music_on</code>, <code>met_wizard</code>,
<code>bridge_down</code> — sentences you can read in JSON six months later.
Headless surfaces can set or inspect any flag
(<code>{"cmd":"set_flag",…}</code>, <code>{"cmd":"query"}</code>), which makes
puzzle chains testable in CI (§10).</p>
<!-- ================================================================ -->
<h2 id="art">8. The art pipeline</h2>
<p>MRPGI never blits full-colour images over the game. Every PNG is
<b>quantized to the 16-colour EGA palette</b> on load (alpha becomes
transparency), so imported art depth-sorts, occludes and walks-behind exactly
like hand-painted pixels. Gradients get an optional Bayer dither for that
stippled retro look.</p>
<h3>Sprite spec</h3>
<ul>
<li><b>PNG with transparency</b>, drawn at game scale — props are ~1228&nbsp;px tall. No upscaling, draw 1:1.</li>
<li><b>Filename is the id</b>: <code>sprites/orc.png</code><code>"sprite":"orc"</code>. Missing sprites show as a magenta placeholder (the object still works).</li>
<li><b>Bottom-centre is the feet.</b> Stand the subject on the canvas's bottom edge; the feet drive depth.</li>
<li>EGA colours are ideal but optional — the engine snaps whatever you feed it. Palette files for Aseprite/Inkscape ship in <code>palettes/</code>.</li>
</ul>
<h3>AI-generated sprites</h3>
<p>Each theme pack ships <code>gemini_sprites.md</code> with per-sprite
prompts. The pattern that works:</p>
<pre><code><span class="c">STYLE (prepend to every prompt):</span>
16-color EGA pixel art sprite, hard pixels, no anti-aliasing, no gradients,
solid magenta (#FF00FF) background, subject centered, feet touching bottom edge.
<span class="c">SPRITE:</span>
a stout wooden treasure chest with iron bands, closed, 20x15 pixels
</code></pre>
<p>Generate, magenta-key the background to transparency, save into
<code>sprites/</code>, press <kbd>R</kbd> in the editor. The engine's
quantizer forgives models that don't quite hold the palette.</p>
<!-- ================================================================ -->
<h2 id="audio">9. Sound &amp; music</h2>
<p>By default everything is <b>synthesized square waves</b> — PCjr-beeper
soul, zero asset files. SFX cues: <i>blip</i> (command sent), <i>pickup</i>,
<i>confirm</i>, <i>error</i>, <i>door</i> (room change), <i>win</i> (fanfare).
Each room loops one of five ambient moods, set with the strip's <b></b>
button.</p>
<h3>Bring your own audio</h3>
<pre><code>games/mygame/sfx/door.ogg <span class="c"># overrides one cue: blip|pickup|confirm|error|door|win</span>
games/mygame/music/eerie.ogg <span class="c"># overrides a mood by name (calm|eerie|tense|jolly|spooky)…</span>
games/mygame/music/2.ogg <span class="c"># …or by number 1-5</span></code></pre>
<p>WAV/OGG/MP3 all load; <b>prefer OGG for music</b> — MP3 encoders pad
silence that clicks at the loop seam. Whatever you don't provide keeps its
chiptune default. <kbd>F2</kbd> mutes everything.</p>
<div class="note">In the engine's architecture, sound is an <i>event</i>
(<code>{"event":"audio","cue":"pickup"}</code>) — front-ends decide how to
play it. That's what makes a future Strudel live-coding sidecar a matter of
subscribing a browser page to the event stream, not an engine rewrite.</div>
<!-- ================================================================ -->
<h2 id="headless">10. Driving it with code</h2>
<p>The engine core is fully headless: a game state driven by JSON
<b>Commands</b> in and <b>Events</b> out. The GUI is just one client. The
<code>mrpgi-headless</code> binary exposes the same bus four more ways.</p>
<pre><code>cargo build -p mrpgi-core <span class="c"># builds target/debug/mrpgi-headless</span>
mrpgi-headless --help</code></pre>
<h3>10a. JSONL stdio — the substrate</h3>
<p>One JSON command per line in, events stream out. A real session against
<i>Lost Fuse</i> — these are actual engine outputs:</p>
<pre><code>$ mrpgi-headless --game games/lost-fuse
<span class="s">{"event":"room_changed","room":0,"music":2}</span>
<span class="s">{"event":"transcript","line":"Night. A scrapyard. Somewhere above, a party has gone horribly silent. Type 'help'."}</span>
<span class="k">{"cmd":"parse","text":"take fuse"}</span>
<span class="s">{"event":"transcript","line":"&gt; take fuse"}</span>
<span class="s">{"event":"transcript","line":"You take the fuse."}</span>
<span class="s">{"event":"inventory_changed","items":["fuse"]}</span>
<span class="s">{"event":"audio","cue":"pickup"}</span>
<span class="k">{"cmd":"walk_to","x":156,"y":85}</span>
<span class="s">{"event":"transcript","line":"You step into room 1."}</span>
<span class="s">{"event":"audio","cue":"door"}</span>
<span class="s">{"event":"room_changed","room":1,"music":3}</span>
<span class="s">{"event":"ego_moved","x":18,"y":84,"arrived":true}</span>
<span class="k">{"cmd":"query"}</span>
<span class="s">{"event":"state","state":{"game":"The DJ's Lost Fuse","room":1,"ego":[18,84,0],
"inventory":["fuse"],"won":false,"flags":{},"exits":[2,null,null,0],
"objects":[{"name":"amp","x":122,"y":132,"takeable":false,"has_dialogue":false},
{"name":"bouncer","x":80,"y":108,"takeable":false,"has_dialogue":true}], …}}</span></code></pre>
<p>The engine only advances when told: <code>{"cmd":"tick","n":20}</code> runs
one second of fixed game cycles; <code>walk_to</code> ticks until arrival or
blocked. The full command list is in <a href="#appendix">the appendix</a>.</p>
<h3>10b. Script replay — your quest as a CI test</h3>
<pre><code>mrpgi-headless --script wintest.jsonl --game games/mygame &gt; run1.log
mrpgi-headless --script wintest.jsonl --game games/mygame &gt; run2.log
diff run1.log run2.log <span class="c"># byte-identical, every time</span>
grep -q '"event":"won"' run1.log &amp;&amp; echo "quest still winnable"</code></pre>
<p>Under <code>--script</code> the AI lane defaults off, so replays are fully
deterministic. Keep a win-path script next to your game; run it after every
edit; never ship an unwinnable puzzle again.</p>
<h3>10c. Render with no window</h3>
<pre><code>mrpgi-headless --render-room games/mygame/rooms/room3.json out.png --game games/mygame</code></pre>
<p>Bakes any RoomDoc to a PNG — props, depth and all. The GUI equivalent for
whole-frame ground truth is <code>mrpgi --shot out.png</code>, which saves
the actual GPU backbuffer.</p>
<h3>10d. HTTP + Python</h3>
<pre><code>mrpgi-headless --serve 127.0.0.1:7777 --game games/mygame</code></pre>
<table>
<tr><th>Route</th><th>Does</th></tr>
<tr><td><code>POST /command</code></td><td>one Command JSON → JSON array of Events</td></tr>
<tr><td><code>GET /state</code></td><td>full snapshot</td></tr>
<tr><td><code>GET /frame.png</code></td><td>the current frame</td></tr>
<tr><td><code>GET /events?since=N</code></td><td>event log from a cursor → <code>{"next":M,"events":[…]}</code></td></tr>
<tr><td><code>POST /rooms/{n}</code></td><td>body = RoomDoc (add <code>?persist</code> to write to disk)</td></tr>
</table>
<pre><code><span class="c"># Python needs nothing but requests:</span>
import requests, json
E = "http://127.0.0.1:7777"
requests.post(f"{E}/command", data=json.dumps({"cmd": "parse", "text": "look"}))
state = requests.get(f"{E}/state").json()
open("frame.png", "wb").write(requests.get(f"{E}/frame.png").content)</code></pre>
<!-- ================================================================ -->
<h2 id="ai">11. AIs and MRPGI</h2>
<p>This is the headline act: the engine is an <b>MCP server</b>, so Claude
(or any MCP client) can author <i>and</i> play games — and see real rendered
frames of its own work.</p>
<pre><code>claude mcp add mrpgi -- /path/to/target/debug/mrpgi-headless --mcp --game games/mygame</code></pre>
<h3>The tools Claude gets</h3>
<table>
<tr><th>Tool</th><th>Does</th></tr>
<tr><td><code>mrpgi_parse</code></td><td>type a player command</td></tr>
<tr><td><code>mrpgi_walk_to</code> / <code>mrpgi_tick</code> / <code>mrpgi_choose</code></td><td>move, advance time, answer dialogue</td></tr>
<tr><td><code>mrpgi_state</code></td><td>full snapshot: room, inventory, flags, visible objects, exits</td></tr>
<tr><td><code>mrpgi_render_frame</code></td><td><b>returns a real PNG image</b> — the model looks at the actual frame</td></tr>
<tr><td><code>mrpgi_upsert_room</code> / <code>mrpgi_upsert_object</code> / <code>mrpgi_paint_stroke</code></td><td>author: whole rooms, single objects, individual paint ops</td></tr>
<tr><td><code>mrpgi_save_room</code> / <code>mrpgi_new_game</code> / <code>mrpgi_load_game</code></td><td>persist, reset, switch games</td></tr>
<tr><td><code>mrpgi_command</code></td><td>raw bus escape hatch — anything the JSONL surface accepts</td></tr>
</table>
<p>Room upserts are <b>validated</b> before they land: a room with no
walkable spawn, or an exit pointing at a room that doesn't exist, is rejected
with an explanatory error event. Generated lore either loads or fails
loudly — it can't half-load.</p>
<h3>The loop that makes games from prompts</h3>
<ol>
<li><b>Author</b>: Claude upserts rooms/objects as JSON.</li>
<li><b>Look</b>: <code>mrpgi_render_frame</code> — does the room read visually? Fix strokes.</li>
<li><b>Playtest</b>: <code>mrpgi_parse</code> / <code>mrpgi_walk_to</code> through its own puzzle chain to the win.</li>
<li><b>Commit</b>: <code>mrpgi_save_room</code> writes JSON to disk. Human plays it in the GUI.</li>
</ol>
<!-- ================================================================ -->
<h2 id="prompts">12. Prompt patterns that work</h2>
<p>Real templates with real outputs. The formats are strict serde types, so
generated JSON either deserializes or fails loudly — which is exactly what
makes LLM authoring reliable here.</p>
<h3>12a. "Write me a room" (paste-into-any-chatbot version)</h3>
<pre><code>Here is a room from my MRPGI game as JSON: [paste rooms/room1.json]
Write ONE new room in exactly this format. It is a rooftop afterparty above
the club. Requirements:
- 160x168 grid; backdrop Rect first, then a Floor rect, then Wall rects with
a door gap in the SOUTH wall (split it at x 65-95), exits [null,null,2,null]
- night-sky backdrop (color 1), a moon (yellow Ellipse near the top),
one neon strip (color 13)
- two objects using ONLY these sprites: barrel, sign, villager
- one object should have a look text that rewards climbing all this way
- music: 1 (calm)
Reply with ONLY the JSON.</code></pre>
<p><b>Actual output</b> (this exact room ships in
<code>games/lost-fuse/expansion/roof.jsonl</code> and loads clean):</p>
<pre><code>{"strokes":[
{"Rect":{"x":0,"y":0,"w":160,"h":168,"color":1,"meaning":"None"}},
{"Rect":{"x":6,"y":16,"w":148,"h":146,"color":8,"meaning":"Floor"}},
{"Rect":{"x":0,"y":0,"w":160,"h":16,"color":0,"meaning":"Wall"}},
{"Rect":{"x":0,"y":160,"w":65,"h":8,"color":0,"meaning":"Wall"}},
{"Rect":{"x":95,"y":160,"w":65,"h":8,"color":0,"meaning":"Wall"}},
{"Rect":{"x":0,"y":0,"w":6,"h":168,"color":0,"meaning":"Wall"}},
{"Rect":{"x":154,"y":0,"w":6,"h":168,"color":0,"meaning":"Wall"}},
{"Ellipse":{"x":20,"y":2,"w":14,"h":10,"color":14,"meaning":"None"}},
{"Rect":{"x":30,"y":30,"w":100,"h":4,"color":13,"meaning":"None"}}],
"spawn":[80,144],
"exits":[null,null,2,null],
"objects":[
{"name":"punch","sprite":"barrel","x":40,"y":126,
"look":"Afterparty punch. It is fizzing in a way liquids should not.","synonyms":"drink barrel"},
{"name":"skyline","sprite":"sign","x":110,"y":60,
"look":"The whole neon city, humming below. Worth saving a party for.","synonyms":"city view"}],
"music":1}</code></pre>
<h3>12b. Drop that lore into a RUNNING game</h3>
<pre><code>$ mrpgi-headless --script games/lost-fuse/expansion/roof.jsonl --game games/lost-fuse
<span class="s">{"event":"room_upserted","room":3,"persisted":false}</span> <span class="c"># validated + live</span>
<span class="s">{"event":"edited","room":2,"strokes":13}</span> <span class="c"># room 2's north exit now → 3</span></code></pre>
<p>Same two operations over HTTP (<code>POST /rooms/3</code>) or MCP
(<code>mrpgi_upsert_room</code>) extend a session someone is playing
<i>right now</i>.</p>
<h3>12c. Prompts to give Claude when the MCP server is connected</h3>
<pre><code><span class="c"># the full authoring loop, one sentence:</span>
"Render the current frame, then build a 4-room haunted-lighthouse game:
key &amp; lock, one flag chain, one NPC whose gated dialogue choice unlocks the
finale. Playtest it to the win yourself, render each room so I can see them,
then save all rooms to disk."
<span class="c"># incremental world-building:</span>
"Look at room 2. Add a locked cellar door in the south wall, a new room 5
behind it, and hide the key somewhere that requires talking to the barkeep."
<span class="c"># QA pass:</span>
"Play my game blind from the start. Every time text confuses you or a verb
fails, note it. Win it, then give me the friction list sorted by severity."
<span class="c"># regression testing:</span>
"Play to the win, then write the exact command sequence as a JSONL script I
can replay with --script in CI."</code></pre>
<h3>12d. Constraints that raise output quality</h3>
<ul>
<li><b>Name the sprites that exist.</b> Models invent sprite names otherwise; missing sprites render magenta.</li>
<li><b>Name the flags that exist</b> (or say "invent flags, prefix them <code>lh_</code>").</li>
<li><b>Ask for the walled-room skeleton</b> (§5) explicitly — models that improvise geometry forget door gaps.</li>
<li><b>One room per reply.</b> Whole games in one shot degrade; the upsert-validate loop is your friend.</li>
<li><b>"Reply with ONLY the JSON"</b> — and pipe through <code>--render-room</code> to see it before you keep it.</li>
</ul>
<!-- ================================================================ -->
<h2 id="web">13. The web build &amp; cloud saves</h2>
<p>The page at <a href="./">/engine/</a> is the whole engine (game + editor)
compiled to WASM — about 1&nbsp;MB. Differences from native: the AI parser
lane is off (no local Ollama in a browser), disk saving is replaced by cloud
saves, and the embedded game is fixed at build time.</p>
<h3>Accounts</h3>
<p><b>SIGN IN</b> (top right) → pick any username and password → done. Your
password is stored only as an argon2 hash; your session is a cookie; your
world is a JSON document on the server. <b>Save world</b> snapshots the
entire live world (manifest + every room, including unsaved paint);
<b>Load world</b> swaps it back into the running engine. There's no password
reset yet — pick something you'll remember, or ask the admin to clear your
account.</p>
<h3>For the curious: how save works</h3>
<p>The page can't see the engine's memory, so the SAVE button asks the engine
(via a wasm export) to serialize itself; the engine calls back into page JS
with the JSON on its next frame; the page PUTs it to
<code>/engine/api/world</code>. LOAD reverses the trip. The whole world —
three rooms of <i>Lost Fuse</i> — is about 6.5&nbsp;KB.</p>
<!-- ================================================================ -->
<h2 id="internals">14. Engine internals (for devs)</h2>
<h3>Workspace map</h3>
<pre><code>mrpgi-core/ <span class="c"># the engine. ZERO window/audio deps — enforced by the crate boundary</span>
src/state.rs <span class="c"># GameState: apply(Command) → Vec&lt;Event&gt;, tick(dt); THE spine</span>
src/world.rs <span class="c"># RoomDoc/ObjDef/dialogue data model, World, game folders, WorldBundle</span>
src/parser.rs <span class="c"># verb table, verb-noun parser, dialogue state machine</span>
src/render.rs <span class="c"># headless compositor → RGBA / PNG</span>
src/framebuffer.rs <span class="c"># the dual buffers + band(y) depth table</span>
src/picture.rs <span class="c"># draw primitives: rects, lines, flood fill, stamps</span>
src/sprite.rs <span class="c"># the ego: movement, collision, walk cycle</span>
src/assets.rs <span class="c"># PNG → EGA quantizer, sprite folders</span>
src/audio.rs <span class="c"># square-wave synth + cue definitions (content, not output)</span>
src/ai.rs <span class="c"># the AI lane (worker thread, Ollama/OpenRouter)</span>
src/kit.rs <span class="c"># sample game + starter kit writers</span>
src/bin/mrpgi-headless/ <span class="c"># JSONL / --script / --render-room / HTTP / MCP</span>
tests/engine.rs <span class="c"># parser goldens + full win-path replay determinism test</span>
mrpgi/ <span class="c"># the macroquad GUI — just another bus client</span>
mrpgi-vault/ <span class="c"># standalone: accounts + world saves for the web (argon2 + tiny_http)</span></code></pre>
<h3>The bus</h3>
<p>Everything is <code>GameState::apply(Command) → Vec&lt;Event&gt;</code>
plus <code>tick(dt)</code> for wall-clock front-ends. Commands and events are
serde enums (<code>{"cmd":"…"}</code> / <code>{"event":"…"}</code>), so every
control surface — GUI, stdio, HTTP, MCP, the Sierra menus themselves — is a
thin adapter. Options live at the edges; the core neither knows nor cares
who's connected.</p>
<h3>The AGI trick, precisely</h3>
<p>Rooms rasterize into two 160×168 byte buffers: <b>visual</b> (palette
indices) and <b>priority</b>. Priority values 415 are depth bands —
<code>band(y) = min(4 + y*12/168, 15)</code> — so lower on screen means
nearer the camera. Values 03 are control lines: 0 blocks movement, 1 water,
2 trigger, 3 reserved. The compositor sorts screen objects by the band under
their <i>feet</i> and paints each pixel only where
<code>object_priority ≥ room_priority</code>. That inequality is the entire
walk-behind system.</p>
<h3>Determinism</h3>
<p>The sim advances in fixed cycles (<code>CYCLE_DT = 1/20 s</code>). Real-time
front-ends accumulate frame time into cycles; scripted surfaces send
<code>tick</code> counts. No wall clock, no RNG in the core. The test suite
plays the sample game to the win twice and asserts the transcripts are
byte-identical — if you add nondeterminism, CI will out you.</p>
<h3>The vault API (web accounts)</h3>
<pre><code>POST /engine/api/signup {"user","pass"} <span class="c"># 2-20 chars a-z 0-9 _ - ; argon2id hash at rest</span>
POST /engine/api/login {"user","pass"} <span class="c"># session cookie, 30 days, HttpOnly</span>
POST /engine/api/logout
GET /engine/api/me <span class="c"># {"user":"matt"} or 401</span>
GET /engine/api/world <span class="c"># your saved WorldBundle</span>
PUT /engine/api/world <span class="c"># save (≤ 4 MB, must parse as JSON)</span></code></pre>
<p>Storage is JSON files on disk; sessions are in-memory (a service restart
signs everyone out, harmlessly). A <b>WorldBundle</b> is
<code>{"manifest":…, "rooms":{"0":RoomDoc,…}}</code> — the same single-file
world format used by save/load everywhere.</p>
<!-- ================================================================ -->
<h2 id="extend">15. Extending the engine</h2>
<h3>Add a verb (data only)</h3>
<p>New <i>synonyms</i> are pure manifest data (§5). New verb <i>behaviors</i>
are one arm in <code>parser.rs::run_command</code>'s match — read the
<code>"use"</code> arm as the template; it shows item checks, flag gates and
win handling in ~20 lines.</p>
<h3>Add a Command (worked example)</h3>
<ol>
<li>Add a variant in <code>state.rs</code>: <code>Teleport { x: i32, y: i32 }</code> — serde gives you <code>{"cmd":"teleport","x":80,"y":100}</code> on every surface for free.</li>
<li>Handle it in <code>GameState::apply</code>: clamp, set <code>self.ego.x/y</code>, return an <code>EgoMoved</code> event.</li>
<li>That's it. JSONL, HTTP and <code>mrpgi_command</code> over MCP can all teleport now. Add a dedicated MCP tool in <code>bin/mrpgi-headless/mcp.rs</code> only if you want a nicer schema.</li>
</ol>
<h3>Ideas with prepared landing zones</h3>
<ul>
<li><b>Water &amp; triggers</b>: control lines 12 are already painted and saved; <code>step_cycle</code> is where they'd bite.</li>
<li><b>Strudel music</b>: subscribe a browser page to <code>GET /events</code> and translate <code>music</code>/<code>audio</code> events into live-coded patterns. The event stream already exists.</li>
<li><b>Pathfinding</b>: <code>sprite.rs</code> steering is deliberately naive; A* over the priority buffer is a contained swap.</li>
<li><b>Directional ego views</b>: <code>View</code> already models loops-of-cels; the robot just only has one loop.</li>
</ul>
<!-- ================================================================ -->
<h2 id="trouble">16. Troubleshooting</h2>
<table>
<tr><th>Symptom</th><th>Cause / fix</th></tr>
<tr><td>Objects are magenta squares</td><td>that sprite PNG doesn't exist in the game's <code>sprites/</code> — make it (§8); the object still works</td></tr>
<tr><td>Fill flooded the whole screen</td><td>your outline has a gap; <kbd>Z</kbd> and seal it</td></tr>
<tr><td>Robot won't walk somewhere obvious</td><td>press <kbd>L</kbd><b>both</b>; you probably painted <b>wall</b> meaning under a floor-looking colour</td></tr>
<tr><td>Walked into a room and got stuck</td><td>the arrival edge is walled; the engine falls back to the room's spawn — set the spawn sensibly (<kbd>9</kbd>)</td></tr>
<tr><td>Exit won't trigger</td><td>door gaps must reach the screen border; the trigger zone is the outer ~9 px</td></tr>
<tr><td>AI lane does nothing</td><td>check the status bar's <code>ai:</code> readout; Ollama running? <code>MRPGI_AI_MODEL</code> matches <code>ollama list</code>? It only fires on <i>unknown</i> verbs</td></tr>
<tr><td>No sound on web until you click</td><td>browsers require a user gesture before audio — touch anything</td></tr>
<tr><td>Web save button does nothing</td><td>sign in first; the SAVE/LOAD buttons live in the account panel</td></tr>
<tr><td>Typed a letter, a tool changed</td><td>you're in the editor and no text field is focused — tool keys are single letters; in Play, letters go to the command line</td></tr>
</table>
<!-- ================================================================ -->
<h2 id="appendix">17. Appendices &amp; reference</h2>
<h3>A. The EGA palette</h3>
<table>
<tr><th>#</th><th></th><th>Name</th><th>#</th><th></th><th>Name</th></tr>
<tr><td>0</td><td><span class="swatch" style="background:#000"></span></td><td>black</td>
<td>8</td><td><span class="swatch" style="background:#555"></span></td><td>dark gray</td></tr>
<tr><td>1</td><td><span class="swatch" style="background:#00A"></span></td><td>blue</td>
<td>9</td><td><span class="swatch" style="background:#55F"></span></td><td>light blue</td></tr>
<tr><td>2</td><td><span class="swatch" style="background:#0A0"></span></td><td>green</td>
<td>10</td><td><span class="swatch" style="background:#5F5"></span></td><td>light green</td></tr>
<tr><td>3</td><td><span class="swatch" style="background:#0AA"></span></td><td>cyan</td>
<td>11</td><td><span class="swatch" style="background:#5FF"></span></td><td>light cyan</td></tr>
<tr><td>4</td><td><span class="swatch" style="background:#A00"></span></td><td>red</td>
<td>12</td><td><span class="swatch" style="background:#F55"></span></td><td>light red</td></tr>
<tr><td>5</td><td><span class="swatch" style="background:#A0A"></span></td><td>magenta</td>
<td>13</td><td><span class="swatch" style="background:#F5F"></span></td><td>light magenta</td></tr>
<tr><td>6</td><td><span class="swatch" style="background:#A50"></span></td><td>brown</td>
<td>14</td><td><span class="swatch" style="background:#FF5"></span></td><td>yellow</td></tr>
<tr><td>7</td><td><span class="swatch" style="background:#AAA"></span></td><td>light gray</td>
<td>15</td><td><span class="swatch" style="background:#FFF"></span></td><td>white</td></tr>
</table>
<h3>B. Command reference (every surface)</h3>
<pre><code><span class="c">// playing</span>
{"cmd":"parse","text":"take key"} {"cmd":"walk","dir":3} <span class="c">// 0 stop, 1 N … 8 NW</span>
{"cmd":"move_to","x":80,"y":120} {"cmd":"walk_to","x":80,"y":120}
{"cmd":"choose","n":1} {"cmd":"end_dialogue"}
{"cmd":"tick","n":20} {"cmd":"new_game","room":null}
{"cmd":"goto_room","n":2} {"cmd":"query"}
{"cmd":"set_flag","name":"x","value":true} {"cmd":"set_var","name":"x","value":1}
<span class="c">// authoring (the same bus the editor uses)</span>
{"cmd":"paint_stroke","stroke":{…}} {"cmd":"undo"} {"cmd":"clear_strokes"}
{"cmd":"set_spawn","x":80,"y":140} {"cmd":"set_exit","dir":1,"target":2}
{"cmd":"set_music","mood":4} {"cmd":"reload_sprites"}
{"cmd":"upsert_object","room":null,"obj":{…}} {"cmd":"delete_object","room":null,"name":"x"}
{"cmd":"upsert_room","n":7,"doc":{…},"persist":true}
{"cmd":"save_room","n":null} {"cmd":"load_game","dir":"games/other"}</code></pre>
<h3>C. Event reference</h3>
<pre><code>transcript{line} room_changed{room,music} inventory_changed{items}
dialogue_open{lines} dialogue_closed won
audio{cue} music{mood} ego_moved{x,y,arrived}
state{state} edited{room,strokes} room_upserted{room,persisted}
room_saved{room,path} game_loaded{name,room} error{message}</code></pre>
<h3>D. The AI lane (native)</h3>
<pre><code>MRPGI_AI=ollama <span class="c"># ollama (default) | openrouter | off</span>
MRPGI_AI_MODEL=gemma3:4b <span class="c"># your local tag — check `ollama list`</span>
MRPGI_AI_URL=… <span class="c"># endpoint override</span>
OPENROUTER_API_KEY=… <span class="c"># for openrouter</span></code></pre>
<p>The prompt sent to the model contains only the verb whitelist (generated
from the live verb table) and the visible object names. It may answer with
one short command; that command then runs through the normal deterministic
parser. The model cannot invent items, rooms, or outcomes.</p>
<h3>E. Keyboard reference card</h3>
<p><b>Global:</b> <kbd>Tab</kbd> edit⇄play · <kbd>F1</kbd> CRT · <kbd>F2</kbd> mute · <kbd>Esc</kbd> unfocus/leave-chat/quit<br>
<b>Play:</b> arrows/click walk · type + <kbd>Enter</kbd> · <kbd>1</kbd><kbd>6</kbd> dialogue · top menus<br>
<b>Edit:</b> <kbd>1</kbd><kbd>0</kbd> tools · <kbd>L</kbd> view · <kbd>[</kbd><kbd>]</kbd> sprite/kit · <kbd>,</kbd><kbd>.</kbd> brush · <kbd>-</kbd><kbd>=</kbd> rooms · <kbd>Z</kbd> undo · <kbd>K</kbd> save · <kbd>O</kbd> load · <kbd>C</kbd> clear · <kbd>R</kbd> reload sprites</p>
<p style="margin-top:60px; color:var(--dim)">MRPGI — Monster Robot Party Game Interpreter.
Built from "let's reimagine Sierra's AGI" into an engine your terminal, your
friends and your AIs can all play. Now go make something weird. 🤖</p>
</main>
<a class="top" href="#">↑ top</a>
</body>
</html>