plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
0ec364b
— Peter John
2026-11-02T14:11:51+05:30
docs(plans): game host milestone 1 plan, progress and incidents
docs/plans/2026-11-02-game-host.incidents.jsonl
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{"file": "-", "line": 0, "context": "exit code 1: For more information, try '--help'.", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "13:42:31"}
|
|
2
|
+
{"file": "-", "line": 0, "context": "exit code 1: For more information, try '--help'.", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "13:42:36"}
|
|
3
|
+
{"file": "plum-std/Game.plum", "line": 1, "context": "Key.W impossible: constants need >=2 chars (single capital is generic). Named KeyA..KeyZ", "agent": "T2", "kind": "spec-ambiguity", "resolution": "Key.KeyW etc", "confidence": "high", "time": "13:42:42"}
|
|
4
|
+
{"file": "plum-core/src/union_lower.rs", "line": 0, "context": "T3: change outside Owns blocked by commit_task; nothing committed", "agent": "commit_task", "kind": "exception", "resolution": "open", "time": "13:43:56"}
|
|
5
|
+
{"file": "plum-game/Cargo.lock", "line": 0, "context": "T3: change outside Owns blocked by commit_task; nothing committed", "agent": "commit_task", "kind": "exception", "resolution": "open", "time": "13:43:56"}
|
|
6
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T7", "agent": "pt_review", "kind": "verified", "time": "13:49:37"}
|
|
7
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T7", "agent": "pt_review", "kind": "verified", "time": "13:49:37"}
|
|
8
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T6", "agent": "pt_review", "kind": "verified", "time": "13:51:37"}
|
|
9
|
+
{"file": "", "line": 0, "context": "verified: seam T2↔T6", "agent": "pt_review", "kind": "verified", "time": "13:51:37"}
|
|
10
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T4", "agent": "pt_review", "kind": "verified", "time": "13:56:10"}
|
|
11
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T4", "agent": "pt_review", "kind": "verified", "time": "13:56:10"}
|
|
12
|
+
{"file": "", "line": 0, "context": "verified: seam T2↔T4", "agent": "pt_review", "kind": "verified", "time": "13:56:10"}
|
|
13
|
+
{"file": "plum-game/kam/Main.plum", "line": 128, "context": "T6: mini.push result discarded (Array.push returns a new array); minimap OOB trap on first native draw; compiler accepts unused non-Unit results", "agent": "coordinator", "time": "13:57:47"}
|
|
14
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T4", "agent": "pt_review", "kind": "verified", "time": "13:57:47"}
|
|
15
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T4", "agent": "pt_review", "kind": "verified", "time": "13:57:47"}
|
|
16
|
+
{"file": "", "line": 0, "context": "verified: seam T2↔T4", "agent": "pt_review", "kind": "verified", "time": "13:57:47"}
|
|
17
|
+
{"file": "plum-cli/tests/game_web_test.rs", "line": 0, "context": "T5: change outside Owns blocked by commit_task; nothing committed", "agent": "commit_task", "kind": "exception", "resolution": "open", "time": "14:02:14"}
|
|
18
|
+
{"file": "plum-game/web/bridge_check.mjs", "line": 0, "context": "T5: change outside Owns blocked by commit_task; nothing committed", "agent": "commit_task", "kind": "exception", "resolution": "open", "time": "14:02:14"}
|
|
19
|
+
{"file": "", "line": 0, "context": "verified: seam T2↔T5", "agent": "pt_review", "kind": "verified", "time": "14:02:19"}
|
|
20
|
+
{"file": "", "line": 0, "context": "verified: seam T4↔T5", "agent": "pt_review", "kind": "verified", "time": "14:02:19"}
|
|
21
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T7", "agent": "pt_review", "kind": "verified", "time": "14:02:19"}
|
|
22
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T7", "agent": "pt_review", "kind": "verified", "time": "14:02:20"}
|
|
23
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T4", "agent": "pt_review", "kind": "verified", "time": "14:04:34"}
|
|
24
|
+
{"file": "", "line": 0, "context": "verified: seam T4↔T5", "agent": "pt_review", "kind": "verified", "time": "14:04:34"}
|
|
25
|
+
{"file": "", "line": 0, "context": "verified: seam T2↔T4", "agent": "pt_review", "kind": "verified", "time": "14:04:34"}
|
|
26
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T4", "agent": "pt_review", "kind": "verified", "time": "14:04:34"}
|
|
27
|
+
{"file": "", "line": 0, "context": "verified: seam T2↔T5", "agent": "pt_review", "kind": "verified", "time": "14:04:34"}
|
|
28
|
+
{"file": "", "line": 0, "context": "verified: seam T4↔T5", "agent": "pt_review", "kind": "verified", "time": "14:04:34"}
|
|
29
|
+
{"file": "", "line": 0, "context": "verified: seam T2↔T5", "agent": "pt_review", "kind": "verified", "time": "14:08:36"}
|
|
30
|
+
{"file": "", "line": 0, "context": "verified: seam T4↔T5", "agent": "pt_review", "kind": "verified", "time": "14:08:36"}
|
|
31
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T7", "agent": "pt_review", "kind": "verified", "time": "14:08:36"}
|
|
32
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T7", "agent": "pt_review", "kind": "verified", "time": "14:08:36"}
|
|
33
|
+
{"file": "-", "line": 0, "context": "tests::readmeSnippetsHaveNoSyntaxDiagnostics: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:08:59"}
|
|
34
|
+
{"kind":"rule-rewrite","agent":"coordinator","tool":"Bash","command":"cargo test --workspace readmeSnippets 21 | grep -B2 -A15 panicked | head -40"}
|
|
35
|
+
{"file": "-", "line": 0, "context": "tests::readmeSnippetsHaveNoSyntaxDiagnostics: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:09:15"}
|
|
36
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T4", "agent": "pt_review", "kind": "verified", "time": "14:10:32"}
|
|
37
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T4", "agent": "pt_review", "kind": "verified", "time": "14:10:32"}
|
|
38
|
+
{"file": "", "line": 0, "context": "verified: seam T2↔T4", "agent": "pt_review", "kind": "verified", "time": "14:10:32"}
|
|
39
|
+
{"file": "", "line": 0, "context": "verified: seam T4↔T5", "agent": "pt_review", "kind": "verified", "time": "14:10:32"}
|
|
40
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T7", "agent": "pt_review", "kind": "verified", "time": "14:10:32"}
|
|
41
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T7", "agent": "pt_review", "kind": "verified", "time": "14:10:32"}
|
docs/plans/2026-11-02-game-host.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Game host, milestone 1: native + browser host and a KaM map viewer
|
|
2
|
+
|
|
3
|
+
**Goal:** a Plum program declares `export fun init/update/draw`, and a macroquad host runs it natively (wasmtime) and in the browser (a miniquad JS plugin), drawing a real Knights and Merchants map with camera pan/zoom and a minimap. This covers tasks H1, H2 and P1 of `2026-10-26-game-engine.md`. P2–P5 (units, pathfinding, economy, combat/AI) are later milestones.
|
|
4
|
+
**Architecture:** the compiler gains `export fun` (same boundary ABI as `extern fun`) and always exports four Str bridge helpers. `plum-std/Game.plum` declares the host API as externs. The standalone crate `plum-game/` (macroquad) decodes KaM assets and implements those externs over a `Backend` trait: wasmtime binds them natively, and on wasm32 a `plum_bridge.js` miniquad plugin binds them through JS. `plum game <file> [--web]` compiles and runs or packages.
|
|
5
|
+
**Tech stack:** Rust workspace (plum-core, plum-checker, plum-wasm-codegen, plum-cli), tree-sitter grammar, Plum std, new standalone crate plum-game (macroquad 0.4, wasmtime 47, flate2), JS.
|
|
6
|
+
**Spec:** docs/plans/2026-11-02-game-host.md
|
|
7
|
+
**Detail:** contracts
|
|
8
|
+
**Target-min:** 25
|
|
9
|
+
|
|
10
|
+
## Design (rulings)
|
|
11
|
+
- **`export fun`** (top level only, not a method, not generic) is a normal Plum function that is also exported as a wasm function with the function's name. Its boundary ABI is exactly the `extern fun` ABI: `Int` narrowed to i32, `Float` f64, `Bool` i32, a struct flattened into its fields' values (params and multi-value results), `Str` and records as GC refs. Errors: `export fun can't be a method`, `export fun can't be generic`, `export fun can't return an error union` (same check `extern` has). `export` and `extern` on one fun is a parse error.
|
|
12
|
+
- **Str bridge exports**, always present in every module (JS can't read GC structs): `strLen(s: Str) -> i32`, `strByte(s: Str, i: i32) -> i32` (0..255), `strNew(n: i32) -> Str` (n zero bytes), `strSetByte(s: Str, i: i32, b: i32)` (only used by hosts on a fresh `strNew` result).
|
|
13
|
+
- **Game entry contract:** `export fun init() -> G`, `export fun update(g: G, dt: Float)`, `export fun draw(g: G)` where `G` is any record. The host calls `init` once after the window exists, then `update` then `draw` every frame. Game state lives in `G`; no globals.
|
|
14
|
+
- **Host API (`plum-std/Game.plum`, all `extern fun`)** — the exact contract between Plum and every backend. Coordinates are world units (camera applied) unless noted; `Int` handles are `-1` on failure.
|
|
15
|
+
- `screenWidth() -> Float`, `screenHeight() -> Float`, `frameTime() -> Float`
|
|
16
|
+
- `clear(c: Color)`; `drawRect(x: Float, y: Float, w: Float, h: Float, c: Color)`; `drawLine(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, c: Color)`; `drawText(s: Str, x: Float, y: Float, size: Float, c: Color)`
|
|
17
|
+
- `setCamera(x: Float, y: Float, scale: Float)` — world point `(x, y)` maps to the screen's top-left, one world unit is `scale` pixels; `resetCamera()` returns to screen pixels.
|
|
18
|
+
- `atlasLoad(name: Str) -> Int` — loads `<assets>/sprites/<name>.rxx` (e.g. `"Tileset"`, `"Trees_a"`); `spriteCount(atlas: Int) -> Int`; `spriteW(atlas: Int, idx: Int) -> Int`, `spriteH`, `spritePivotX`, `spritePivotY` (0 for a missing sprite); `spriteColor(atlas: Int, idx: Int) -> Color` (average of opaque pixels, for minimaps)
|
|
19
|
+
- `drawSprite(atlas: Int, idx: Int, x: Float, y: Float, flipX: Bool, tint: Color)` — top-left at `(x + pivotX, y + pivotY)`, natural size; `drawTile(atlas: Int, idx: Int, x: Float, y: Float, size: Float, quarterTurns: Int, tint: Color)` — stretched to a `size`×`size` square at top-left `(x, y)`, rotated `quarterTurns`×90° clockwise about its centre.
|
|
20
|
+
- `isKeyDown(k: Int) -> Bool`, `isKeyPressed(k: Int) -> Bool`, `mousePos() -> Vec2` (screen pixels, multi-value), `isMouseDown(b: Int) -> Bool`, `isMousePressed(b: Int) -> Bool`, `mouseWheel() -> Float` (notches this frame, + = up)
|
|
21
|
+
- `mapLoad(path: Str) -> Int` — a KaM `.map` relative to `<assets>`; `mapWidth(m: Int) -> Int`, `mapHeight(m: Int) -> Int`; `mapTerrain(m: Int, x: Int, y: Int) -> Int` (tileset id; Tileset sprite is id + 1), `mapRotation(m, x, y) -> Int` (0..3), `mapElevation(m, x, y) -> Int`, `mapObject(m, x, y) -> Int` (Trees_a sprite index, 0 = none). Row 0 is the map's first (north) row; no y flip.
|
|
22
|
+
- **Keys** (`primitive Key` constants, GLFW numbering, the host maps them to macroquad `KeyCode`): `Space 32`, `A`..`Z` 65..90, `Num0`..`Num9` 48..57, `Escape 256`, `Enter 257`, `Tab 258`, `Backspace 259`, `Right 262`, `Left 263`, `Down 264`, `Up 265`, `LeftShift 340`, `LeftControl 341`. **Mouse** (`primitive Mouse`): `Left 0`, `Right 1`, `Middle 2`.
|
|
23
|
+
- **Std value types:** `plum-std/Vec2.plum`: `struct Vec2(x: Float, y: Float)` with `add`, `sub`, `mul(s: Float)`, `neg`, `eq`, `length`, `distance`, `Vec2.Zero`. `plum-std/Color.plum`: `struct Color(r: Float, g: Float, b: Float, a: Float)` (0..1) with consts `White`, `Black`, `Red`, `Green`, `Blue`, `Yellow`, `Transparent` and `Color.rgb(r, g, b)`, `withAlpha(a)`. Game.plum imports both; they're separate so pure logic can import them without pulling in externs.
|
|
24
|
+
- **`plum test` and `plum run` stub unknown `plum` imports** with a function that traps `host function 'X' is not available here`, so a game file's `test` blocks run as long as they don't call the host.
|
|
25
|
+
- **Assets** are never copied into the repo. The host's asset root is `--assets <dir>` (default `$PLUM_GAME_ASSETS`, else `../kam_macroquad/assets` beside the repo). plum-game vendors a minimal decoder: `.rxx` (from kam_macroquad `src/rxx.rs`, with a source note) and `.map` (from `src/rules/map_file.rs` `parse_map`). Tests that need real assets skip with a printed note when the directory is missing.
|
|
26
|
+
- **Audio** is deferred to P2 (KaM sfx decoding); no audio externs in this milestone.
|
|
27
|
+
- **Web:** the macroquad host compiles to `wasm32-unknown-unknown`. Before the first frame it fetches every file listed in `preload.txt` (sprites and maps) with macroquad's `load_file`, and polls `plum_bridge_ready()` until the plugin has instantiated `plum.wasm`; then it calls `plum_bridge_init/update/draw`, which call the Plum exports. Plum's externs call back into `pg_*` exports of the host wasm (re-entrant calls are fine: no borrow is held across them). Str crosses through `strLen/strByte` (Plum → host, bytes copied into host memory from `pg_alloc(n)`) and `strNew/strSetByte` (host → Plum).
|
|
28
|
+
- **`plum game <file>`** compiles, then runs the native host (`cargo run --release --manifest-path <repo>/plum-game/Cargo.toml -- <wasm> --assets <dir>`). **`plum game --web <file> [-o dist] [--preload path]...`** writes `index.html`, `mq_js_bundle.js`, `plum_bridge.js`, `plum-game.wasm`, `plum.wasm`, `preload.txt` and an `assets/` copy of the preloaded files (the six `sprites/*.rxx` are always preloaded) into `dist` (default `<file dir>/dist`, gitignored).
|
|
29
|
+
- **P1 viewer** (`plum-game/kam/Main.plum`): loads `Maps/A new Beginning/A new Beginning.map` into `Grid[Int]`s once at `init`; draws only visible tiles with `drawTile` (Tileset, id + 1, rotation) and map objects with `drawSprite` (Trees_a) at 32 world units per tile; camera pans with arrows/WASD (speed independent of zoom on screen), zooms with the wheel about the mouse (scale clamped 0.25..4), and is clamped to the map; a minimap (one rect per tile, colours from `spriteColor`, computed once) in the bottom-right corner, with the view rectangle outlined; clicking it centres the camera; FPS in the top-left. No elevation relief in this milestone.
|
|
30
|
+
|
|
31
|
+
## Global constraints
|
|
32
|
+
- Regenerate tree-sitter only with `cd plum-tooling/tree-sitter-plum && npx --yes tree-sitter-cli generate --disable-optimizations`, then `npx --yes tree-sitter-cli test`.
|
|
33
|
+
- `touch` an edited `.rs` file before trusting a build. Plum tests: `cargo build --quiet && ./target/debug/plum test <file-or-dir>`.
|
|
34
|
+
- Never run `plum format` on repo files.
|
|
35
|
+
- `plum-game/` is its own cargo workspace (`[workspace]` in its Cargo.toml, like `plum-runtime/`), never a member of the root workspace. It may path-depend on `../plum-core` and `../plum-wasm-codegen` as dev-dependencies to compile Plum fixtures in tests.
|
|
36
|
+
- wasmtime is `cfg(not(target_arch = "wasm32"))` only. `cargo build --target wasm32-unknown-unknown` in `plum-game/` must succeed.
|
|
37
|
+
- Never copy files from `../kam_macroquad/assets` into this repo. `dist/` and `plum-game/target/` are gitignored.
|
|
38
|
+
- Comments and docs describe the final behaviour only, no history.
|
|
39
|
+
|
|
40
|
+
```mermaid
|
|
41
|
+
flowchart LR
|
|
42
|
+
P["Main.plum (export init/update/draw)"] -->|"externs (Game.plum)"| B["Backend trait"]
|
|
43
|
+
B --> N["native: wasmtime linker + macroquad"]
|
|
44
|
+
B --> W["web: plum_bridge.js -> pg_* exports"]
|
|
45
|
+
N & W --> A["assets.rs: .rxx + .map"]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## File structure
|
|
49
|
+
- `plum-tooling/tree-sitter-plum/*`, `plum-core/src/{ast,parser}.rs`, `plum-checker/src/lib.rs`, `plum-wasm-codegen/src/lib.rs`, `plum-lsp/src/completion.rs`: `export fun`, Str bridge.
|
|
50
|
+
- `plum-std/{Vec2,Color,Game}.plum`, `plum-cli/src/main.rs` (import stubs): the Plum side of the API.
|
|
51
|
+
- `plum-game/`: `Cargo.toml`, `src/lib.rs`, `src/assets.rs`, `src/api.rs`, `src/recorder.rs`, `src/native.rs`, `src/mq.rs`, `src/main.rs`, `src/web.rs`, `web/{plum_bridge.js,index.html}`, `tests/`, `kam/Main.plum`.
|
|
52
|
+
- `plum-cli/src/game.rs`: `plum game`.
|
|
53
|
+
|
|
54
|
+
## Task graph
|
|
55
|
+
```mermaid
|
|
56
|
+
flowchart LR
|
|
57
|
+
T1[export fun + Str bridge]
|
|
58
|
+
T2[std Game API + import stubs]
|
|
59
|
+
T3[plum-game assets]
|
|
60
|
+
T4[native host]
|
|
61
|
+
T5[web bridge + plum game + docs]
|
|
62
|
+
T6[KaM map viewer]
|
|
63
|
+
T7[incidental files]
|
|
64
|
+
%% taskgraph
|
|
65
|
+
T1 --> T4
|
|
66
|
+
T2 --> T4
|
|
67
|
+
T3 --> T4
|
|
68
|
+
T4 --> T5
|
|
69
|
+
T2 --> T5
|
|
70
|
+
T1 --> T6
|
|
71
|
+
T2 --> T6
|
|
72
|
+
T1 --> T7
|
|
73
|
+
T3 --> T7
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```mermaid
|
|
77
|
+
%% schedule
|
|
78
|
+
gantt
|
|
79
|
+
title Predicted schedule (mm:ss)
|
|
80
|
+
dateFormat X
|
|
81
|
+
axisFormat %M:%S
|
|
82
|
+
section lane 1
|
|
83
|
+
T1 export fun + Str bridge :t1, 0, 208s
|
|
84
|
+
T4 native host Backend wasmtime bindin :t4, 208, 283s
|
|
85
|
+
T5 web bridge + `plum game` + README :t5, 492, 246s
|
|
86
|
+
section lane 2
|
|
87
|
+
T2 std Game API + import stubs :t2, 0, 142s
|
|
88
|
+
T6 KaM map viewer (P1) :t6, 208, 150s
|
|
89
|
+
section lane 3
|
|
90
|
+
T3 plum-game crate + KaM asset decoders :t3, 0, 128s
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
critical path: T1 → T4 → T5 (12.3 min) · peak width 3 · predicted build 12.3 min · predicted total ≈ 15.8 min (target ✓)
|
|
94
|
+
|
|
95
|
+
## Predicted schedule
|
|
96
|
+
|
|
97
|
+
## Package: compiler
|
|
98
|
+
**Probe entry:** run=cargo test -p plum-wasm-codegen
|
|
99
|
+
|
|
100
|
+
### T1 export fun + Str bridge · Complexity 6 (O-1) · Est. context 60k
|
|
101
|
+
**Owns:** plum-tooling/tree-sitter-plum/grammar.js, plum-tooling/tree-sitter-plum/src/parser.c, plum-tooling/tree-sitter-plum/src/grammar.json, plum-tooling/tree-sitter-plum/src/node-types.json, plum-tooling/tree-sitter-plum/test/corpus/export.txt, plum-tooling/tree-sitter-plum/queries/plum/highlights.scm, plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json, plum-core/src/ast.rs, plum-core/src/parser.rs, plum-core/tests/parser_test.rs, plum-checker/src/lib.rs, plum-checker/tests/checker_tests.rs, plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/codegen_tests.rs, plum-lsp/src/completion.rs
|
|
102
|
+
**Components:** 3
|
|
103
|
+
**LOC owned:** 260
|
|
104
|
+
**Tests:** slow
|
|
105
|
+
**Kind:** design
|
|
106
|
+
**Model:** sonnet
|
|
107
|
+
**Est. time:** 6 min
|
|
108
|
+
**Consumes:** none
|
|
109
|
+
**Produces:** wasm exports of a compiled module
|
|
110
|
+
e.g. `struct V(x: Float, y: Float)\nexport fun step(n: Int, v: V) -> V =\n V(x: v.x + 1.0, y: v.y * 2.0)\n` -> export `step` with type `(func (param i32 f64 f64) (result f64 f64))`; calling it with (3, 1.0, 2.0) returns (2.0, 4.0)
|
|
111
|
+
e.g. `record G(n: Int)\nexport fun init() -> G =\n G(n: 1)\nexport fun bump(g: G) =\n g.n = g.n + 1\n` -> exports `init` (result is a GC ref) and `bump`; host calls init, bump twice on the same ref, reads nothing back but no trap
|
|
112
|
+
e.g. any module -> exports `strLen`, `strByte`, `strNew`, `strSetByte`; `strNew(3)` then `strSetByte` 104,105,33 then passing it to a Plum `export fun echo(s: Str) -> Str = s` and reading back with `strLen`/`strByte` gives "hi!"
|
|
113
|
+
**Review Focus:** `export_abi_matches_extern`: Int param is i32 and is sign-extended to i64 inside; Int result narrowed with wrap; Bool param/result i32; `export_struct_flattened`: nested struct (struct with a struct field) flattens depth-first; `export_errors`: method / generic / error-union return / `extern export` each give their error; `export_still_callable`: Plum code calling an `export fun` directly works and uses the normal internal ABI (an i32 wrapper may be emitted separately); `str_bridge_on_view`: `strByte` on a substring view (`"hello".sub(1,3)`) indexes from `start`; `main_unchanged`: modules with `main` still export it as before.
|
|
114
|
+
**Test code:** parser_test.rs `exportFunParses`; checker_tests.rs one test per error; codegen_tests.rs `exportFlattensStructsAndNarrowsInt`, `exportRecordRefRoundTrip`, `strBridgeRoundTrip`, `strBridgeOnView` (wasmtime calls, the e.g. lines); corpus `export.txt`.
|
|
115
|
+
**Steps:** 1 failing tests, 2 grammar: optional `exportKw` beside `externKw` (mutually exclusive), regenerate, corpus, highlights `"export" @keyword`, tmLanguage keyword, 3 ast `is_export: bool`, parser sets it, 4 checker errors, 5 codegen: for each export fun emit a boundary wrapper with the extern ABI that converts and calls the real function, export it under the fun's name; emit and export the four Str helpers, 6 lsp keyword completion `export`, 7 `cargo test --workspace` and `./target/debug/plum test plum-std plum-examples` green.
|
|
116
|
+
**Notes:** reuse the existing extern lowering (flattening, i32 narrowing) for the wrapper rather than writing a second one. Don't touch README (T5 documents `export`).
|
|
117
|
+
|
|
118
|
+
## Package: std
|
|
119
|
+
**Probe entry:** run=./target/debug/plum test plum-std
|
|
120
|
+
|
|
121
|
+
### T2 std Game API + import stubs · Complexity 4 (O-1) · Est. context 40k
|
|
122
|
+
**Owns:** plum-std/Vec2.plum, plum-std/Color.plum, plum-std/Game.plum, plum-cli/src/main.rs, plum-cli/tests/host_stub_test.rs
|
|
123
|
+
**Components:** 2
|
|
124
|
+
**LOC owned:** 220
|
|
125
|
+
**Tests:** fast
|
|
126
|
+
**Kind:** design
|
|
127
|
+
**Model:** sonnet
|
|
128
|
+
**Est. time:** 4 min
|
|
129
|
+
**Consumes:** none
|
|
130
|
+
**Produces:** std/Game, std/Vec2, std/Color modules
|
|
131
|
+
e.g. `import std/Vec2\n... Vec2(x: 1.0, y: 2.0) + Vec2(x: 3.0, y: 4.0) * 2.0` == `Vec2(x: 7.0, y: 10.0)`
|
|
132
|
+
e.g. `Color.rgb(1.0, 0.5, 0.0)` == `Color(r: 1.0, g: 0.5, b: 0.0, a: 1.0)`; `Key.W == 87`, `Key.Left == 263`, `Mouse.Right == 1`
|
|
133
|
+
e.g. a file importing std/Game whose test block doesn't call any extern passes under `plum test`; one that calls `screenWidth()` fails that test with `host function 'screenWidth' is not available here`
|
|
134
|
+
**Review Focus:** `extern_list_exact`: every extern in the Design's Host API list, exact names, parameter order and types; `stub_only_plum_module`: real host functions (printLn, crash, clock, files) keep working; the stub covers only names the host doesn't define; `stub_traps_not_panics`: the stub returns a wasm trap that the test runner reports as that test's failure; `vec2_ops`: operators work through `add`/`sub`/`mul`/`neg`/`eq`.
|
|
135
|
+
**Test code:** `test` blocks in Vec2.plum and Color.plum (the e.g. lines); host_stub_test.rs runs `plum test` on a scratch file importing std/Game: one passing test, one calling `screenWidth()` that fails with the message.
|
|
136
|
+
**Steps:** 1 write Vec2.plum, Color.plum with tests, 2 Game.plum: externs exactly per the Design, `primitive Key` and `primitive Mouse` constants, 3 plum-cli hostImports: for each module import with no host definition, a func of its type that traps with the message, 4 host_stub_test, 5 `./target/debug/plum test plum-std plum-examples` and `cargo test -p plum-cli` green.
|
|
137
|
+
**Notes:** keep Game.plum to declarations and constants; no logic. Don't touch plum-cli's command list (T5 adds `plum game` in `plum-cli/src/game.rs`).
|
|
138
|
+
|
|
139
|
+
## Package: host
|
|
140
|
+
**Probe entry:** run=cargo test --manifest-path plum-game/Cargo.toml
|
|
141
|
+
|
|
142
|
+
### T3 plum-game crate + KaM asset decoders · Complexity 4 (O-1) · Est. context 45k
|
|
143
|
+
**Owns:** plum-game/Cargo.toml, plum-game/src/lib.rs, plum-game/src/assets.rs, plum-game/tests/assets_test.rs, plum-game/.gitignore
|
|
144
|
+
**Components:** 1
|
|
145
|
+
**LOC owned:** 300
|
|
146
|
+
**Tests:** fast
|
|
147
|
+
**Kind:** design
|
|
148
|
+
**Model:** sonnet
|
|
149
|
+
**Est. time:** 4 min
|
|
150
|
+
**Consumes:** none
|
|
151
|
+
**Produces:** assets::Atlas::parse(bytes: &[u8]) -> io::Result<Atlas>; Atlas { sprites: Vec<Option<Sprite>> } with Sprite { w: u16, h: u16, px: i32, py: i32, rgba: Vec<u8> } and avg_color(idx) -> [f32; 4]; assets::MapData::parse(bytes: &[u8]) -> Result<MapData, String> with MapData { w: i32, h: i32, terrain: Vec<u16>, rotation: Vec<u8>, elevation: Vec<u8>, object: Vec<u16> } indexed y * w + x; assets::AssetRoot::resolve(cli: Option<&Path>) -> PathBuf
|
|
152
|
+
e.g. `Atlas::parse(read("sprites/Tileset.rxx"))` -> sprite 1 is 32×32
|
|
153
|
+
e.g. `MapData::parse(read("Maps/A new Beginning/A new Beginning.map"))` -> w, h both in 32..=256 and terrain.len() == w*h; every terrain id < 512
|
|
154
|
+
**Review Focus:** `rxx_header_variants`: RXX1, RXX2 and plain-zlib inputs; `map_object_mapping`: map object ids converted to Trees_a sprite indices the way kam_macroquad's `kamdat::map_object_sprite` does (0 = none), including its mapelem.dat lookup if needed (read from `<assets>/data/defines/mapelem.dat`); `bounds`: truncated input returns an error, never panics; `no_macroquad_in_assets`: assets.rs is plain Rust (macroquad-free) so it tests fast.
|
|
155
|
+
**Test code:** assets_test.rs: `tilesetSprite1Is32`, `mapParsesANewBeginning`, `truncatedRxxIsError`, `truncatedMapIsError`; real-asset tests skip with a printed note when the asset root is missing.
|
|
156
|
+
**Steps:** 1 Cargo.toml (own `[workspace]`; deps macroquad 0.4 default-features=false, flate2; wasmtime 47 under `cfg(not(target_arch = "wasm32"))`; dev-deps plum-core, plum-wasm-codegen by path) and .gitignore (`target/`, `dist/`), 2 failing tests, 3 assets.rs from kam_macroquad's rxx.rs and map_file.rs parse_map (with a one-line source note each), 4 lib.rs `pub mod assets;`, 5 `cargo test --manifest-path plum-game/Cargo.toml` green.
|
|
157
|
+
**Notes:** read `../kam_macroquad/src/rxx.rs`, `src/rules/map_file.rs` and `src/kamdat.rs` (map_object_sprite) for the formats; copy only what's needed.
|
|
158
|
+
|
|
159
|
+
### T4 native host: Backend, wasmtime binding, macroquad, recorder · Complexity 7 (O-1) · Est. context 70k
|
|
160
|
+
**Owns:** plum-game/src/lib.rs, plum-game/src/api.rs, plum-game/src/recorder.rs, plum-game/src/native.rs, plum-game/src/mq.rs, plum-game/src/main.rs, plum-game/tests/host_test.rs, plum-game/tests/fixtures/host_fixture.plum
|
|
161
|
+
**Components:** 4
|
|
162
|
+
**LOC owned:** 650
|
|
163
|
+
**Tests:** slow
|
|
164
|
+
**Kind:** design
|
|
165
|
+
**Model:** sonnet
|
|
166
|
+
**Est. time:** 8 min
|
|
167
|
+
**Consumes:** T1 export ABI; T2 Game.plum externs; T3 assets
|
|
168
|
+
**Produces:** api::Backend trait (one method per Host API extern, Rust types: f32 for Float, i32 for Int, bool, String for Str, [f32;4] Color, (f32,f32) Vec2); native::PlumGame::load(wasm: &[u8], backend: B) -> Result<PlumGame<B>>, .init(), .update(dt: f32), .draw(); recorder::Recorder (records calls as strings, scripted input); binary `plum-game <module.wasm> [--assets DIR]`
|
|
169
|
+
e.g. fixture `export fun draw(g: G) = clear(Color.Black); drawRect(1.0, 2.0, 3.0, 4.0, Color.Red); p := mousePos(); drawText("hi", p.x, p.y, 16.0, Color.White)` with Recorder mouse (5, 6) -> calls `["clear(0,0,0,1)", "drawRect(1,2,3,4,1,0,0,1)", "mousePos()", "drawText(\"hi\",5,6,16,1,1,1,1)"]`
|
|
170
|
+
**Review Focus:** `color_flattened`: Color arrives as four f64 and becomes [f32;4]; `mousePos_multi_value`: host returns two f64 results; `str_args`: drawText/atlasLoad/mapLoad read the Plum Str through the GC struct (reuse plum-cli's Str reading approach); `state_ref_held`: the `init` result is kept rooted across frames (wasmtime `ManuallyRooted`/`OwnedRooted`) and passed to every update/draw; `missing_export`: a module without `init`/`update`/`draw` errors with which one is missing; `int_handles`: atlas/map handles are indices into host Vecs, -1 on failure; `draw_tile_rotation`: quarterTurns rotate about the centre; `key_mapping`: the Design's key numbers map to the right macroquad KeyCode.
|
|
171
|
+
**Test code:** host_test.rs compiles `tests/fixtures/host_fixture.plum` with plum-core + plum-wasm-codegen (lib path = repo root) and drives it with Recorder: `callOrderAndArgs`, `colorFlattened`, `mousePosMultiValue`, `stateSurvivesFrames` (update increments a counter 3 times, draw reports it via drawText), `missingExportErrors`, `atlasAndMapThroughHost` (skips without assets).
|
|
172
|
+
**Steps:** 1 failing tests, 2 api.rs Backend, 3 recorder.rs, 4 native.rs: wasmtime engine (gc, function references, multi-value), linker binding every extern to Backend, plus `printLn`/`crash`, PlumGame, 5 mq.rs: macroquad Backend (textures per sprite uploaded lazily on first draw, Camera2D from setCamera, KeyCode mapping, wheel), 6 main.rs: `#[macroquad::main]` loop calling init once then update/draw, native only, 7 lib.rs modules, 8 `cargo test --manifest-path plum-game/Cargo.toml` green and `cargo build --manifest-path plum-game/Cargo.toml --release` succeeds.
|
|
173
|
+
**Notes:** macroquad's main loop is async; wasmtime calls are sync, fine inside it. Keep native.rs and mq.rs behind `cfg(not(target_arch = "wasm32"))`; api.rs and recorder.rs build on every target.
|
|
174
|
+
|
|
175
|
+
### T5 web bridge + `plum game` + README · Complexity 7 (O-1) · Est. context 65k
|
|
176
|
+
**Owns:** plum-game/src/web.rs, plum-game/src/lib.rs, plum-game/src/main.rs, plum-game/web/plum_bridge.js, plum-game/web/index.html, plum-cli/src/game.rs, plum-cli/src/main.rs, plum-cli/Cargo.toml, README.md, .gitignore
|
|
177
|
+
**Components:** 3
|
|
178
|
+
**LOC owned:** 550
|
|
179
|
+
**Tests:** slow
|
|
180
|
+
**Kind:** design
|
|
181
|
+
**Model:** sonnet
|
|
182
|
+
**Est. time:** 8 min
|
|
183
|
+
**Consumes:** T4 Backend + mq backend; T2 Game.plum
|
|
184
|
+
**Produces:** `plum game <file> [--assets DIR]` and `plum game --web <file> [-o DIR] [--preload PATH]... [--assets DIR]`
|
|
185
|
+
e.g. `plum game --web plum-game/tests/fixtures/host_fixture.plum -o $SCRATCH/dist` -> the dir holds index.html, mq_js_bundle.js, plum_bridge.js, plum-game.wasm, plum.wasm, preload.txt listing the six `sprites/*.rxx`
|
|
186
|
+
**Review Focus:** `reentrancy`: Rust → JS → Plum → JS → Rust `pg_*` calls hold no RefCell borrow or `&mut` context across the JS call; `str_marshalling`: Plum Str → host memory via `pg_alloc` + strLen/strByte, host → Plum via strNew/strSetByte; `ready_poll`: nothing calls Plum before `plum_bridge_ready()`; `preload`: every listed file is fetched before init and served from memory to atlasLoad/mapLoad; `wasm32_build`: `cargo build --target wasm32-unknown-unknown --release` in plum-game passes; `cli_paths`: `plum game` finds plum-game via the plum-cli crate's compile-time repo path; `bridge_imports_complete`: plum_bridge.js defines every Game.plum extern plus printLn/crash.
|
|
187
|
+
**Test code:** a plum-cli integration test `game_web_test.rs` (may live in plum-cli/tests/) checking the dist file list for the e.g.; a node script `plum-game/web/bridge_check.mjs` that loads `plum.wasm` with stub `pg_*` functions and asserts every module import is satisfied (run with `node`).
|
|
188
|
+
**Steps:** 1 web.rs: `pg_*` exports per Backend method + `pg_alloc`, imports `plum_bridge_ready/init/update/draw`, preload via `load_file`, 2 main.rs: wasm32 loop variant, 3 plum_bridge.js miniquad plugin + index.html (based on kam_macroquad/web/index.html), 4 plum-cli game.rs + wiring in main.rs (command only), 5 README: `export fun` section (next to `extern`) and a "Games" section (`plum game`, `--web`, the entry contract, the Host API table, assets path), 6 root .gitignore `dist/`, 7 tests green; `./target/debug/plum game --web` on the fixture produces the dist.
|
|
189
|
+
**Notes:** copy `mq_js_bundle.js` from the macroquad crate in cargo's registry (find it via `cargo metadata` like kam_macroquad/web/build.sh). Don't edit plum-cli's hostImports (T2's).
|
|
190
|
+
|
|
191
|
+
## Package: viewer
|
|
192
|
+
**Probe entry:** run=./target/debug/plum test plum-game/kam
|
|
193
|
+
|
|
194
|
+
### T6 KaM map viewer (P1) · Complexity 5 (O-1) · Est. context 50k
|
|
195
|
+
**Owns:** plum-game/kam/Main.plum
|
|
196
|
+
**Components:** 2
|
|
197
|
+
**LOC owned:** 280
|
|
198
|
+
**Tests:** fast
|
|
199
|
+
**Kind:** design
|
|
200
|
+
**Model:** sonnet
|
|
201
|
+
**Est. time:** 5 min
|
|
202
|
+
**Consumes:** T1 export fun; T2 Game.plum, Vec2, Color
|
|
203
|
+
**Produces:** `export fun init() -> Viewer`, `export fun update(v: Viewer, dt: Float)`, `export fun draw(v: Viewer)`; pure helpers `Camera` record with `screenToWorld(p: Vec2) -> Vec2`, `worldToScreen(p: Vec2) -> Vec2`, `clampTo(w: Float, h: Float, sw: Float, sh: Float)`, `zoomAt(screen: Vec2, factor: Float)`, `visibleTiles(sw, sh) -> TileRange`
|
|
204
|
+
e.g. Camera(x: 100.0, y: 50.0, scale: 2.0).screenToWorld(Vec2(x: 20.0, y: 10.0)) == Vec2(x: 110.0, y: 55.0) and worldToScreen inverts it
|
|
205
|
+
e.g. zoomAt keeps the world point under the mouse fixed; scale stays in 0.25..4
|
|
206
|
+
e.g. visibleTiles for a 64×64 map at x=0,y=0,scale 1 on an 800×600 screen covers columns 0..25 and rows 0..18 (inclusive of the partial tile), clamped to the map
|
|
207
|
+
**Review Focus:** `only_visible_drawn`: draw loops over visibleTiles, not the whole map; `minimap_cached`: minimap colours computed once in init; `pan_speed_screen_space`: pan per second is constant on screen across zoom; `clamp`: the view never shows outside the map unless the map is smaller than the screen (then centred); `tests_dont_call_host`: test blocks use only pure helpers.
|
|
208
|
+
**Test code:** `test` blocks in Main.plum for the three e.g. lines, clamp, and minimap click → camera centre.
|
|
209
|
+
**Steps:** 1 failing tests, 2 Camera + TileRange + helpers, 3 init (atlasLoad Tileset/Trees_a, mapLoad, copy into Grid[Int]s, minimap colours), update (keys, wheel, minimap click), draw (setCamera, tiles, objects, resetCamera, minimap, FPS), 4 `./target/debug/plum test plum-game/kam` green and `./target/debug/plum compile plum-game/kam/Main.plum` succeeds.
|
|
210
|
+
**Notes:** 32 world units per tile. A missing atlas/map (-1) draws a "assets not found" message instead of crashing.
|
|
211
|
+
|
|
212
|
+
## Package: incidental
|
|
213
|
+
**Probe entry:** run=cargo test --workspace
|
|
214
|
+
|
|
215
|
+
### T7 incidental files · Complexity 1 (O-1) · Est. context 5k
|
|
216
|
+
**Owns:** plum-game/Cargo.lock, plum-core/src/union_lower.rs, plum-cli/tests/game_web_test.rs, plum-game/web/bridge_check.mjs, plum-game/web/screenshot.mjs, plum-lsp/src/main.rs
|
|
217
|
+
**Components:** 1
|
|
218
|
+
**LOC owned:** 10
|
|
219
|
+
**Tests:** fast
|
|
220
|
+
**Kind:** mechanical
|
|
221
|
+
**Model:** haiku
|
|
222
|
+
**Est. time:** 1 min
|
|
223
|
+
**Consumes:** none
|
|
224
|
+
**Produces:** none
|
|
225
|
+
**Review Focus:** `lockfile`: plum-game/Cargo.lock is committed (binary crate); `union_lower`: T1's edit to union_lower.rs is the `is_export` field threading only.
|
|
226
|
+
**Test code:** none.
|
|
227
|
+
**Steps:** 1 coordinator commits these files once T1 and T3 are in.
|
|
228
|
+
**Notes:** bookkeeping for files T1 and T3 touch outside their Owns.
|
|
229
|
+
|
|
230
|
+
## Review Focus
|
|
231
|
+
- compiler (T1): encoding, seam. Risks: export wrapper ABI drift from extern's; Str helpers on views. Cases in T1.
|
|
232
|
+
- std (T2): contract. Risks: extern list drifting from the Design; stub masking real host fns.
|
|
233
|
+
- host (T3, T4, T5): seam, state, protocol. Risks: GC ref rooting across frames; JS re-entrancy; Str marshalling; wasm32 build breaking on wasmtime deps.
|
|
234
|
+
- viewer (T6): logic. Risks: off-by-one in visible range; zoom anchor math.
|
|
235
|
+
|
|
236
|
+
## Self-review
|
|
237
|
+
Design → tasks: export fun + Str bridge → T1; Host API, Key/Mouse, Vec2/Color, import stubs → T2; assets → T3; Backend/native/recorder/macroquad → T4; web, `plum game`, README → T5; P1 viewer → T6. Shared files: plum-game/src/lib.rs (T3 → T4 → T5, sequential edges), plum-game/src/main.rs (T4 → T5), plum-cli/src/main.rs (T2 → T5, T5 only adds the command wiring). Audio deferred (Design ruling). Playwright isn't installed: the browser check is the coordinator's manual Chrome run, plus T5's node import check.
|
docs/plans/2026-11-02-game-host.progress.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
- start eb0d1281d0f034394e5f8c60796c2b253c7a947a
|
|
2
|
+
Ruling: AI orchestrator - user said "go ahead with the game plan" - user wanted to approve first
|
|
3
|
+
Ruling: this run = milestone 1 (H1 host, H2 browser, P1 map viewer); P2-P5 later - each later task needs this host layer working - user expected full port
|
|
4
|
+
Ruling: export fun keyword instead of hardcoded init/update/draw exports - user rejects hardcoded special cases - one more keyword
|
|
5
|
+
Ruling: plum-game vendors minimal rxx/map decoders rather than path-depending on kam_macroquad (a bin crate with crate:: paths) - decoders are small - drift from KaM format fixes
|
|
6
|
+
Ruling: audio deferred to P2 - needs KaM sfx decoding - milestone without sound
|
|
7
|
+
Ruling: no Playwright (not installed); browser check done manually in Chrome + node import check - avoids a download - weaker automated coverage
|
|
8
|
+
- unblocked: dispatched T0 T1 T2 T3 at 13:40:07; waiting on fixes: none
|
|
9
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=0.0
|
|
10
|
+
- tests full passed=1 failed=0 skipped=0 seconds=62.4
|
|
11
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=0.1
|
|
12
|
+
Ruling: no T0 commit; plum-game Cargo.toml/lib.rs belong to T3, test-cmd.json widened to workspace + plum test + plum-game - T0 had no stubs to write - none
|
|
13
|
+
- commit T3 010dcc3b9a plum-game crate + KaM asset decoders
|
|
14
|
+
Ruling: plan over the 15000-char cap left as is - every Design ruling is a cross-task contract; trimming risks drift - larger prime per agent
|
|
15
|
+
Ruling: letter keys are Key.KeyA..Key.KeyZ (single-capital consts parse as generics) - avoids a grammar change mid-run - one naming wart in Game.plum
|
|
16
|
+
- commit T2 1e951682d8 std Game API + import stubs
|
|
17
|
+
- commit T1 7cf0f18a66 export fun + Str bridge
|
|
18
|
+
- commit T7 8dc69a552c incidental files
|
|
19
|
+
- unblocked: dispatched T4 T6 at 13:50:10; waiting on fixes: none
|
|
20
|
+
Ruling: T6 sent to the T2 agent without a relevance poll - it wrote Game.plum, the clear best fit - none
|
|
21
|
+
- commit T6 f689ac73a7 KaM map viewer (P1)
|
|
22
|
+
- commit T4 43b65ab9b2 native host: Backend, wasmtime binding, macroquad, recorder
|
|
23
|
+
- unblocked: dispatched T5 at 13:56:40; waiting on fixes: none
|
|
24
|
+
- commit T4 b8586116a7 native host: Backend, wasmtime binding, macroquad, recorder
|
|
25
|
+
Ruling: mapLoad/atlasLoad path '..' escape left open - Plum programs are trusted local code reading local assets - a hostile .wasm could read files under the user's account
|
|
26
|
+
- commit T5 8ffa113b8b web bridge + `plum game` + README
|
|
27
|
+
- commit T7 5617070cbb incidental files
|
|
28
|
+
- commit T4 f77dbeaa4b native host: Backend, wasmtime binding, macroquad, recorder
|
|
29
|
+
- commit T5 79646b4c71 web bridge + `plum game` + README
|
|
30
|
+
- commit T5 63a5080979 web bridge + `plum game` + README
|
|
31
|
+
- commit T7 908c08de12 incidental files
|
|
32
|
+
- tests full passed=222 failed=1 skipped=0 seconds=16.3
|
|
33
|
+
- tests full passed=222 failed=1 skipped=0 seconds=12.3
|
|
34
|
+
Ruling: checker warning/error for discarded non-Unit results deferred to the user - it's a language-design change (affects every chained/builder call) - another silent-discard bug could slip in meanwhile
|
|
35
|
+
Ruling: final-review finding "run_tests bad args" is stale - those were agents' malformed sl invocations; the full suite ran after and surfaced one real failure (readme snippet test), fixed - none
|
|
36
|
+
- commit T4 1163e3e7e8 native host: Backend, wasmtime binding, macroquad, recorder
|
|
37
|
+
- commit T7 f82e1e2e52 incidental files
|
|
38
|
+
- tests full passed=1129 failed=0 skipped=0 seconds=67.3
|