- MANUAL.md brought up to the workspace era: game folders + manifest, flags & gated puzzles, hidden dialogue choices, audio file overrides, a Programmatic control section (CLI/HTTP/MCP/Python), and the new two-crate architecture map. - games/lost-fuse: three rooms that demonstrate every mechanic in one chain — item, needs-lock, object sets_flag, a dialogue choice hidden behind requires_flag, dialogue sets_flag, and a flag-locked wins finale. Verified winnable headlessly, gate included. - games/lost-fuse/expansion/roof.jsonl: a live lore drop that upserts a fourth room into a running game and rewires an exit — the "drop JSON lore in" workflow, runnable as a one-liner. - games/README.md: the guided tour — mechanic-to-JSON map, the live expansion demo, and how to prompt an LLM (or point Claude's MCP at the engine) to write valid lore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
649 lines
27 KiB
Markdown
649 lines
27 KiB
Markdown
# MRPGI — The Complete Manual
|
||
|
||
**Monster Robot Party Game Interpreter** — a new-school, Apple-Silicon-native
|
||
reimagining of Sierra's 1980s AGI adventure engine, written in Rust + macroquad.
|
||
You paint rooms, drop in objects, write puzzles, and walk through them — with a
|
||
text parser, optional local-AI understanding, branching dialogue, chiptune sound,
|
||
and ambient music. No external level files, no build step beyond `cargo`.
|
||
|
||
This manual is exhaustive. Jump around with the table of contents.
|
||
|
||
---
|
||
|
||
## Table of contents
|
||
1. [What MRPGI is](#1-what-mrpgi-is)
|
||
2. [Install & requirements](#2-install--requirements)
|
||
3. [Running it](#3-running-it)
|
||
4. [The two modes](#4-the-two-modes)
|
||
5. [Core concepts (read this)](#5-core-concepts-read-this)
|
||
6. [The editor — full reference](#6-the-editor--full-reference)
|
||
7. [Keyboard cheat sheet](#7-keyboard-cheat-sheet)
|
||
8. [Building a world (rooms & exits)](#8-building-a-world-rooms--exits)
|
||
9. [Objects & game logic](#9-objects--game-logic)
|
||
10. [The parser](#10-the-parser)
|
||
11. [The AI parser lane](#11-the-ai-parser-lane)
|
||
12. [Dialogue trees](#12-dialogue-trees)
|
||
13. [Sound & music](#13-sound--music)
|
||
14. [The art / sprite pipeline](#14-the-art--sprite-pipeline)
|
||
15. [The kit & theme packs](#15-the-kit--theme-packs)
|
||
16. [Games as folders & the file formats](#16-games-as-folders--the-file-formats)
|
||
17. [Programmatic control (CLI · HTTP · MCP · Python)](#17-programmatic-control-cli--http--mcp--python)
|
||
18. [Project layout & architecture](#18-project-layout--architecture)
|
||
19. [Troubleshooting](#19-troubleshooting)
|
||
20. [Roadmap & known gaps](#20-roadmap--known-gaps)
|
||
|
||
---
|
||
|
||
## 1. What MRPGI is
|
||
|
||
AGI was the engine behind *King's Quest*, *Space Quest*, and *Police Quest*. Its
|
||
genius was a **dual-buffer** model: every room is drawn into a *visual* screen
|
||
(what you see) **and** a hidden *priority* screen (depth + walkability). MRPGI
|
||
keeps that soul and modernizes everything around it:
|
||
|
||
- **In-engine room editor** — paint walls, floors, and walkability by hand.
|
||
- **Sprite pipeline** — drop PNGs in a folder; they're auto-quantized to EGA.
|
||
- **Multi-room worlds** with edge exits.
|
||
- **Objects** with descriptions, locks, keys, and win conditions — no code.
|
||
- **A forgiving parser** + an **optional local LLM** that understands plain English.
|
||
- **Branching dialogue**, **chiptune SFX**, and **ambient room music**.
|
||
- **Flags & gated puzzles** — multi-step quests authored as plain JSON.
|
||
- **A content kit**: 61 click-to-place archetypes across fantasy, sci-fi,
|
||
monsters, "saucy" nightlife, and classic horror.
|
||
- **A fully headless core** — the same engine is drivable from the command
|
||
line, HTTP, Python, or an AI over MCP, with no window (§17).
|
||
|
||
Everything saves to small, human-readable JSON: **a game is a folder** you can
|
||
zip, share, or have an LLM write. The engine builds with `cargo` into two
|
||
binaries: the GUI (`mrpgi`) and the headless multi-tool (`mrpgi-headless`).
|
||
|
||
---
|
||
|
||
## 2. Install & requirements
|
||
|
||
**Required:**
|
||
- **Rust** (stable, 1.85+). Install from <https://rustup.rs> or Homebrew.
|
||
- That's it for building — macroquad pulls its own dependencies and renders
|
||
natively (Metal on macOS). No system libraries to install.
|
||
|
||
**Optional (for the AI parser):**
|
||
- **[Ollama](https://ollama.com)** running locally with a small instruct model
|
||
(e.g. a Gemma 4B). Free, offline, private. Or an **OpenRouter** API key.
|
||
|
||
First build downloads the crate tree and takes a minute or two; subsequent builds
|
||
are seconds.
|
||
|
||
---
|
||
|
||
## 3. Running it
|
||
|
||
```sh
|
||
cargo run -p mrpgi # build + run the GUI (dev)
|
||
cargo run -p mrpgi --release # optimized build (smoother), slower first compile
|
||
./run.sh # convenience launcher that points the AI lane at your model
|
||
```
|
||
|
||
### Launch flags
|
||
| Flag | Effect |
|
||
|------|--------|
|
||
| *(none)* | Loads the game in the current directory (`game.json` + `rooms/room0.json`) and opens in the **editor**. |
|
||
| `--game DIR` | Load a game folder instead of the current directory (see §16). |
|
||
| `--sample` | Writes a 3-room "escape the party" demo (+ `game.json`) then runs it (existing rooms backed up to `*.json.bak`). |
|
||
| `--kit` | Generates the fantasy starter kit + placeholder sprites, and installs a 4-room fantasy world (existing → `.bak`). |
|
||
|
||
```sh
|
||
cargo run -p mrpgi -- --kit # install + play the fantasy starter world
|
||
cargo run -p mrpgi -- --sample # install + play the robot escape demo
|
||
cargo run -p mrpgi -- --game games/lost-fuse # play the example game (§16)
|
||
```
|
||
|
||
The headless binary takes the same `--game` / `--sample` / `--kit` flags:
|
||
|
||
```sh
|
||
cargo run -p mrpgi-core --bin mrpgi-headless -- --sample --game games/demo
|
||
```
|
||
|
||
### AI environment variables
|
||
| Var | Default | Meaning |
|
||
|-----|---------|---------|
|
||
| `MRPGI_AI` | `ollama` | `ollama` · `openrouter` · `off` |
|
||
| `MRPGI_AI_MODEL` | `gemma3:4b` | the model tag (set to YOUR Ollama tag) |
|
||
| `MRPGI_AI_URL` | `http://localhost:11434/api/generate` | endpoint override |
|
||
| `OPENROUTER_API_KEY` | — | required when `MRPGI_AI=openrouter` |
|
||
|
||
```sh
|
||
MRPGI_AI_MODEL="your-gemma-tag" cargo run
|
||
MRPGI_AI=openrouter OPENROUTER_API_KEY=sk-... MRPGI_AI_MODEL=google/gemma-2-9b-it:free cargo run
|
||
MRPGI_AI=off cargo run # deterministic parser only
|
||
```
|
||
|
||
---
|
||
|
||
## 4. The two modes
|
||
|
||
MRPGI is always in one of two modes; **`Tab`** flips between them:
|
||
|
||
- **EDIT** — the painting/authoring mode. Left panel of tools, a canvas, a top
|
||
strip for world/room controls. The engine boots here.
|
||
- **PLAY** — you become the robot. Walk with arrows or click, type commands, talk
|
||
to NPCs. A bottom panel shows the transcript + command line.
|
||
|
||
`Esc` quits (in edit it first un-focuses a text field; in play it first ends a
|
||
conversation).
|
||
|
||
---
|
||
|
||
## 5. Core concepts (read this)
|
||
|
||
### The dual buffer — LOOK vs MEANING
|
||
Every pixel of a room has **two** independent properties:
|
||
|
||
- **LOOK** — its colour (one of 16 EGA colours). What the player sees.
|
||
- **MEANING** — what it *is* to the engine: walkable floor, a wall, water, a
|
||
trigger, or a depth band. Stored in a hidden "priority" buffer.
|
||
|
||
**Colour does not imply meaning.** A brown floor and a brown wall look identical
|
||
but behave differently. When you paint, you set a colour *and* (optionally) a
|
||
meaning tag; they're applied to the same region but stored separately. This is the
|
||
single most important idea in MRPGI.
|
||
|
||
### Meaning tags
|
||
| Tag | Effect on the priority buffer |
|
||
|-----|-------------------------------|
|
||
| **floor** | walkable, with automatic depth (lower on screen = drawn in front) |
|
||
| **wall** | blocked — the robot can't enter |
|
||
| **water** | reserved (special) |
|
||
| **trigger** | reserved (signal line for future logic) |
|
||
| **none** | paint colour only; leave walkability unchanged |
|
||
|
||
By default an empty room is **entirely walkable** — you paint *walls* where you
|
||
don't want walking.
|
||
|
||
### The picture
|
||
The play area is **160 × 168 logical pixels**, displayed at a **2:1 pixel ratio**
|
||
(each pixel is twice as wide as tall — classic AGI). Colours are the 16-colour
|
||
EGA palette (indices 0–15). A perfect circle drawn in art tools looks slightly
|
||
wide in-game; that's intentional retro flavour.
|
||
|
||
### EGA palette
|
||
`0` black · `1` blue · `2` green · `3` cyan · `4` red · `5` magenta · `6` brown ·
|
||
`7` light gray · `8` dark gray · `9` light blue · `10` light green ·
|
||
`11` light cyan · `12` light red · `13` light magenta · `14` yellow · `15` white.
|
||
|
||
### The ego
|
||
"Ego" is the AGI term for the player avatar (the little robot). Its position is
|
||
its **feet** (bottom-centre of its sprite), which is what determines its depth and
|
||
collision. In play it's blocked by `wall` pixels and walks off `exit` edges.
|
||
|
||
---
|
||
|
||
## 6. The editor — full reference
|
||
|
||
The editor screen: a **tool panel** on the left, the **canvas** on the right, and
|
||
a **world strip** across the top of the canvas.
|
||
|
||
### Tools (number keys, or click in the panel)
|
||
| Key | Tool | What it does |
|
||
|-----|------|--------------|
|
||
| `1` | **brush** | freehand paint with the current colour + meaning (size via `,` `.`) |
|
||
| `2` | **line** | click to add points; **right-click or Enter** finishes the polyline |
|
||
| `3` | **rect** | drag a filled rectangle |
|
||
| `4` | **fill** | flood-fill a sealed region (the bucket) — fast |
|
||
| `5` | **pick** | eyedropper; grab a colour off the canvas |
|
||
| `6` | **erase** | rub back to blank + walkable |
|
||
| `7` | **image** | stamp a sprite from `sprites/` (`[` `]` to choose, `R` to reload) |
|
||
| `8` | **oval** | drag a filled ellipse |
|
||
| `9` | **spawn** | click to set where the robot appears in play (cyan circle) |
|
||
| `0` | **object** | place a live object — see below |
|
||
|
||
### Colour & meaning
|
||
- **Palette** (left panel): click a swatch to set the current LOOK colour.
|
||
- **Meaning** buttons: floor / wall / water / trigger / none — the MEANING applied
|
||
by fills, rects, brush, etc.
|
||
|
||
### View modes (`L` cycles)
|
||
- **look** — the plain picture.
|
||
- **both** — picture + walls tinted red, water blue, triggers green (so you can
|
||
*see* what's walkable).
|
||
- **ctrl** — the raw meaning map (depth + control).
|
||
|
||
### The object tool & kit browser (`0`)
|
||
With the object tool active the panel shows **`kit [ ]: <name>`**. Press `[` `]`
|
||
to flip through 60+ ready-made archetypes (orc, console, cocktail, coffin…). Click
|
||
an empty spot to **stamp** the selected archetype, fully written. Cycle to
|
||
`(blank)` to drop a bare object named after the current sprite. Click an existing
|
||
object to **select** it; the inspector then shows editable fields:
|
||
|
||
- **name** — what the parser calls it.
|
||
- **look text** — response to `look`.
|
||
- **use text** — response to `use`/`open`.
|
||
- **needs item** — an item required to `use` it (empty = unlocked).
|
||
- **synonyms** — extra nouns it answers to (space-separated).
|
||
- **takeable / fixed** toggle, **wins** toggle, **delete**.
|
||
|
||
Click a field, type, `Enter` jumps to the next field, `Esc` finishes.
|
||
|
||
### The world strip (top)
|
||
- **room N** + `<` `>` — move between rooms (also `-` `=`). Switching auto-saves.
|
||
- **exits** `N E S W` — click to cycle each edge's target room number.
|
||
- **♪** — cycle the room's ambient music mood (off · calm · eerie · tense · jolly · spooky).
|
||
|
||
### Saving
|
||
- `K` saves the current room to `rooms/room<N>.json` (an editable list of strokes
|
||
+ objects + exits + spawn + music). `O` reloads it. `C` clears, `Z`/`Backspace`
|
||
undoes. The game auto-loads `rooms/room0.json` on startup.
|
||
|
||
---
|
||
|
||
## 7. Keyboard cheat sheet
|
||
|
||
**Global:** `Tab` edit ⇄ play · `Esc` quit (or un-focus / end conversation) ·
|
||
`F1` CRT scanlines · `F2` mute sound + music.
|
||
|
||
**Editor:** `1`–`0` tools · `L` view · `[` `]` choose sprite / kit archetype ·
|
||
`,` `.` brush size · `-` `=` prev / next room · `Z`/`Backspace` undo · `C` clear ·
|
||
`K` save · `O` load · `R` reload sprites + kit.
|
||
|
||
**Play:** arrows / click to walk · type a command + `Enter` · `1`–`6` choose a
|
||
dialogue reply · `Esc` leave a conversation.
|
||
|
||
---
|
||
|
||
## 8. Building a world (rooms & exits)
|
||
|
||
A room is one screen. A world is rooms linked by their edges.
|
||
|
||
1. Paint a room. Leave a **walkable gap** in a wall where you want a doorway.
|
||
2. On the strip, set that edge's **exit** to a room number (e.g. `E: 1`).
|
||
3. Press `>` to go to that room (going past the last room makes a new blank one),
|
||
paint it, and set a matching exit back.
|
||
4. In play, **walk off the edge with an exit** → you arrive in the target room,
|
||
entering from the opposite side.
|
||
|
||
Rooms are files: `rooms/room0.json`, `room1.json`, … Room 0 is where the game
|
||
starts. The **spawn** tool sets each room's entry point (used if the edge you
|
||
arrive at is walled).
|
||
|
||
---
|
||
|
||
## 9. Objects & game logic
|
||
|
||
Objects are sprites you can interact with. Each carries:
|
||
|
||
- a **name** + **synonyms** (what the parser matches),
|
||
- **look text** (shown on `look`),
|
||
- **use text** (shown on `use`/`open`),
|
||
- **needs** — an item that must be in your inventory to `use` it,
|
||
- **wins** — using it successfully ends the game with a victory banner,
|
||
- **takeable** — whether `take` adds it to your inventory,
|
||
- **requires_flag** — a flag that must be true before `use` works,
|
||
- **sets_flag** — a flag set true when `use` succeeds.
|
||
|
||
This is enough for the classic adventure loop **with no code**:
|
||
|
||
> Put a `key` (takeable) in one room and a `gate` in another with **needs = key**,
|
||
> **use text** = *"The gate grinds open…"*, **wins = on**. Now: `take key` →
|
||
> travel → `use gate` → **win**.
|
||
|
||
### Flags — multi-step quests
|
||
|
||
**Flags** are named booleans that live for one play session (a game's
|
||
`game.json` can preset them). Objects and dialogue choices read and write them:
|
||
|
||
> A `lever` with **sets_flag = power_on**. A `door` with **requires_flag =
|
||
> power_on**, **wins = on**. Now `use door` says *"Nothing happens. Something
|
||
> else must come first."* until you `use lever`.
|
||
|
||
Chain `needs` (items) with `requires_flag`/`sets_flag` (state) and gated
|
||
dialogue (§12) and you get real multi-step quests — all data, no code. The
|
||
example game `games/lost-fuse/` demonstrates the whole chain (§16).
|
||
|
||
Taking an object removes it from the room (runtime only — it returns on a fresh
|
||
play). Objects do not block movement (walls come from the painted meaning layer).
|
||
Currently `requires_flag`/`sets_flag` are JSON-authored (the editor inspector
|
||
shows the older fields only).
|
||
|
||
---
|
||
|
||
## 10. The parser
|
||
|
||
Type a sentence; it's lower-cased, filler words are dropped, the first word maps to
|
||
a canonical verb, and the rest is the noun (matched against object names +
|
||
synonyms, loosely).
|
||
|
||
**Verbs (and their synonyms):**
|
||
- **look** — look, l, examine, x, inspect, check, read, view, see, study
|
||
- **take** — take, get, grab, pick, snatch, steal, pocket, collect, nab
|
||
- **use** — use, open, unlock, activate, push, pull, turn, operate
|
||
- **drop** — drop, discard, leave, put
|
||
- **talk** — talk, speak, ask, chat, greet, say
|
||
- **inventory** — inventory, inv, i, items, bag
|
||
- **help** — help, ?, commands, verbs
|
||
|
||
**Ignored filler:** the, a, an, at, to, with, on, of, my, your, some, please, go, up.
|
||
|
||
`look` with no noun (or `look around`) lists what's in the room. Unknown verbs fall
|
||
through to the AI lane (if enabled) or print "I don't understand".
|
||
|
||
---
|
||
|
||
## 11. The AI parser lane
|
||
|
||
When the deterministic parser can't recognise your verb, MRPGI can hand the raw
|
||
sentence to a small LLM, which translates it into **one allowed command** — then
|
||
re-runs *that* through the normal logic. The AI bends the *language*; your game
|
||
still decides what's *possible*. It never invents items or outcomes, and it runs on
|
||
a background thread so the game never stalls. The chosen command is shown as
|
||
`(≈ open crate)`.
|
||
|
||
**Setup (local, recommended):** run Ollama with an instruct model and point MRPGI
|
||
at it via `MRPGI_AI_MODEL` (see §3). Default provider is `ollama`. If Ollama isn't
|
||
running or the model is missing, the lane stays silent and you get the normal
|
||
deterministic behaviour — nothing breaks.
|
||
|
||
**OpenRouter:** set `MRPGI_AI=openrouter`, `OPENROUTER_API_KEY`, and a
|
||
`MRPGI_AI_MODEL` like `google/gemma-2-9b-it:free`.
|
||
|
||
The bottom bar shows `ai: ollama/<model>` (or `off`).
|
||
|
||
---
|
||
|
||
## 12. Dialogue trees
|
||
|
||
An object can carry a **branching conversation**. In play, `talk <name>` starts it:
|
||
the NPC speaks and you press a **number key (1–6)** to choose a reply. Choices jump
|
||
to other lines; some end the chat. `Esc` leaves. Walking pauses while you talk.
|
||
|
||
Dialogue is authored as a `dialogue` array on the object (a visual builder is
|
||
planned). Each entry is a **node**; node 0 is the entry. See the inn barkeep in
|
||
`rooms/room3.json` for a working example:
|
||
|
||
```json
|
||
"dialogue": [
|
||
{ "says": "You're for the cave, aren't you?",
|
||
"choices": [
|
||
{ "text": "What's down there?", "goto": 1 },
|
||
{ "text": "Just passing through.", "goto": 3 }
|
||
] },
|
||
{ "says": "Orcs. Dark. A spellbook nobody should read aloud.",
|
||
"choices": [ { "text": "Thanks.", "goto": -1 } ] }
|
||
]
|
||
```
|
||
|
||
A choice's **`goto`** is the next node index, or **`-1`** to end. A node with no
|
||
choices is a terminal line (shown, then the conversation ends).
|
||
|
||
### Gated choices
|
||
|
||
Choices can read and write flags (§9), which makes conversations part of the
|
||
puzzle chain:
|
||
|
||
```json
|
||
{ "text": "Hear that? I'm with the band.", "goto": 2,
|
||
"requires_flag": "music_on", "sets_flag": "on_the_list" }
|
||
```
|
||
|
||
A choice with `requires_flag` is **hidden** until that flag is true — the
|
||
numbering the player sees always matches what they can pick. `sets_flag` fires
|
||
when the choice is chosen. So an NPC can refuse to say something until you've
|
||
done a thing, and telling them can unlock a door elsewhere.
|
||
|
||
---
|
||
|
||
## 13. Sound & music
|
||
|
||
By default all audio is **synthesized at runtime** (square waves → PCM, no
|
||
files), in the spirit of AGI's PCjr beeper — and any of it can be **overridden
|
||
per game with real audio files** (below).
|
||
|
||
**SFX (play mode):** a blip when you submit a command, a ding on `take`, a chime on
|
||
a successful `use`/`look`, a buzz when nothing matches, a whoosh on room change, a
|
||
4-note fanfare on **win**.
|
||
|
||
**Ambient music:** each room can loop one of five moods (calm / eerie / tense /
|
||
jolly / spooky), set with the **♪** strip button in the editor and saved per room.
|
||
It plays in play mode and switches as you move between rooms.
|
||
|
||
**`F2`** mutes music and SFX together. If a machine has no audio backend, everything
|
||
degrades to silence.
|
||
|
||
### Your own audio files
|
||
|
||
Drop files into the game folder and they replace the synth, cue for cue:
|
||
|
||
```
|
||
<game>/sfx/blip|pickup|confirm|error|door|win .wav|.ogg|.mp3
|
||
<game>/music/calm|eerie|tense|jolly|spooky .wav|.ogg|.mp3 (or 1.ogg … 5.ogg)
|
||
```
|
||
|
||
Anything you don't provide keeps its chiptune default. Prefer **OGG for
|
||
looping music** (MP3 encoders pad silence that clicks at the loop seam).
|
||
|
||
---
|
||
|
||
## 14. The art / sprite pipeline
|
||
|
||
Sprites live in `sprites/` as PNGs. On load, any image is **quantized to the EGA
|
||
palette** (with transparency from its alpha), so it depth-sorts and walks-behind
|
||
like native art. Replace any sprite and press **`R`** to reload.
|
||
|
||
**The sprite spec:**
|
||
- **PNG with transparency.** Transparent pixels are see-through.
|
||
- **Small, at game scale.** A prop is ~12–28 px tall; draw 1:1.
|
||
- **Filename = id.** lowercase, no spaces (`orc.png`). The object's `sprite` field
|
||
references this name. A missing sprite shows as a magenta placeholder.
|
||
- **Bottom-centre = feet.** Stand the thing on the bottom edge of the canvas.
|
||
- **EGA colours** ideal but not required — load `palettes/mrpgi-ega.gpl` into
|
||
Aseprite/Inkscape, or let the engine snap your colours on load.
|
||
|
||
**Aseprite:** Palette → Load Palette → `palettes/mrpgi-ega.gpl`; draw small;
|
||
export PNG to `sprites/`; press `R`.
|
||
|
||
**Inkscape:** set a tiny px page; Export PNG (transparent) to `sprites/`.
|
||
|
||
**AI (Gemini "Nano Banana" / Imagen):** each theme's `gemini_sprites.md` has a
|
||
shared STYLE line + per-sprite prompts. Generate on a solid magenta background, key
|
||
it out to transparency, save into `sprites/`. (See also `palettes/mrpgi-ega.gpl`
|
||
and `palettes/mrpgi-ega.hex`.)
|
||
|
||
---
|
||
|
||
## 15. The kit & theme packs
|
||
|
||
`cargo run -- --kit` generates the **fantasy starter kit** and installs a playable
|
||
4-room world (forest → cave → dungeon → inn, with a key/spellbook/altar quest).
|
||
|
||
The reusable library lives in `kit/`:
|
||
- **`kit/objects.json`** — 13 fantasy object archetypes.
|
||
- **`kit/locations/`** — prefab rooms (forest, cave, dungeon, inn).
|
||
- **`kit/ART_MANIFESTO.md`** — exact sprite specs to replace the placeholders.
|
||
- **`kit/themes/{scifi,monsters,saucy,horror}/`** — each with `objects.json`
|
||
(12 archetypes), a sample `locations/*.json`, and `gemini_sprites.md`.
|
||
|
||
**Use an object:** the kit browser (object tool, `[` `]`) stamps any of these by
|
||
clicking. Or copy a `{ … }` block from any `objects.json` into a room's `objects`
|
||
array. **Use a location:** `cp kit/themes/horror/locations/crypt.json rooms/room5.json`,
|
||
then wire its exits in the editor.
|
||
|
||
---
|
||
|
||
## 16. Games as folders & the file formats
|
||
|
||
**A game is a folder.** Zip it, share it, generate it with an LLM — the engine
|
||
loads it with `--game DIR` (both binaries):
|
||
|
||
```
|
||
games/mygame/
|
||
game.json the manifest (below) — optional, every field defaults
|
||
rooms/room0.json one RoomDoc per room; start room comes from the manifest
|
||
sprites/*.png auto-quantized to EGA; filename = sprite name
|
||
sfx/ music/ optional audio-file overrides (§13)
|
||
```
|
||
|
||
A bare directory with just `rooms/` is already a valid game — that's how
|
||
legacy projects keep loading.
|
||
|
||
**game.json (the manifest):**
|
||
```json
|
||
{
|
||
"name": "The DJ's Lost Fuse",
|
||
"start_room": 0,
|
||
"intro_text": "The party upstairs went quiet an hour ago. Type 'help'.",
|
||
"verbs": [ { "name": "use", "words": ["plug", "spin"] } ],
|
||
"flags": { "met_bouncer": false }
|
||
}
|
||
```
|
||
`verbs` merges extra synonyms into the built-in verb table (the AI lane's
|
||
whitelist is generated from the same table, so they can't drift). `flags` are
|
||
the starting flag values for each new game.
|
||
|
||
A complete worked example ships in **`games/lost-fuse/`** — three rooms
|
||
demonstrating every mechanic (items, locks, flags, gated dialogue, win), with
|
||
a guided tour in [`games/README.md`](games/README.md).
|
||
|
||
### Room file format (full schema)
|
||
|
||
A room is a `RoomDoc` serialized as JSON:
|
||
|
||
```json
|
||
{
|
||
"strokes": [ <Stroke>, ... ],
|
||
"spawn": [x, y],
|
||
"exits": [N, E, S, W], // each: a room number or null
|
||
"objects": [ <ObjDef>, ... ],
|
||
"music": 0 // 0=silence, 1..5 = mood
|
||
}
|
||
```
|
||
|
||
**Stroke** is an externally-tagged enum — exactly one key:
|
||
```json
|
||
{ "Rect": { "x":0,"y":0,"w":160,"h":168,"color":2,"meaning":"Floor" } }
|
||
{ "Line": { "pts":[[x,y],...], "color":15 } }
|
||
{ "Brush": { "pts":[[x,y],...], "color":6,"meaning":"Wall","size":2 } }
|
||
{ "Ellipse": { "x":..,"y":..,"w":..,"h":..,"color":..,"meaning":"None" } }
|
||
{ "Fill": { "x":..,"y":..,"color":..,"meaning":"Floor" } }
|
||
{ "Image": { "name":"crate","x":..,"y":..,"meaning":"Wall" } }
|
||
```
|
||
`meaning` is one of `"None"`, `"Floor"`, `"Wall"`, `"Water"`, `"Trigger"`.
|
||
`color` is an EGA index 0–15.
|
||
|
||
**ObjDef:**
|
||
```json
|
||
{
|
||
"name": "gate", "sprite": "door", "x": 80, "y": 120,
|
||
"look": "A heavy gate.", "takeable": false,
|
||
"synonyms": "door portcullis", "use_text": "It grinds open!",
|
||
"needs": "key", "wins": true,
|
||
"requires_flag": "power_on", "sets_flag": "gate_open",
|
||
"dialogue": [ <DlgNode>, ... ]
|
||
}
|
||
```
|
||
**DlgNode:** `{ "says": "...", "choices": [ { "text": "...", "goto": 1,
|
||
"requires_flag": "", "sets_flag": "" } ] }` — `goto` is a node index or `-1`
|
||
to end; the flag fields are optional (§12).
|
||
|
||
All fields except `name`/`sprite`/`x`/`y` default if omitted, so hand-written JSON
|
||
can be minimal. Strokes are *replayed* into the buffers on load, so rooms stay tiny
|
||
and re-editable.
|
||
|
||
---
|
||
|
||
## 17. Programmatic control (CLI · HTTP · MCP · Python)
|
||
|
||
The engine core is fully headless: a `GameState` driven by JSON `Command`s in
|
||
and `Event`s out. The GUI is just one consumer of that bus — the same bus is
|
||
exposed as:
|
||
|
||
- **JSONL stdio** — `mrpgi-headless --game DIR`; pipe
|
||
`{"cmd":"parse","text":"take key"}` lines in, events stream out.
|
||
- **Script replay** — `--script play.jsonl` replays a command log
|
||
deterministically (byte-identical transcripts; the CI golden-test workflow).
|
||
- **Headless render** — `--render-room rooms/room0.json out.png` bakes any
|
||
room to a PNG with no window.
|
||
- **HTTP** — `--serve 127.0.0.1:7777`: `POST /command`, `GET /state`,
|
||
`GET /frame.png`, `GET /events?since=N`, `POST /rooms/{n}`. Python drives it
|
||
with plain `requests`.
|
||
- **MCP** — `--mcp`: a Model Context Protocol server, so Claude (or any MCP
|
||
client) can both *play* the game and *author* it — including
|
||
`mrpgi_render_frame`, which returns a real PNG so the model sees its work:
|
||
`claude mcp add mrpgi -- /path/to/mrpgi-headless --mcp --game games/mygame`
|
||
|
||
Full command/event reference and authoring contract:
|
||
**[docs/CONTROL.md](docs/CONTROL.md)**.
|
||
|
||
---
|
||
|
||
## 18. Project layout & architecture
|
||
|
||
Two crates in one workspace:
|
||
|
||
```
|
||
mrpgi-core/ the headless engine (ZERO window/audio deps — enforced)
|
||
src/state.rs GameState + the Command/Event bus + fixed-step tick
|
||
src/world.rs RoomDoc/ObjDef/dialogue data model, World, game folders
|
||
src/parser.rs data-driven verb table, verb-noun parser, dialogue state
|
||
src/render.rs headless compositing → RGBA / PNG
|
||
src/framebuffer.rs visual + priority buffers, the band(y) depth table
|
||
src/picture.rs drawing primitives (lines, rects, flood-fill, stamps)
|
||
src/view.rs sprites as cels; ASCII-grid cel builder
|
||
src/sprite.rs the ego + props: movement, collision, walk-cycle
|
||
src/assets.rs PNG load + EGA quantize/dither; placeholder sprites
|
||
src/audio.rs chiptune synthesis + audio cues (content, not output)
|
||
src/ai.rs the AI parser lane (Ollama / OpenRouter, worker thread)
|
||
src/kit.rs sample game + starter kit writers
|
||
src/bin/mrpgi-headless/ the CLI/HTTP/MCP surfaces
|
||
tests/engine.rs parser goldens + a full win-path replay test
|
||
mrpgi/ the macroquad GUI (one consumer of the bus)
|
||
src/main.rs window, play loop over Commands/Events, HUD
|
||
src/editor.rs the paint tool (commits edits through the same bus)
|
||
src/sound.rs audio out: game-folder files override the synth
|
||
kit/ the archetype/theme library + art manifesto
|
||
games/ game folders (lost-fuse example lives here)
|
||
sprites/ rooms/ the legacy in-place game (still loads fine)
|
||
palettes/ the EGA palette for Aseprite/Inkscape (.gpl, .hex)
|
||
```
|
||
|
||
**Rendering:** rooms rasterize into a 160×168 visual buffer + a priority buffer.
|
||
Each frame, the ego and props are drawn back-to-front by depth; a sprite pixel is
|
||
only drawn where `sprite_priority ≥ scenery_priority` (the walk-behind trick). The
|
||
result is composited headlessly in the core; the GUI just uploads it to a GPU
|
||
texture. A fixed-step "cycle" (20/s) drives movement, decoupled from frame
|
||
rate — which is why scripted replays are bit-identical.
|
||
|
||
---
|
||
|
||
## 19. Troubleshooting
|
||
|
||
- **Objects are little magenta squares** → that sprite PNG doesn't exist yet in
|
||
`sprites/`. Make it (see §14) or it's just a placeholder; the object still works.
|
||
- **A fill flooded the whole screen** → your outline has a gap; the region wasn't
|
||
sealed. `Z` to undo and close the gap.
|
||
- **Robot stuck after a room transition** → the edge you arrived at is walled;
|
||
leave a walkable gap there (it otherwise falls back to the room's spawn).
|
||
- **AI does nothing** → check the bottom bar `ai:` readout; ensure Ollama is
|
||
running and `MRPGI_AI_MODEL` matches a pulled model (`ollama list`). It only
|
||
fires on *unknown verbs*.
|
||
- **No sound** → some machines lack an audio backend; MRPGI stays silent rather
|
||
than crashing. Check `F2` isn't muted.
|
||
- **Typing a letter flipped a mode** → in play, letters go to the command line;
|
||
use `Tab`/`F1`/`F2`/`Esc` (non-letters) for controls.
|
||
|
||
---
|
||
|
||
## 20. Roadmap & known gaps
|
||
|
||
- In-editor **dialogue-tree builder** and flag-field editing (both are
|
||
JSON-authored for now; the `dialogue`/`requires_flag`/`sets_flag` fields
|
||
work — the inspector just doesn't show them yet).
|
||
- The **kit-browser villager** has no built-in dialogue yet (the live inn one does).
|
||
- More theme packs; **title screen / world picker**; **save/load a playthrough**.
|
||
- Water/trigger control lines are reserved but not yet wired to behaviour.
|
||
- True-colour ("HD") background layer and polygon fill regions.
|
||
- **Strudel** live-coded music as a browser sidecar over the HTTP surface.
|
||
- Pathfinding for click-to-walk (today: greedy steering); cross-room NPC state.
|
||
|
||
---
|
||
|
||
*MRPGI — built from "let's reimagine AGI" into a complete, AI-assisted, audible
|
||
adventure-game maker. Have fun making weird little worlds.* 🤖🎸
|