docs: add architecture handover — headless core + command bus plan
Captures the 2026-07-05 design session: making MRPGI fully programmatically controllable (MCP + CLI/stdio + HTTP/WebSocket) via a headless mrpgi-core engine behind a typed command/event bus, with the macroquad GUI as one optional front-end. Includes: current-state analysis, the core refactor (GameState with apply/tick), games-as-folders + JSON lore ingestion, an "always want options" audio design (SynthSink / FileSink mp3-ogg-wav / StrudelSink, all toggleable), a P0-P6 roadmap, quick wins, risks, and a source map. Adds docs/architecture.svg (self-contained) plus an inline Mermaid diagram in the handover. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d0caf983a0
commit
d0728e121c
262
docs/HANDOVER.md
Normal file
262
docs/HANDOVER.md
Normal file
@ -0,0 +1,262 @@
|
||||
# MRPGI — Handover: making the engine fully programmatically controllable
|
||||
|
||||
> **Status:** design / discussion complete. **No engine code has been changed yet.**
|
||||
> **Date:** 2026-07-05
|
||||
> **Purpose:** capture the vision, the current-state analysis, the decisions locked in, and the phased plan so any future session (human or AI) can pick up exactly where we left off.
|
||||
|
||||
---
|
||||
|
||||
## 1. The dream (in the owner's words)
|
||||
|
||||
> "It's an engine for a text adventure but it's got its own paint app inside it and can import art or images and you can assign functions to bands or objects, so you can build a Sierra-style game with it. I want it **100% fully programmatically controllable via CLI / terminal / endpoints / hooks / websockets / MCP / Python**, so an AI can create a whole game from prompts and I can just drop JSON text lore in for expansions. As many features as the OG engine had, plus newer tech. Use **Strudel** to make sounds, which can also be controlled. Always want **options**."
|
||||
|
||||
The engine is **MRPGI — Monster Robot Party Game Interpreter**: a new-school, Apple-Silicon-native reimagining of Sierra's 1980s AGI engine (King's Quest / Space Quest / Police Quest), written in Rust + [macroquad](https://macroquad.rs).
|
||||
|
||||
---
|
||||
|
||||
## 2. Decisions locked this session
|
||||
|
||||
| Question | Decision |
|
||||
|---|---|
|
||||
| Primary control surfaces | **MCP server + CLI/stdio (JSONL) + HTTP/WebSocket** (all three). Python rides on HTTP. |
|
||||
| Runtime model | **Headless-first, GUI optional.** The engine core runs with no window; macroquad becomes one optional viewer. |
|
||||
| Audio | **Offer all backends, toggle any combination on/off** — native chiptune synth, audio files (mp3/ogg/wav), and Strudel. |
|
||||
| Guiding principle | **"Always want options."** Every subsystem is a swappable, runtime-toggleable backend; nothing is locked in. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture at a glance
|
||||
|
||||

|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph FE["Front-ends — how you drive it (any/all at once)"]
|
||||
GUI["macroquad GUI<br/>play + paint · optional"]
|
||||
CLI["CLI · stdio<br/>JSONL in, events out"]
|
||||
HTTP["HTTP + WebSocket<br/>REST · SSE · Python"]
|
||||
MCP["MCP server<br/>Claude authors + plays"]
|
||||
end
|
||||
|
||||
BUS(["Command / event bus<br/>commands down · events up"])
|
||||
|
||||
subgraph CORE["mrpgi-core — headless engine (zero macroquad)"]
|
||||
GS["GameState<br/>apply(cmd) → events · tick(dt) → events"]
|
||||
PARSE["Parser + AI lane<br/>verb·noun → authoritative logic"]
|
||||
FB["FrameBuffer<br/>visual + priority buffers"]
|
||||
WORLD["World<br/>rooms · exits · flags/vars"]
|
||||
RENDER["render_visual()<br/>→ PNG, no window"]
|
||||
end
|
||||
|
||||
DATA["A game = a folder<br/>game.json · rooms/*.json · sprites/*.png · audio"]
|
||||
AUDIO["Audio out — toggle both/either/off<br/>SynthSink · FileSink (mp3/ogg/wav) · StrudelSink"]
|
||||
|
||||
GUI --> BUS
|
||||
CLI --> BUS
|
||||
HTTP --> BUS
|
||||
MCP --> BUS
|
||||
BUS --> CORE
|
||||
DATA -->|load_game · lore upsert| CORE
|
||||
CORE -->|AudioSink events| AUDIO
|
||||
```
|
||||
|
||||
**The one insight:** "CLI or terminal or endpoints or hooks or websockets or MCP or Python" is **one feature, not seven** — a headless core behind a typed command/event bus. Every surface is a thin adapter that pushes `Command`s in and reads `Event`s out.
|
||||
|
||||
---
|
||||
|
||||
## 4. Current-state assessment (the honest read)
|
||||
|
||||
MRPGI is a solid, coherent ~3,500-line AGI recreation whose foundations are **much closer to headless-ready than they look.**
|
||||
|
||||
**What's genuinely good and reusable as-is:**
|
||||
- The real AGI priority/depth trick: dual `visual` + `priority` buffers, a `band(y)` depth table, and per-pixel `pri >= room.priority[i]` compositing (`compose()` at `src/main.rs:387`). This is the actual "walk behind the tree" mechanism, not a fake z-sort.
|
||||
- A deterministic verb-noun parser (`run_command()` at `src/main.rs:586`) with an **AI fallback that cannot cheat** — the LLM only translates free text into an *already-allowed* command, which is then re-run through the real logic (`src/ai.rs`). This is the correct architecture; most homages let the LLM invent state.
|
||||
- A 1,362-line in-engine paint editor with dual LOOK + MEANING layers, so art and walkability/depth are painted together (`src/editor.rs`).
|
||||
- **The whole data model already derives `serde`** with disciplined `#[serde(default)]`: `RoomDoc`, `ObjDef`, `DlgNode`, `Stroke`, `Meaning` (`src/editor.rs:104-146`). Rooms already round-trip to `rooms/roomN.json`; there is already a `kit/` of themed objects and locations.
|
||||
|
||||
**The single weakness — it is structural, not rot:**
|
||||
`src/main.rs` fuses three things into one macroquad loop:
|
||||
1. **game state** — locals at `src/main.rs:68-76` (`ego`, `inventory`, `taken`, `won`, `dlg`, `command`, `transcript`);
|
||||
2. **update logic** — `ego.update()` (283), `run_command()` (188), the exit-transition scan (289–324), the AI poll (234);
|
||||
3. **presentation** — `get_frame_time()`, `get_char_pressed()`, `mouse_position()`, `compose()`, `draw_texture_ex()`, `next_frame().await`.
|
||||
|
||||
There is currently **no way to advance the game without rendering a frame.** Everything the owner wants is downstream of fixing that one fusion.
|
||||
|
||||
**Why it's ~80% ready and doesn't know it:** `ScreenObj::update(&FrameBuffer)`, the entire `picture::*` / `FrameBuffer` / `Cel` / `PicOp` pipeline, and `run_command()` are **already pure computation with no macroquad dependency.** They already take `&mut FrameBuffer` and return values. This is a *give-it-a-spine* job, not a rewrite.
|
||||
|
||||
---
|
||||
|
||||
## 5. The core architectural move
|
||||
|
||||
Split the crate into a Cargo workspace:
|
||||
- **`mrpgi-core`** — a library with **zero** macroquad dependency (a compile error is the guardrail).
|
||||
- **`mrpgi`** — the macroquad GUI, now just *one consumer* of the core.
|
||||
|
||||
Hoist the scattered `main()` locals into one owned struct with two pure methods and a typed bus:
|
||||
|
||||
```rust
|
||||
struct GameState {
|
||||
ego: ScreenObj,
|
||||
world: World, // rooms + current + FrameBuffer (today: the Editor struct)
|
||||
inventory: Vec<String>,
|
||||
taken: Vec<(u32, String)>,
|
||||
won: bool,
|
||||
dlg: Option<Dlg>,
|
||||
transcript: Vec<String>,
|
||||
flags: HashMap<String, bool>, // P4
|
||||
vars: HashMap<String, i32>, // P4
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
fn apply(&mut self, cmd: Command) -> Vec<Event>; // wraps run_command, goto_room, move_target, paint
|
||||
fn tick(&mut self, dt: f32) -> Vec<Event>; // wraps ego.update + exit-transition scan
|
||||
}
|
||||
```
|
||||
|
||||
- `compose()` splits into a pure `render_visual(&self) -> Vec<u8>` (lives in core) + a 3-line macroquad texture blit (lives in the GUI).
|
||||
- The clock becomes a `dt: f32` **parameter** instead of `get_frame_time()`, so scripted replays are bit-identical.
|
||||
- Input becomes `Command`s pushed onto the bus instead of direct `is_key_down` reads.
|
||||
- **The AI lane needs no change** — it already runs on a worker thread and reports over an `mpsc` channel (`src/ai.rs:81-88`), which is the event-bus pattern in miniature. It just moves from a `main.rs` local into the engine.
|
||||
|
||||
### Command / Event sketch (starting point)
|
||||
|
||||
```rust
|
||||
enum Command {
|
||||
Walk { dir: u8 }, MoveTo { x: i32, y: i32 },
|
||||
Parse { text: String }, Verb { verb: String, noun: String },
|
||||
GotoRoom { n: u32 },
|
||||
LoadRoom { n: u32, doc: RoomDoc }, UpsertObject { room: u32, obj: ObjDef },
|
||||
PaintStroke { stroke: Stroke },
|
||||
SetMusic { mood: u8 }, PlaySfx { cue: String },
|
||||
NewGame, Query,
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Transcript(String), RoomChanged(u32), InventoryChanged,
|
||||
DialoguePrompt { node: usize }, Won,
|
||||
Audio(AudioEvent), FrameReady, Error(String),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Control surfaces (all thin adapters over the bus)
|
||||
|
||||
| Surface | How it works | Effort |
|
||||
|---|---|---|
|
||||
| **In-process Rust API** | `Engine::apply(Command) -> Vec<Event>` — the substrate everything wraps. | M |
|
||||
| **CLI (stdio, JSONL)** | `mrpgi headless` reads newline JSON commands on stdin, writes newline JSON events on stdout. `--script foo.jsonl` replays a command log for deterministic tests. | S |
|
||||
| **HTTP + WebSocket** | axum listener on a worker thread (exactly like `ai.rs` spawns its translate thread). `POST /command`, `GET /state`, `GET /events` (SSE), `GET /frame.png` (headless render), `POST /rooms/{n}`, `POST /games`. | M |
|
||||
| **MCP server** | Wrap the bus as MCP tools: `mrpgi_apply_command`, `mrpgi_get_state`, `mrpgi_load_room`, `mrpgi_paint_stroke`, `mrpgi_render_frame` (returns the image so the model can *see* its own moves), `mrpgi_new_game`. | M |
|
||||
| **Python** | Thin client over the HTTP server (or a pyo3 binding). Falls out for free. | S |
|
||||
|
||||
**Concurrency model:** prefer a single owner thread with an `mpsc` command channel (mirroring `ai.rs`) over a `Mutex<Engine>` — keeps tick ordering deterministic, avoids lock contention.
|
||||
|
||||
---
|
||||
|
||||
## 7. Audio — "always want options"
|
||||
|
||||
Model audio as a `trait AudioSink` behind the bus. Sound calls (currently imperative: `sfx.pickup()` at `src/main.rs:162,199,217-224`) become `Event::Audio(..)` on the same event bus every control surface already consumes. Then pick sink(s) at runtime and/or declare them per-game in `game.json`:
|
||||
|
||||
- **`SynthSink`** — the existing `src/sound.rs` runtime chiptune synth. Offline default, procedural, tiny, LLM-authorable as text.
|
||||
- **`FileSink`** — plays `.mp3` / `.ogg` / `.wav` from the game folder. macroquad's `load_sound` auto-detects all three (`load_sound_from_bytes` is already what `sound.rs` calls, so this is a data-source swap, not a new system). **Prefer OGG for looping music** — MP3 encoders pad a few ms of silence that clicks at loop seams; fine for one-shot SFX. Likely the **first** audio backend to build (easier than the Strudel bridge).
|
||||
- **`StrudelSink`** — Strudel is browser/Web-Audio JS; the engine is native Rust, so they meet at a **message boundary, not shared memory.** The engine emits audio *descriptors* (`{sfx:"door"}`, `{pattern:"note(\"c e g\").s(\"sawtooth\")"}`) over the WebSocket to a tiny browser page running `@strudel/core` + `@strudel/webaudio`, which is the actual synth. Rooms gain an optional `music_pattern: Option<String>` so an LLM-authored room can carry a Strudel one-liner in its JSON.
|
||||
|
||||
Any combination runs at once, or none (silent). This mirrors the existing `ai.rs` provider switch (`ollama / openrouter / off`) — the template for every option in the engine.
|
||||
|
||||
**The rule that keeps options from becoming chaos:** options live at the *edges*, never in the core. `GameState` neither knows nor cares which sink or control surface is attached — it only emits `Event`s and applies `Command`s. That's what makes "add another option later" a 30-line trait impl instead of a rewrite.
|
||||
|
||||
---
|
||||
|
||||
## 8. Games as data (the "drop JSON lore in" milestone)
|
||||
|
||||
The bones already exist (`RoomDoc`, `ObjDef`, `DlgNode`, `Stroke` all serde; rooms round-trip; `kit/` holds JSON archetypes). The gap: a "game" is not yet a first-class loadable unit — the engine hardcodes `rooms/roomN.json` paths, room 0 as boot, and the seven verbs in code.
|
||||
|
||||
1. **Game manifest** — a `GameManifest` (`game.json`): `{ name, start_room, start_spawn, rooms_dir, sprites_dir, verbs, intro_text, win_text, flags, music_patterns }`. A game becomes a folder: `games/<name>/{game.json, rooms/*.json, sprites/*.png, sfx/*, music/*}`. `World::load_game(dir)` replaces the hardcoded paths.
|
||||
2. **Data-driven verbs** — lift the verb table (today split across `canon_verb` `src/main.rs:551`, `build_prompt` `src/ai.rs:73`, and the `run_command` match) into `Vec<VerbDef>` in the manifest. `build_prompt` then generates its whitelist *from* this table, so the AI and the parser can never drift.
|
||||
3. **Flags & vars** — add `flags`/`vars` to `GameState`; give `ObjDef`/`DlgChoice` optional `requires`/`sets` (all `#[serde(default)]` so existing rooms keep loading). Turns one-shot `wins` and tree dialogue into real gated puzzles.
|
||||
4. **Lore ingestion** — because the types are serde, an LLM expansion is just JSON that deserializes. Add `UpsertRoom` / `UpsertObject` / `PatchDialogue` commands so a running game extends live over HTTP/MCP. Ship a schemars-derived **JSON Schema** as the authoring contract. Validate ingested rooms with the existing `find_spawn` (`src/editor.rs`) + exit-target resolution before committing.
|
||||
|
||||
---
|
||||
|
||||
## 9. AGI-soul gaps surfaced (real, but downstream of the core move)
|
||||
|
||||
Not on the critical path to programmability — sequence *after* the bus/server land, or the refactor never ships:
|
||||
- Only one active room at a time (`editor.current: u32`) — no cross-room NPCs/state.
|
||||
- Click-to-walk uses naive greedy steering (`sprite.rs`), no pathfinding — gets stuck on diagonal obstacles.
|
||||
- Dialogue is tree-only, no conditions on choices.
|
||||
- Victory is a single `won: bool` (no flags/vars) — addressed in P4.
|
||||
- No item combining; take/drop are simple vec push/remove.
|
||||
- Room dimensions hardcoded 160×168 (`framebuffer.rs`).
|
||||
|
||||
---
|
||||
|
||||
## 10. Phased roadmap (each phase ships something usable)
|
||||
|
||||
| Phase | Goal | Unlocks |
|
||||
|---|---|---|
|
||||
| **P0 · Carve the core** | Workspace: `mrpgi-core` (lib, no macroquad) + `mrpgi` (GUI). Move the pure pieces over. No behaviour change. | A compilable proof the sim has no rendering dependency — precondition for everything. |
|
||||
| **P1 · GameState + bus** | `GameState` with `apply(Command)`/`tick(dt)`; GUI rewritten to drive them; `compose()` split; AI worker delivers a `Command::Parse` result as an `Event`. | Engine fully drivable & observable in-process without rendering. Golden-transcript tests possible. |
|
||||
| **P2 · Headless CLI + game folders** | `mrpgi headless` (JSONL, `--script` replay); `GameManifest` + `World::load_game`; data-driven verb table. | LLM/pipe control today; CI golden tests; authoring a game without touching Rust. **("Drop JSON lore in.")** |
|
||||
| **P3 · HTTP/WS + MCP** | axum server (incl. headless `/frame.png`); MCP tools incl. `render_frame`; Python client; JSON Schema authoring contract. | Claude/Python/curl drive **and** author games; the model can *see* frames it produces. **The headline scenario.** |
|
||||
| **P4 · AGI soul: flags/vars/conditions** | `flags`/`vars` in `GameState`; `requires`/`sets` on objects & dialogue; upsert commands with validation. | Real puzzles, gated dialogue, multi-step quests — all authorable as JSON. |
|
||||
| **P5 · Audio sinks** | `trait AudioSink`; `sound.rs` → `SynthSink`; `FileSink` (mp3/ogg/wav) + `music/`+`sfx/` folder convention; `StrudelSink` browser sidecar over WS; `music_pattern` on rooms. | Programmatic, LLM-authored music/sfx; every backend toggleable. |
|
||||
| **P6 · Headless editor** | `PaintStroke`/`UpsertObject` commands so the editor's stroke pipeline runs without `frame()`; GUI editor becomes a thin front-end over the same commands. | An LLM paints rooms via the same bus a human uses. |
|
||||
|
||||
---
|
||||
|
||||
## 11. Quick wins (afternoon-sized, near-zero risk — the recommended first move)
|
||||
|
||||
1. **`mrpgi --render-room rooms/room0.json out.png`** — bake a `RoomDoc` to PNG headless via the existing compose path. Proves headless rendering with almost no refactor; gives the LLM a way to preview authored rooms *today*.
|
||||
2. **Lift `run_command()` into `mrpgi-core` and unit-test it** with golden `(input → lines, understood)` cases. Zero risk; instant safety net for the P1 refactor.
|
||||
3. **Generate the AI verb list in `build_prompt` (`src/ai.rs:68`) *from* `canon_verb`'s table** instead of a hardcoded string, so the two can never drift. ~15 min; also preps data-driven verbs.
|
||||
4. **Wrap sound calls behind `trait AudioSink`** with the existing `Sfx`/`Music` as the impl (no behaviour change). Mechanical; unlocks the whole audio-options path later.
|
||||
|
||||
---
|
||||
|
||||
## 12. Risks & guardrails
|
||||
|
||||
- **macroquad owns the window/audio/GL context** via `#[macroquad::main]`; the core must never call into it. Enforce with the workspace boundary — a compile error is the guardrail. Watch `assets.rs`: the `quantize()`/`Cel` path is pure (`image` crate) but any macroquad `Image`/`Texture` use stays in the front-end.
|
||||
- **Determinism:** the headless core must take `dt` explicitly and fix the cycle rate (`CYCLE_DT = 1/20`) so replays are bit-identical. Keep the (non-deterministic) AI lane *out* of the deterministic core path — it produces a `Command` that then runs through deterministic logic (the current design already respects this).
|
||||
- **Backward-compat:** every new `RoomDoc`/`ObjDef` field MUST be `#[serde(default)]` (the codebase already does this) so the existing `rooms/*.json` and `kit/` packs keep loading.
|
||||
- **Strudel is a genuine native/web split** — accept a browser sidecar as the real path; don't over-invest in a native mini-notation interpreter until a native-only user needs it.
|
||||
- **Editor/Play share state** — the core split is the chance to make `World` the single source of truth. Keep the GUI editor driving the *same* commands the server uses, rather than a parallel path, to avoid regressing the working editor.
|
||||
|
||||
---
|
||||
|
||||
## 13. Open questions / next step
|
||||
|
||||
**Recommended opening play:** P0 (carve `mrpgi-core`) + the four quick wins, on a branch — gets a headless engine rendering a room and passing its first tests without touching the fun stuff.
|
||||
|
||||
**Or keep designing first:** nail down the exact `Command`/`Event` enums and the `game.json` manifest schema before any code moves.
|
||||
|
||||
Still to decide when building starts:
|
||||
- HTTP framework: axum vs. a minimal hyper/tiny_http (weigh against build times).
|
||||
- MCP transport: stdio (simplest) to start.
|
||||
- Whether `FileSink` lands in P2 (alongside game folders) or waits for P5.
|
||||
|
||||
---
|
||||
|
||||
## 14. Source map (current engine)
|
||||
|
||||
| File | Lines | Role |
|
||||
|---|---|---|
|
||||
| `src/main.rs` | 728 | Window, game loop, mode switch, parser (`run_command`), compositing, HUD. **The fusion to break up.** |
|
||||
| `src/editor.rs` | 1362 | In-engine paint tool; also the de-facto world database (`RoomDoc`, `ObjDef`, `DlgNode`, sprite catalog). |
|
||||
| `src/assets.rs` | 440 | PNG → EGA quantize + dither; tile / pour-into-shape / object insertion; hot-reload. |
|
||||
| `src/picture.rs` | 283 | Vector room painting (`PicOp` rects / lines / pixels), `stamp_cel_fit`. |
|
||||
| `src/sprite.rs` | 166 | The ego: movement, click-to-walk, collision, walk animation; `Prop`. |
|
||||
| `src/ai.rs` | 134 | AI parser lane (Ollama / OpenRouter / off) — **the template for the whole option/adapter pattern.** |
|
||||
| `src/sound.rs` | 151 | Runtime square-wave chiptune synth + 5 music moods → WAV bytes. |
|
||||
| `src/framebuffer.rs` | 87 | The `visual` + `priority` buffers and the `band(y)` depth table. |
|
||||
| `src/room.rs` | 72 | Hand-authored demo `Scene`. |
|
||||
| `src/view.rs` | 51 | Sprites as loops + cels, authored from ASCII grids. |
|
||||
| `src/palette.rs` | 40 | The 16-color EGA palette (indices → RGBA). |
|
||||
|
||||
Resources: `rooms/*.json` (room docs), `kit/` (themed objects + locations — proto game-folders), `sprites/*.png`, `assets/*.png`, `palettes/`. Docs: `README.md`, `MANUAL.md`, `EDITOR_GUIDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## 15. Provenance
|
||||
|
||||
This handover was produced from a full read of the engine source (`src/*.rs`) plus a parallel architecture-mapping pass over every subsystem (game loop, editor, render pipeline, actors, AI + sound, resources) and a synthesis design pass. It reflects the state of `main @ d0caf98` (Initial commit) and the design conversation of 2026-07-05. Diagram source: [`architecture.svg`](architecture.svg).
|
||||
91
docs/architecture.svg
Normal file
91
docs/architecture.svg
Normal file
@ -0,0 +1,91 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="560" viewBox="0 0 680 560" font-family="-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M2 1L8 5L2 9" fill="none" stroke="#888780" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="680" height="560" fill="#ffffff"/>
|
||||
|
||||
<text x="20" y="22" font-size="15" font-weight="500" fill="#2c2c2a">One engine, many front-ends</text>
|
||||
<text x="20" y="41" font-size="12" fill="#5f5e5a">Blue = how you drive it · teal = the headless engine core</text>
|
||||
|
||||
<!-- front-ends -->
|
||||
<g>
|
||||
<rect x="20" y="56" width="148" height="70" rx="8" fill="#e6f1fb" stroke="#4a90d9"/>
|
||||
<text x="94" y="88" font-size="14" font-weight="500" fill="#1b4a7a" text-anchor="middle">macroquad GUI</text>
|
||||
<text x="94" y="107" font-size="12" fill="#3a6ea5" text-anchor="middle">play + paint, optional</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="184" y="56" width="148" height="70" rx="8" fill="#e6f1fb" stroke="#4a90d9"/>
|
||||
<text x="258" y="88" font-size="14" font-weight="500" fill="#1b4a7a" text-anchor="middle">CLI · stdio</text>
|
||||
<text x="258" y="107" font-size="12" fill="#3a6ea5" text-anchor="middle">JSONL in, events out</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="348" y="56" width="148" height="70" rx="8" fill="#e6f1fb" stroke="#4a90d9"/>
|
||||
<text x="422" y="88" font-size="14" font-weight="500" fill="#1b4a7a" text-anchor="middle">HTTP + WebSocket</text>
|
||||
<text x="422" y="107" font-size="12" fill="#3a6ea5" text-anchor="middle">REST · SSE · Python</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="512" y="56" width="148" height="70" rx="8" fill="#e6f1fb" stroke="#4a90d9"/>
|
||||
<text x="586" y="88" font-size="14" font-weight="500" fill="#1b4a7a" text-anchor="middle">MCP server</text>
|
||||
<text x="586" y="107" font-size="12" fill="#3a6ea5" text-anchor="middle">Claude authors + plays</text>
|
||||
</g>
|
||||
|
||||
<line x1="94" y1="126" x2="94" y2="149" stroke="#888780" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<line x1="258" y1="126" x2="258" y2="149" stroke="#888780" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<line x1="422" y1="126" x2="422" y2="149" stroke="#888780" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<line x1="586" y1="126" x2="586" y2="149" stroke="#888780" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- bus -->
|
||||
<rect x="20" y="150" width="640" height="44" rx="8" fill="#f1efe8" stroke="#b4b2a9"/>
|
||||
<text x="340" y="169" font-size="14" font-weight="500" fill="#2c2c2a" text-anchor="middle">Command / event bus</text>
|
||||
<text x="340" y="185" font-size="12" fill="#5f5e5a" text-anchor="middle">commands down · events up</text>
|
||||
|
||||
<line x1="340" y1="194" x2="340" y2="213" stroke="#888780" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- core -->
|
||||
<rect x="20" y="214" width="640" height="176" rx="10" fill="#f7f7f5" stroke="#c9c7be"/>
|
||||
<rect x="32" y="224" width="266" height="26" rx="13" fill="#e1f5ee" stroke="#1d9e75"/>
|
||||
<text x="165" y="242" font-size="14" font-weight="500" fill="#0f6e56" text-anchor="middle">mrpgi-core · headless (no macroquad)</text>
|
||||
|
||||
<rect x="32" y="262" width="292" height="46" rx="6" fill="#ffffff" stroke="#d3d1c7"/>
|
||||
<text x="44" y="282" font-size="14" font-weight="500" fill="#2c2c2a">GameState</text>
|
||||
<text x="44" y="299" font-size="12" fill="#5f5e5a">apply(cmd) → events · tick(dt) → events</text>
|
||||
|
||||
<rect x="340" y="262" width="308" height="46" rx="6" fill="#ffffff" stroke="#d3d1c7"/>
|
||||
<text x="352" y="282" font-size="14" font-weight="500" fill="#2c2c2a">Parser + AI lane</text>
|
||||
<text x="352" y="299" font-size="12" fill="#5f5e5a">verb·noun → authoritative logic</text>
|
||||
|
||||
<rect x="32" y="320" width="196" height="46" rx="6" fill="#ffffff" stroke="#d3d1c7"/>
|
||||
<text x="42" y="340" font-size="14" font-weight="500" fill="#2c2c2a">FrameBuffer</text>
|
||||
<text x="42" y="357" font-size="12" fill="#5f5e5a">visual + priority buffers</text>
|
||||
|
||||
<rect x="240" y="320" width="196" height="46" rx="6" fill="#ffffff" stroke="#d3d1c7"/>
|
||||
<text x="250" y="340" font-size="14" font-weight="500" fill="#2c2c2a">World</text>
|
||||
<text x="250" y="357" font-size="12" fill="#5f5e5a">rooms · exits · flags/vars</text>
|
||||
|
||||
<rect x="448" y="320" width="200" height="46" rx="6" fill="#ffffff" stroke="#d3d1c7"/>
|
||||
<text x="458" y="340" font-size="14" font-weight="500" fill="#2c2c2a">render_visual()</text>
|
||||
<text x="458" y="357" font-size="12" fill="#5f5e5a">→ PNG, no window</text>
|
||||
|
||||
<line x1="170" y1="416" x2="170" y2="392" stroke="#888780" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="182" y="409" font-size="12" fill="#5f5e5a">load_game · lore upsert</text>
|
||||
<line x1="510" y1="392" x2="510" y2="416" stroke="#888780" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="522" y="409" font-size="12" fill="#5f5e5a">AudioSink events</text>
|
||||
|
||||
<!-- data folder -->
|
||||
<rect x="20" y="418" width="300" height="122" rx="10" fill="#f7f7f5" stroke="#c9c7be"/>
|
||||
<text x="34" y="442" font-size="14" font-weight="500" fill="#2c2c2a">A game = a folder</text>
|
||||
<text x="34" y="464" font-size="12" fill="#5f5e5a">games/<name>/</text>
|
||||
<text x="34" y="483" font-size="12" fill="#5f5e5a">game.json — manifest, verbs</text>
|
||||
<text x="34" y="501" font-size="12" fill="#5f5e5a">rooms/*.json — art, objects, dialogue</text>
|
||||
<text x="34" y="519" font-size="12" fill="#5f5e5a">sprites/*.png — cels</text>
|
||||
|
||||
<!-- audio -->
|
||||
<rect x="360" y="418" width="300" height="122" rx="10" fill="#f7f7f5" stroke="#c9c7be"/>
|
||||
<text x="374" y="442" font-size="14" font-weight="500" fill="#2c2c2a">Audio out — toggle both / either / off</text>
|
||||
<rect x="374" y="456" width="272" height="32" rx="6" fill="#ffffff" stroke="#d3d1c7"/>
|
||||
<text x="386" y="476" font-size="12" fill="#5f5e5a">Native chiptune · SynthSink (offline)</text>
|
||||
<rect x="374" y="496" width="272" height="32" rx="6" fill="#ffffff" stroke="#d3d1c7"/>
|
||||
<text x="386" y="516" font-size="12" fill="#5f5e5a">FileSink (mp3/ogg/wav) · StrudelSink (browser)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
Loading…
Reference in New Issue
Block a user