plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
1140b98
— Peter John
2026-11-02T17:01:16+05:30
README: browser-only games flow, std/Scene guidance, headless runner, browser check
README.md
CHANGED
|
@@ -146,7 +146,7 @@ plum test file.plum dir/ # run the `test` blocks of files and directories (
|
|
|
146
146
|
plum compile file.plum # write file.wasm
|
|
147
147
|
plum build file.plum # embed the module into a native executable
|
|
148
148
|
plum format file.plum # rewrite in place (--check, --stdin)
|
|
149
|
-
plum game file.plum #
|
|
149
|
+
plum game file.plum # serve a game and open it in the browser (see Games); -o DIR writes a static build
|
|
150
150
|
plum editor helix|vscode # install highlighting and the language server
|
|
151
151
|
```
|
|
152
152
|
|
|
@@ -158,12 +158,7 @@ plum editor helix|vscode # install highlighting and the language server
|
|
|
158
158
|
cargo run -q -p plum-cli -- game plum-examples/game.plum # or, after `cargo install --path plum-cli`: plum game plum-examples/game.plum
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
Arrow keys or WASD steer the ship, collect the orbs, dodge the mines; Enter or R restarts after the game is over.
|
|
162
|
-
|
|
163
|
-
```sh
|
|
164
|
-
plum game --web plum-examples/game.plum -o /tmp/orb-run
|
|
165
|
-
python3 -m http.server -d /tmp/orb-run 8080 # then open http://localhost:8080
|
|
166
|
-
```
|
|
161
|
+
This compiles the game, serves it on `http://127.0.0.1:8080/` (any free port if that one is taken) and opens it in your default browser. It needs a browser with WebGPU: Chrome or Edge, Safari 26+ or Firefox. Arrow keys or WASD steer the ship, collect the orbs, dodge the mines; Enter or R restarts after the game is over. Press a key first: browsers start audio only after a user gesture.
|
|
167
162
|
|
|
168
163
|

|
|
169
164
|
|
|
@@ -1042,11 +1037,13 @@ Every module that imports `std/Str` also exports four helpers for hosts that can
|
|
|
1042
1037
|
|
|
1043
1038
|
## Games
|
|
1044
1039
|
|
|
1045
|
-
|
|
1040
|
+
Games run in the browser on Plum's own WebGPU renderer (`plum-game/web/plum_bridge.js`): the Plum module calls the bridge directly, one extern call per draw, and the bridge batches everything into one buffer upload per frame. WebGPU is required (Chrome or Edge, Safari 26+, Firefox); there is no fallback.
|
|
1041
|
+
|
|
1042
|
+
`plum game file.plum [--assets DIR] [--port N] [--no-open]` compiles the game, serves it on 127.0.0.1 (port 8080, else any free port; `--port` picks one), prints the URL and opens the default browser unless `--no-open` is given. Ctrl-C stops it. `plum game file.plum -o dist [--assets DIR]` writes a static build instead and exits: `index.html`, `plum_bridge.js`, `plum.wasm`, `preload.txt` and an `assets/` copy of the whole asset directory (every file in it is listed in `preload.txt` and fetched before the first frame). Host that directory on any static file server; the browser needs wasm GC support. The asset root defaults to `assets` beside the source file.
|
|
1046
1043
|
|
|
1047
|
-
`plum
|
|
1044
|
+
`plum-examples/game.plum` is a small arcade game: steer a ship with the arrow keys or WASD, collect orbs, dodge mines (Enter restarts). It draws shapes, text and an animated sprite sheet (`plum-examples/assets/sprites.png`, generated by `plum-examples/tools/make_sprites.py`) and plays music and sound effects (`plum-examples/assets/*.wav`, generated by `plum-examples/tools/make_sounds.py`); both generators live outside the asset dir so they are not shipped. For a bigger game built on this engine see the Knights and Merchants port in the sibling repo `../kam_plum` (`./run.sh` there). Its rules are plain functions with `test` blocks, so `plum test plum-examples` covers them without a browser.
|
|
1048
1045
|
|
|
1049
|
-
`
|
|
1046
|
+
**`std/Scene`** is a scene-graph library written in Plum (see [`plum-examples/scene.plum`](plum-examples/scene.plum)): a tree of `Node`s (group, sprite, rect, circle, text) under a `Stage`, with parent-relative position, rotation, scale and colour, per-node `act` behaviours, a camera and hit testing. Use it for UI and other hierarchical scenes, where moving a parent should move everything in it. For a large world, keep the game objects in flat records and lists instead (for example `enum Entity = Unit | House` with an `update` and a `draw` that the enum dispatches to its variants) and draw them directly through `std/Game`; a tree of thousands of nodes costs a closure call and a matrix composition per node every frame.
|
|
1050
1047
|
|
|
1051
1048
|
A game declares three exports. `G` is any record and holds all game state (there are no globals):
|
|
1052
1049
|
|
|
@@ -1056,21 +1053,21 @@ export fun update(g: G, dt: Float)
|
|
|
1056
1053
|
export fun draw(g: G)
|
|
1057
1054
|
```
|
|
1058
1055
|
|
|
1059
|
-
The
|
|
1056
|
+
The page calls `init` once after the assets are loaded, then `update` and `draw` every frame. A game imports `std/Game`, `std/Vec2` and `std/Color`; pure logic can import `std/Vec2` and `std/Color` alone. Under `plum test` and `plum run` the host functions are stubs that trap with `host function 'X' is not available here`, so `test` blocks work as long as they don't call the host.
|
|
1060
1057
|
|
|
1061
|
-
**Headless
|
|
1058
|
+
**Headless tests:** the `plum-game` crate (a standalone crate, wasmtime only) runs a compiled game with no browser against a `Recorder` backend that logs every host call as a string and answers input from scripted values, so `cargo test --manifest-path plum-game/Cargo.toml` checks games' draw-call sequences (`plum-game/tests/host_test.rs`). `plum-game <wasm> [--assets DIR] [--frames N]` does the same from the command line and prints the host calls of the last frame.
|
|
1062
1059
|
|
|
1063
|
-
**Browser check:** `cargo run --manifest-path plum-game/web-test/Cargo.toml -- plum-examples/game.plum --screenshot /tmp/orb-run.png` builds the web version, serves it and drives headless Chromium through [playwright-rs](https://crates.io/crates/playwright-rs): it fails if the canvas stays blank or stops changing, or the page logs an error, and reports whether audio started. One-time setup: `cargo run --manifest-path plum-game/web-test/Cargo.toml --example install-browsers`. See [`plum-game/web-test/README.md`](plum-game/web-test/README.md).
|
|
1060
|
+
**Browser check:** `cargo run --manifest-path plum-game/web-test/Cargo.toml -- plum-examples/game.plum --screenshot /tmp/orb-run.png` builds the static web version, serves it and drives headless Chromium (software WebGPU) through [playwright-rs](https://crates.io/crates/playwright-rs): it fails if the canvas stays blank or stops changing, or the page logs an error, and reports whether audio started. It also takes `--assets DIR`, `--require-audio`, `--measure SECS` and `--headed`. One-time setup: `cargo run --manifest-path plum-game/web-test/Cargo.toml --example install-browsers`. See [`plum-game/web-test/README.md`](plum-game/web-test/README.md); `docs/game.png` above was made with `--screenshot`. [`plum-game/web-test/BENCHMARK.md`](plum-game/web-test/BENCHMARK.md) records the renderer benchmark that led to this design.
|
|
1064
1061
|
|
|
1065
|
-
**Asset root:**
|
|
1062
|
+
**Asset root:** textures and files are read relative to the asset root. A path that escapes the root (`..` or absolute) fails like a missing file: `-1`, `""` or `false`. The headless runner's root is `--assets` or `assets` beside the wasm; it reads image headers for `textureWidth`/`textureHeight` and records sounds without decoding them.
|
|
1066
1063
|
|
|
1067
|
-
**Sounds:**
|
|
1064
|
+
**Sounds:** the browser decodes every `.wav` and `.ogg` file of the asset root before `init`, and `soundLoad` looks the decoded sound up by path with the same path rules as `textureLoad`. Browsers only start audio after a user gesture (a key press or click); sounds played before that are silent, and the music of the example begins once you press a key.
|
|
1068
1065
|
|
|
1069
1066
|
**Host API** (`std/Game`; texture and sound handles are `-1` on failure; coordinates are world units once a camera is set):
|
|
1070
1067
|
|
|
1071
1068
|
| function | meaning |
|
|
1072
1069
|
|---|---|
|
|
1073
|
-
| `screenWidth()`, `screenHeight()`, `frameTime()` |
|
|
1070
|
+
| `screenWidth()`, `screenHeight()`, `frameTime()` | canvas size in pixels, last frame time in seconds |
|
|
1074
1071
|
| `clear(c)` | fill the screen |
|
|
1075
1072
|
| `drawRect(x, y, w, h, c)`, `drawCircle(x, y, r, c)`, `drawLine(x1, y1, x2, y2, thickness, c)`, `drawText(s, x, y, size, c)` | shapes and text (`x, y` is the text's top-left, a circle's centre) |
|
|
1076
1073
|
| `setCamera(x, y, scale)`, `resetCamera()` | world point `(x, y)` maps to the screen's top-left, one world unit is `scale` pixels |
|