plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
f5ce2ba
— Peter John
2026-11-03T21:20:05+05:30
docs(plans): state/view parity review plan; pure-scene-enums plan notes
docs/plans/2026-11-03-pure-scene-enums.review.md
CHANGED
|
@@ -21,6 +21,8 @@ The mutable `std/game/Scene`, `game_scene.plum`, `scene.plum` and the trait exam
|
|
|
21
21
|
- `own record Node` holds the fields every node has: id, position, size, origin, scale, rotation, colour, visible, touchable and actions, plus `kind: Kind`.
|
|
22
22
|
- `Kind` is an inline-variant enum: `Group(children: Slice[Node])`, `Sprite(texture region)`, `Label(text, size)`, `Button(text, textures, pressed, over)` and `Custom(actor: Actor)`.
|
|
23
23
|
- `n.x` stays a plain field read, and there's no 14-field header to repeat.
|
|
24
|
+
- Added mid-run (user): the position, size, origin, scale and rotation fields come from an embedded `record Transform`, which also carries `localMatrix()` and the box hit test.
|
|
25
|
+
- Trees and child lists are written with `Slice(a, b, c)` / `List(a, b)` instead of push chains. Plum has no `[a, b]` literal.
|
|
24
26
|
- Narrowing to a std kind is a `match` on `n.kind`.
|
|
25
27
|
- **The game's kinds are records behind a behaviour-only trait** (Rust's `Box<dyn Actor>`).
|
|
26
28
|
- `trait Actor` in std has no fields, only two methods:
|
docs/plans/2026-11-03-state-view-parity.review.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Functional state/view at feature parity with PureScene: review plan
|
|
2
|
+
|
|
3
|
+
**Builds on:** master after `2026-11-03-pure-scene-enums` (its T1–T6; T7 dropped).
|
|
4
|
+
**Goal (user, 2026-11-03):** the most functional approach, where a frame is a recursive value built from state and walked recursively, with the same features as PureScene. Performance is measured and reported, not chased: matching the mutable scene isn't a goal ("otherwise I would just stick with mutate").
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
`game_state_view` + `std/game/Picture` is already the functional style:
|
|
8
|
+
- `State` holds plain values and cons lists;
|
|
9
|
+
- `step(state, msg)` returns a new state;
|
|
10
|
+
- `view(state)` builds a recursive `SceneNode` tree every frame;
|
|
11
|
+
- `Picture.draw` / `bounds` / `hitPath` recurse over it.
|
|
12
|
+
|
|
13
|
+
Nothing is mutated. What it lacks compared with PureScene:
|
|
14
|
+
- timed actions;
|
|
15
|
+
- buttons with press/enter/exit/release/click events;
|
|
16
|
+
- pointer capture and event routing by id.
|
|
17
|
+
|
|
18
|
+
This plan adds those as pure functions over values, ports the Orb Run state/view game to use them, and benchmarks it. It answers whether PureScene is still needed.
|
|
19
|
+
|
|
20
|
+
## Folders (user, 2026-11-03)
|
|
21
|
+
The three scene styles currently share `plum-std/game/`, which is confusing. Each style gets its own folder; the shared basics stay where they are.
|
|
22
|
+
|
|
23
|
+
| Folder | Contents | Import |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `plum-std/game/` | shared: `Game` (host API), `Vec2`, `Color`, `Affine`, and the mutable `Scene` | `std/game/Scene` (unchanged) |
|
|
26
|
+
| `plum-std/game/pure/` | PureScene, SceneTree, SceneInput, Actions | `std/game/pure/PureScene`, ... |
|
|
27
|
+
| `plum-std/game/functional/` | Picture, plus the new Anim and Ui | `std/game/functional/Picture`, ... |
|
|
28
|
+
| `plum-examples/games/pure/` | `game_pure_scene.plum` | |
|
|
29
|
+
| `plum-examples/games/functional/` | `game_state_view.plum` | |
|
|
30
|
+
|
|
31
|
+
The mutable examples (`game.plum`, `game_scene.plum`, `scene.plum`, `trait_scene.plum`) stay at the top of `plum-examples`. Imports in std, examples, web-test fixtures, host tests, web checks and the README are updated in the same part, and an import of an old path is a compile error. This is done first, as one mechanical part, so nothing else moves twice.
|
|
32
|
+
|
|
33
|
+
## The design
|
|
34
|
+
- **Animations are functions of time, not stepped state.**
|
|
35
|
+
- New `std/game/Anim`: `enum Anim = MoveBy(dx, dy, secs) | RotateBy(radians, secs) | ScaleTo(sx, sy, secs) | Delay(secs) | Seq(steps: List[Anim]) | Par(steps: List[Anim]) | Forever(anim: Anim)`.
|
|
36
|
+
- `Anim.at(anim, t) -> Pose` gives the pose offset `t` seconds after the start; `Anim.length(anim)` gives the duration, which is infinite for `Forever`.
|
|
37
|
+
- The state stores only when an animation started, so there's nothing to step, compact or copy, and no drift: `Forever` is `t` modulo the lap length, and `Seq` picks its segment by time.
|
|
38
|
+
- The view applies the pose as a `Transform` on the node.
|
|
39
|
+
- **UI is a pure function of pointer, state and picture.**
|
|
40
|
+
- `SceneNode` gains `Tagged(id: Int, child: SceneNode)`, and `Picture.hitId(node, x, y) -> Int?` returns the topmost tagged node under the point.
|
|
41
|
+
- New `std/game/Ui` holds `record UiState(pointerX, pointerY, down: Bool, hot: Int?, captured: Int?)`. `Ui.step(ui, input, hit: Int?)` returns the new `UiState` and a `List[UiEvent]`.
|
|
42
|
+
- It uses PureScene's rules:
|
|
43
|
+
- Press on down;
|
|
44
|
+
- Enter/Exit on move;
|
|
45
|
+
- Release on up;
|
|
46
|
+
- Click only on up over the node that captured the press;
|
|
47
|
+
- capture is dropped when the captured id is no longer in the picture.
|
|
48
|
+
- Button visuals come from the UI state: `Ui.pressed(ui, id)`, `Ui.hovered(ui, id)`. `Ui.button(id, label, rect, ui)` builds the button's `SceneNode`.
|
|
49
|
+
- Events are `UiEvent(id, kind)`, as in PureScene, and the game handles them in `step`.
|
|
50
|
+
- **The game shell stays thin.** `update` reads input into a `Msg`, then calls `step`, then `Ui.step` against `view(state)`, then `step` again for each `UiEvent`. Sound remains the only side effect.
|
|
51
|
+
|
|
52
|
+
## What you will get
|
|
53
|
+
- `std/game/Anim` and `std/game/Ui` with tests named after PureScene's. Button: press needs press-and-over, release elsewhere gives no click, drag off and back gives one click. Capture: stale capture dropped. Actions: zero-length, forever with no drift, sequence handover, parallel.
|
|
54
|
+
- `Picture` with `Tagged` and `hitId`. Existing Picture tests unchanged.
|
|
55
|
+
- `game_state_view.plum` using them:
|
|
56
|
+
- the restart button through `Ui` instead of the hand-written `onRestart` hit test;
|
|
57
|
+
- a title/game-over animation and the ship's hit flash through `Anim`;
|
|
58
|
+
- game rules and existing tests unchanged in value.
|
|
59
|
+
- `stress_state_view` fixture with tagged nodes, a few buttons and running animations, so the benchmark exercises the new parts.
|
|
60
|
+
- A benchmark table (headed Chromium, same session): mutable scene, PureScene, state/view before and after this plan. The plan reports the numbers. The only bar is that state/view keeps every frame well inside the 120 Hz budget (p95 under 4 ms on the M3 Pro).
|
|
61
|
+
- README: a "Functional scenes (state/view)" section showing the `State → view → draw` loop with Anim and Ui.
|
|
62
|
+
|
|
63
|
+
## What will be built
|
|
64
|
+
| Part | What it does | Size |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| Folders | move pure and functional modules and their example games into their folders; update every import and test path | S |
|
|
67
|
+
| Anim | `enum Anim`, `at`, `length`, poses; tests for zero-length, sequences, parallel and forever without drift | M |
|
|
68
|
+
| Picture tags | `Tagged(id, child)`, `hitId`, draw/bounds pass-through; tests | S |
|
|
69
|
+
| Ui | `UiState`, `Ui.step`, events, capture, `pressed`/`hovered`, `button`; PureScene-equivalent tests | M |
|
|
70
|
+
| Orb Run state/view | restart via Ui, Anim for the title and hit flash; tests unchanged plus Ui/Anim cases | M |
|
|
71
|
+
| Stress fixture | tags, buttons, animations in `stress_state_view.plum` | S |
|
|
72
|
+
| Benchmark + README | headed table, "Functional scenes" section | S |
|
|
73
|
+
|
|
74
|
+
## How it fits together
|
|
75
|
+
```mermaid
|
|
76
|
+
flowchart LR
|
|
77
|
+
I["host input"] --> M["Msg"] --> S["step(State, Msg) -> State"]
|
|
78
|
+
S --> V["view(State) -> SceneNode (recursive, Tagged ids)"]
|
|
79
|
+
V --> H["Picture.hitId"] --> U["Ui.step(UiState, input, hit) -> UiState + List[UiEvent]"]
|
|
80
|
+
U --> S
|
|
81
|
+
V --> D["Picture.draw"]
|
|
82
|
+
A["Anim.at(anim, now - start) -> Pose"] --> V
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Decisions you can change
|
|
86
|
+
- **Animation model.**
|
|
87
|
+
- Default: pure functions of time, with no stepping state.
|
|
88
|
+
- Alternative: PureScene-style stepped actions kept in `State`, more like libGDX but more state to carry.
|
|
89
|
+
- **Hit ids.**
|
|
90
|
+
- Default: a `Tagged(id, child)` wrapper node.
|
|
91
|
+
- Alternative: an `id` field on every `SceneNode` variant.
|
|
92
|
+
- **PureScene's future.**
|
|
93
|
+
- Default: decided after the benchmark and the supercompiler.
|
|
94
|
+
- Alternative: retire it right after this plan.
|
|
95
|
+
|
|
96
|
+
## Risks
|
|
97
|
+
- **R1: allocation.** `view` rebuilds the tree every frame, and the UI adds a hit test per input. Measured and reported; the supercompiler's view-into-draw fusion is the later lever.
|
|
98
|
+
- **R2: hit test against the view.** `Ui.step` needs `view(state)` for `hitId`. Building it twice per frame (once for input, once for draw) is allowed, measured, and may be shared later.
|
|
99
|
+
- **R4: nested std paths.** No std module lives three levels deep yet (`std/game/pure/X`). If the loader, LSP or `plum game` asset/bundle code assumes two levels, the Folders part fixes that in the same part, with a test.
|
|
100
|
+
- **R3: compiler gaps.** Recursive types and generic `List` code have hit gaps before. Each gap is reported as an incident and fixed in the compiler, not worked around.
|
|
101
|
+
|
|
102
|
+
## Out of scope
|
|
103
|
+
- Removing PureScene, SceneTree, SceneInput or Actions (a later decision).
|
|
104
|
+
- Fixed timestep and replays (separate queued plan).
|
|
105
|
+
- The supercompiler.
|
|
106
|
+
|
|
107
|
+
## How we will know it works
|
|
108
|
+
- Anim, Ui and Picture tests pass, including PureScene-equivalent button and capture cases.
|
|
109
|
+
- `game_state_view` tests are unchanged in value. New tests cover a restart click through Ui and the hit-flash pose at fixed times.
|
|
110
|
+
- Host parity and browser checks pass for `game_state_view`.
|
|
111
|
+
- Full suites stay green.
|
|
112
|
+
- Benchmark table reported; p95 under 4 ms for state/view.
|
|
113
|
+
|
|
114
|
+
## Rough time and cost
|
|
115
|
+
About 6 parts:
|
|
116
|
+
- in parallel first: Anim, Picture tags and Ui;
|
|
117
|
+
- then the game and the fixture;
|
|
118
|
+
- then the benchmark and README.
|
|
119
|
+
|
|
120
|
+
Rough agent time is 45–70 minutes, plus one headed benchmark run.
|