plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
34b61a0
— Peter John
2026-10-26T10:45:12+05:30
docs(plans): game plan becomes browser-first simplified KaM port
- docs/plans/2026-10-26-game-engine.md +141 -93
docs/plans/2026-10-26-game-engine.md
CHANGED
|
@@ -1,121 +1,169 @@
|
|
|
1
|
-
# Plum for games
|
|
1
|
+
# Plum for games: browser-first macroquad host, port of a simplified KaM
|
|
2
2
|
|
|
3
|
-
**Status:** draft. Planned only
|
|
4
|
-
**
|
|
5
|
-
**Goal:**
|
|
6
|
-
**Architecture:**
|
|
7
|
-
**Tech stack:** Rust, macroquad, wasmtime (GC, multi-value and SIMD enabled), wasm-encoder.
|
|
3
|
+
**Status:** draft. Planned only and not run; the user has more ideas to fold in first.
|
|
4
|
+
**Depends on:** `2026-10-26-zig-generics-and-numbers.md`, which supplies the native `i32 i64 f32 f64 bool` types with i32/f32 literal defaults, duck-typed per-type-copy generics, no traits (enums plus duck-typed operator methods), and `Bytes`. That plan runs first.
|
|
5
|
+
**Goal:** Port `../kam_macroquad`, a 98k-line Rust/macroquad remake of Knights and Merchants that already runs in the browser via wasm, to Plum as a **simplified** game. There is no mission scripting (rhai) and the rules are simpler, but animations and sounds stay identical. It runs in the browser first.
|
|
6
|
+
**Architecture:** the Rust/macroquad host keeps asset decoding, audio and the window, compiled to wasm32 for the web or native. The Plum wasm-gc module owns game state, rules, pathfinding, AI, animation timing and per-frame draw calls. On the web the two modules talk through JS (miniquad plugin), and natively through wasmtime.
|
|
8
7
|
**Detail:** tests
|
|
9
8
|
|
|
10
9
|
## Design
|
|
11
10
|
|
|
12
11
|
What the user decided:
|
|
13
|
-
- macroquad is the host
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
- `
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
12
|
+
- macroquad is the host, and Plum handles game logic plus rendering.
|
|
13
|
+
- `struct` value types and simd128 are wanted.
|
|
14
|
+
- Port KaM, with no scripting engine and simpler rules but the same animations and sounds.
|
|
15
|
+
- It must work in the browser, as the Rust version does today.
|
|
16
|
+
|
|
17
|
+
Survey of `../kam_macroquad`:
|
|
18
|
+
- 98k LOC, 356 structs, 180 enums, 864 `match`.
|
|
19
|
+
- ~1.9k `&mut self` methods, 967 tests, ~350 macroquad calls.
|
|
20
|
+
- Collections: `Vec` ×997, `Option` ×814, `HashMap`/`Set`/`BTree`/`VecDeque`/`BinaryHeap` ×145.
|
|
21
|
+
- Closures and iterator chains ×~3.7k each.
|
|
22
|
+
- Numbers: `i32` ×5.2k, `f32` ×1.8k, `u8`/`u16`/`u32`/`usize` ×5.7k (mostly binary asset parsing).
|
|
23
|
+
- Traits are small: 7 traits, 63 impls, 40 `dyn`.
|
|
24
|
+
- The asset side uses flate2, postcard, RON and serde.
|
|
25
|
+
|
|
26
|
+
1. **Split of responsibilities.** Ruling: the host keeps asset decoding (`.rx` sprites/atlases, `kamdat`, flate2, map files), the serde/postcard/RON formats and audio, because that removes almost all the unsigned/binary-parsing and serialization code from the port. The cost is that the host stays Rust and is game-specific.
|
|
27
|
+
- The host exposes **handles**: a sprite is `(atlas: i32, rect index: i32)`, and sounds and maps are `i32` ids. Animations stay identical because Plum gets the same frame tables, read through host imports.
|
|
28
|
+
2. **Browser host first.**
|
|
29
|
+
- `plum-game` is a Rust crate on macroquad. For the web it compiles to wasm32 with macroquad's JS loader.
|
|
30
|
+
- A `plum_bridge.js` miniquad plugin instantiates Plum's wasm-gc module, wires Plum's `extern fun` imports to functions exported from the macroquad wasm, and has macroquad's loop call Plum's `init`, `update(g, dt: f32)` and `draw(g)`.
|
|
31
|
+
- The native build runs the same Plum module under wasmtime (GC, multi-value and SIMD on), with the same extern API.
|
|
32
|
+
- Ruling: game state lives in the Plum `Game` ref, held by the host and passed to every call, so no mutable globals are needed.
|
|
33
|
+
3. **Extern API (`plum-std/Game.plum`).**
|
|
34
|
+
- Window: `screenSize() -> Vec2` and `frameTime() -> f32`.
|
|
35
|
+
- Drawing: `clear(c)`, `drawSprite(atlas, rect, x, y: f32, flipX: bool, tint: Color)`, `drawRect`, `drawLine`, `drawText(s: Str, x, y, size: f32, c)`, `setCamera2D(target: Vec2, zoom: f32)` and `resetCamera()`.
|
|
36
|
+
- Input: `isKeyDown(k: Key)`, `isKeyPressed`, `mousePos() -> Vec2`, `isMouseDown(b: MouseButton)` and `mouseWheel() -> f32`.
|
|
37
|
+
- Audio: `playSound(id: i32, x, y: f32)` (positional, as in KaM), `playMusic(id)` and `stopMusic()`.
|
|
38
|
+
- Assets, as host lookups by name: `atlasId`, `animFrames(unitKind, action, dir) -> Bytes` (packed frame indices) and `animStep`, `loadMap(name) -> MapData`, which copies terrain into Plum `Array[i32]`s once.
|
|
39
|
+
- `Color` and `Vec2` are structs flattened across the boundary. `Key` and `MouseButton` are enums matching macroquad's codes.
|
|
40
|
+
4. **Language additions this port needs** (beyond the Zig-generics plan and items 5–7 below):
|
|
41
|
+
- **Unboxed typed arrays.** With copies per type, `Array[i32]` and `Array[f32]` lower to native `array (mut i32)` and `array (mut f32)`. These are for tile grids, fog of war, passability and path costs. Add `Array.filled(n, v)` and a 2D index helper.
|
|
42
|
+
- **Collections in std:** `Set[T]`, `Deque[T]` (ring buffer) and `Heap[T]` (binary min-heap with a key fn, for A*), all Plum code over `Array`.
|
|
43
|
+
- **Constants:** large top-level constant tables must parse and be usable in expressions. A probe file with `LIMIT = 10` followed by a `type` failed to parse (cause not yet confirmed), and the notes record a SCREAMING_CASE constant gotcha. The fix needs tests with 1k-entry tables.
|
|
44
|
+
- **Seeded RNG** (`Random(seed: i64)` with xorshift/PCG) for deterministic AI and simulation.
|
|
45
|
+
- Lazy iteration is deferred. Hot loops use `for … range`, and it's revisited only if profiling shows `map`/`filter` allocation in `update`.
|
|
46
|
+
5. **Value structs** (`struct Vec2(x: f32, y: f32)`, `Color`, `TilePos(x: i32, y: i32)`): flattened locals and params, multi-value returns, copy semantics and in-place field assignment. They are boxed only inside `List`/`Map`/`Try`, and by then they belong to a generic copy, so a `List[Vec2]` copy stores them as GC structs.
|
|
47
|
+
6. **Duck-typed operators** on structs (`add`/`sub`/`mul`/`neg`/`eq`): `pos + dir * speed`.
|
|
48
|
+
7. **SIMD (late):** `f32x4` (wasm v128) and `Mat4`/`Vec4` in `plum-std/Math.plum`. KaM is 2D, so this is low priority for the port and kept for future 3D work.
|
|
49
|
+
8. **Port scope (simplified KaM):**
|
|
50
|
+
- Kept:
|
|
51
|
+
- map and terrain rendering with camera and zoom;
|
|
52
|
+
- units with directional walk/work/fight animations and their sounds;
|
|
53
|
+
- A* pathfinding on the tile grid;
|
|
54
|
+
- buildings placed and constructed with their animations;
|
|
55
|
+
- a simplified economy: woodcutter → sawmill → storehouse, serfs carrying wares;
|
|
56
|
+
- simple melee combat;
|
|
57
|
+
- a basic AI that builds and attacks;
|
|
58
|
+
- selection and commands with the mouse;
|
|
59
|
+
- minimap;
|
|
60
|
+
- win and lose conditions.
|
|
61
|
+
- Dropped: rhai mission scripts, campaign/mission scripting, multiplayer/results screens, the map editor, the full KaM economy tree and saves (maybe Json later).
|
|
62
|
+
9. **Out of scope:** WebGPU, Plum→shader compilation, porting the asset decoders, and threads.
|
|
46
63
|
|
|
47
64
|
Success criteria:
|
|
48
|
-
-
|
|
65
|
+
- The simplified game runs in Chrome/Firefox via `plum game --web`, served as a static folder, at 60 fps on a 128×128 map with 300 units.
|
|
66
|
+
- Animations and sounds match the Rust version's for the kept unit and building types (side-by-side capture).
|
|
49
|
-
-
|
|
67
|
+
- Zero GC allocation in `update` steady-state outside unit spawn and death, measured with a host GC counter in the native build.
|
|
50
|
-
- `Mat4 * Vec4` uses `f32x4` instructions (check the wasm with `wasm-tools print`).
|
|
51
68
|
|
|
52
69
|
## Global constraints
|
|
53
70
|
|
|
54
|
-
-
|
|
71
|
+
- Both suites (`cargo test --workspace`, `plum test plum-std plum-examples`) are green after every task.
|
|
55
|
-
- typeIds: Bool box 7 (from the built-in Bool work), f32 8, i32 9, i32 10, f32x4 box 11. Declared types still start at 16.
|
|
56
|
-
- Enable wasmtime `wasm_simd`, `wasm_multi_value` and `wasm_gc` in every host.
|
|
57
|
-
- The
|
|
72
|
+
- The `plum-game` crate builds for `wasm32-unknown-unknown` (web) and native. CI runs the native headless tests.
|
|
73
|
+
- The asset files are read from `../kam_macroquad/assets` through the host. They are never copied into this repo, because of licensing.
|
|
74
|
+
- Every Plum API uses only `i32 i64 f32 f64 bool` scalars. There are no unsigned types.
|
|
58
75
|
|
|
59
|
-
##
|
|
76
|
+
## Tasks
|
|
60
77
|
|
|
61
78
|
```mermaid
|
|
62
79
|
flowchart LR
|
|
63
|
-
|
|
80
|
+
L1[typed arrays + collections + consts + RNG]
|
|
81
|
+
L2[struct value types]
|
|
82
|
+
L3[duck-typed operators]
|
|
83
|
+
H1[plum-game host native + Game.plum]
|
|
84
|
+
H2[browser bridge]
|
|
85
|
+
P1[map + camera]
|
|
86
|
+
P2[units + anims + sounds]
|
|
87
|
+
P3[pathfinding]
|
|
88
|
+
P4[buildings + economy]
|
|
89
|
+
P5[combat + AI + win/lose]
|
|
64
|
-
|
|
90
|
+
S1["f32x4 + Math.plum (later)"]
|
|
91
|
+
%% taskgraph
|
|
92
|
+
L1 --> L2
|
|
93
|
+
L2 --> L3
|
|
65
|
-
|
|
94
|
+
L2 --> H1
|
|
66
|
-
|
|
95
|
+
H1 --> H2
|
|
96
|
+
H2 --> P1
|
|
97
|
+
L3 --> P1
|
|
67
|
-
|
|
98
|
+
P1 --> P2
|
|
99
|
+
P2 --> P3
|
|
100
|
+
P3 --> P4
|
|
101
|
+
P4 --> P5
|
|
102
|
+
L3 --> S1
|
|
68
103
|
```
|
|
69
104
|
|
|
105
|
+
### L1 typed arrays, collections, constants, RNG
|
|
70
|
-
|
|
106
|
+
- Native `Array[i32]`/`Array[f32]` (wasm-inspected), `Array.filled` and the 2D helper.
|
|
107
|
+
- `Set`, `Deque` and `Heap` in std.
|
|
108
|
+
- Fix top-level constant parsing and use.
|
|
109
|
+
- `Random(seed)`.
|
|
71
110
|
|
|
111
|
+
Tests:
|
|
112
|
+
- `array_i32_native` (the wasm declares `array (mut i32)`);
|
|
113
|
+
- `heap_pop_order` on 1000 random keys;
|
|
72
|
-
``
|
|
114
|
+
- `deque_wraparound`;
|
|
73
|
-
flowchart LR
|
|
74
|
-
|
|
115
|
+
- `set_dedup`;
|
|
75
|
-
|
|
116
|
+
- `const_table_1000_entries`;
|
|
117
|
+
- `random_seed_deterministic` (same seed gives the same first 100 values).
|
|
118
|
+
|
|
76
|
-
|
|
119
|
+
### L2 struct value types
|
|
120
|
+
Grammar `struct`, flattened lowering, multi-value returns, copy semantics, field assignment, recursion rejected. Tests:
|
|
77
|
-
|
|
121
|
+
- `struct_copy_semantics`;
|
|
122
|
+
- `struct_multi_value_return`;
|
|
123
|
+
- `struct_in_list_roundtrip`;
|
|
124
|
+
- `struct_eq_fieldwise`;
|
|
125
|
+
- `recursive_struct_rejected`.
|
|
126
|
+
|
|
127
|
+
### L3 duck-typed operators
|
|
128
|
+
`a + b` → `a.add(b)` and so on, resolved per instantiation. Tests:
|
|
78
|
-
|
|
129
|
+
- `vec2_add_mul_scalar`;
|
|
130
|
+
- `neg`;
|
|
131
|
+
- `missing_operator_method_error` with the use-site chain.
|
|
132
|
+
|
|
79
|
-
|
|
133
|
+
### H1 `plum-game` native host + Game.plum
|
|
134
|
+
- A macroquad + wasmtime crate with the `init`/`update`/`draw` loop.
|
|
135
|
+
- Game.plum externs as in Design item 3.
|
|
136
|
+
- Asset handles backed by kam_macroquad's existing loaders (reused as a path dependency on its asset modules).
|
|
80
|
-
|
|
137
|
+
- `plum game <file>`.
|
|
138
|
+
|
|
81
|
-
|
|
139
|
+
Tests: a headless mock recorder for call order and args; `color_flattened`; `mousePos_multi_value`; `anim_frames_match_rust` for 3 unit kinds.
|
|
82
|
-
%% taskgraph
|
|
83
|
-
T1 --> T2
|
|
84
|
-
T2 --> T3
|
|
85
|
-
T3 --> T4
|
|
86
|
-
T4 --> T5
|
|
87
|
-
T3 --> T6
|
|
88
|
-
T5 --> T7
|
|
89
|
-
T6 --> T7
|
|
90
|
-
T7 --> T8
|
|
91
|
-
```
|
|
92
140
|
|
|
93
|
-
###
|
|
94
|
-
|
|
141
|
+
### H2 browser bridge
|
|
142
|
+
- `plum_bridge.js` miniquad plugin.
|
|
143
|
+
- `plum game --web <file>` writes `dist/` (index.html, the macroquad wasm, the plum wasm, the bridge).
|
|
95
144
|
|
|
96
|
-
|
|
97
|
-
In the checker, an expected sized type flows into literals. In codegen, literals are emitted at that width. **Tests:** `x: f32 = 1.5` gets no Float→f32 error; `drawRect(10, 20, 32, 32)` with f32 params; `v.x * 0.5`; overflow `i32 = 3000000000` is an error naming the type; an unannotated `1.5` is still Float.
|
|
145
|
+
Check: a demo scene (sprites, sound, input) runs in Chrome and Firefox. Playwright headless-Chrome smoke test: canvas pixels change across frames and there are no console errors.
|
|
98
146
|
|
|
99
|
-
###
|
|
100
|
-
|
|
147
|
+
### P1 map + camera
|
|
148
|
+
Load a KaM map via the host, terrain into `Array[i32]`, tile rendering, camera pan/zoom, minimap. Check: renders the same as the Rust version on 2 maps (screenshot diff tolerance).
|
|
101
149
|
|
|
102
|
-
###
|
|
103
|
-
|
|
150
|
+
### P2 units + animations + sounds
|
|
151
|
+
Unit structs, directional animation state machine, walk interpolation, selection, positional sounds. Tests: the animation frame sequence for walk dir 3 equals the Rust table, and the sound fires on the step frames.
|
|
104
152
|
|
|
105
|
-
###
|
|
106
|
-
|
|
153
|
+
### P3 pathfinding
|
|
154
|
+
A* over passability with `Heap`, path smoothing as in `src/pathfinding.rs` (simplified), unit move orders. Tests: shortest-path length on 5 fixture grids matches the Rust implementation, and 300 units repathing stays under 2 ms per frame (native).
|
|
107
155
|
|
|
108
|
-
###
|
|
109
|
-
|
|
156
|
+
### P4 buildings + economy
|
|
157
|
+
Placement, construction stages with animations, woodcutter → sawmill → storehouse, serfs carrying wares. Tests: a deterministic 2-minute sim produces N planks (fixed seed); building stages advance at the Rust timings.
|
|
110
158
|
|
|
111
|
-
###
|
|
112
|
-
|
|
159
|
+
### P5 combat + AI + win/lose
|
|
160
|
+
Melee combat with animations and sounds, a basic AI that builds and attacks, and victory/defeat. Check: an AI-vs-AI game ends deterministically for a fixed seed. The full game plays in the browser at the success-criteria scale.
|
|
113
161
|
|
|
114
|
-
###
|
|
162
|
+
### S1 f32x4 + Math.plum (later)
|
|
115
|
-
|
|
163
|
+
v128, lane ops, `Vec4`/`Quat`/`Mat4`. Tests: `mat4_inverse_roundtrip`, and `simd_ops_emitted`. Not needed for the 2D port.
|
|
116
164
|
|
|
117
|
-
## Open questions
|
|
165
|
+
## Open questions
|
|
118
166
|
|
|
119
|
-
- Will the user's further ideas change
|
|
167
|
+
- Will the user's further ideas change scope or order? This is still a draft.
|
|
168
|
+
- Which unit and building types make the "kept" list? The default above is the wood chain plus militia/axe fighters.
|
|
120
|
-
-
|
|
169
|
+
- Is reusing kam_macroquad's asset modules as a path dependency acceptable, or should the host vendor a minimal decoder?
|
|
121
|
-
- Should struct field mutation require `var` bindings, or is every binding mutable as today?
|