The title screen / world picker now works in the browser, not just natively. `./web/build.sh <folder-of-game-folders>` stages every game inside it plus collection.json and bakes the lot into one wasm; the engine boots into the shelf exactly as it does on the desktop. Sixteen cases of Police Squad Quest come to 2.1 MB total. - embedded_world() becomes embedded_world_at(prefix), so one bundle can hold many games; embedded_games() / embedded_collection_name() are the browser twins of world::scan_collection / collection_name. - The picker is now keyed by String on both platforms and resolved through one load_by_key() — a filesystem path natively, an embedded subdirectory in the browser — so Mode::Title has a single implementation. - Esc is a no-op on the web when there is nowhere to back out to. Breaking the main loop in a browser just leaves a dead black canvas; found by pressing it. - ascii_fold() on every game-text draw. macroquad's built-in font has no em dash, curly quote, star or dagger, so room names like "— Squad Room —" and every death card were drawing tofu boxes in both builds. Folding at the draw layer fixes the whole engine without making game authors type worse prose. - web/build.sh uses the rustup toolchain explicitly: cargo/rustc on PATH may be Homebrew's, which ships no wasm32 std even though `rustup target list` shows the target installed, and the resulting "can't find crate for core" is a thoroughly misleading error. Verified in a real browser: shelf renders, arrows move the selection and swap the live room preview, Enter loads and plays the case, Esc returns to the shelf with the selection preserved. Native --shot and all 13 core tests still pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
649 lines
28 KiB
Markdown
649 lines
28 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). Point it at a **folder of game folders** and you get a **title screen / world picker**: arrow keys choose, Enter plays, Esc backs out of a game to the shelf. An optional `collection.json` `{"name": "..."}` in the folder sets the big title. Works natively *and* in the browser — `./web/build.sh <collection>` bakes the whole shelf into one wasm. |
|
||
| `--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; **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.* 🤖🎸
|