plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
775bbeb
— Peter John
2026-11-03T15:44:05+05:30
Recursive state/view; fix param reassignment codegen; own record docs
- README.md +56 -4
- docs/plans/2026-11-03-own-record.benchmark.md +11 -0
- docs/plans/2026-11-03-own-record.incidents.jsonl +84 -0
- docs/plans/2026-11-03-own-record.md +32 -0
- docs/plans/2026-11-03-own-record.progress.md +117 -0
- docs/plans/2026-11-03-own-record.report.json +342 -0
- docs/plans/2026-11-03-own-record.report.md +49 -0
- plum-examples/game_state_view.plum +208 -162
- plum-game/web-test/fixtures/stress_state_view.plum +88 -75
- plum-std/game/Picture.plum +163 -159
- plum-wasm-codegen/src/lib.rs +9 -1
- plum-wasm-codegen/tests/own_inplace_e2e_test.rs +50 -0
README.md
CHANGED
|
@@ -631,6 +631,58 @@ w.draw() # World.draw: prints "stage" then "world"
|
|
|
631
631
|
|
|
632
632
|
Every method that implements a trait's required method, replaces a trait default or replaces an embedded record's method must be `override fun`; defining one without it is an error (`World.draw hides Stage.draw; mark it override`), and so is an `override fun` that overrides nothing. Inside a record, `self.Owner.method(args)` calls the original from the embedded record or trait `Owner`.
|
|
633
633
|
|
|
634
|
+
### Own records
|
|
635
|
+
|
|
636
|
+
An `own record` (or `own trait`) is a value with a single owner, so the compiler can update it in place instead of copying. Ordinary records are unchanged; ownership is a property of the type. A plain parameter only borrows (read-only); an `own` parameter or `own self` takes the value, and the caller can't use it afterwards:
|
|
637
|
+
|
|
638
|
+
```plum
|
|
639
|
+
import std/Int
|
|
640
|
+
import std/Float
|
|
641
|
+
import std/List
|
|
642
|
+
|
|
643
|
+
own record Cell(x: Float) =
|
|
644
|
+
fun step(own self) -> Cell = Cell(..self, x: self.x + 1.0)
|
|
645
|
+
fun peek(self) -> Float = self.x
|
|
646
|
+
|
|
647
|
+
fun bump(own s: Cell) -> Cell =
|
|
648
|
+
t := Cell(..s, x: s.x + 1.0)
|
|
649
|
+
t
|
|
650
|
+
|
|
651
|
+
fun read(c: Cell) -> Float = c.peek()
|
|
652
|
+
|
|
653
|
+
record World(cell: Cell)
|
|
654
|
+
|
|
655
|
+
fun advance(w: World) =
|
|
656
|
+
w.cell = w.cell.step()
|
|
657
|
+
|
|
658
|
+
test "own"
|
|
659
|
+
c := Cell(x: 1.0)
|
|
660
|
+
d := bump(c)
|
|
661
|
+
assert {read(d) - 2.0}.abs() < 1e-6
|
|
662
|
+
w := World(cell: Cell(x: 0.0))
|
|
663
|
+
advance(w)
|
|
664
|
+
assert {w.cell.x - 1.0}.abs() < 1e-6
|
|
665
|
+
xs := List(Cell(x: 1.0), Cell(x: 2.0))
|
|
666
|
+
ys := xs.mapInPlace(|k| Cell(..k, x: k.x + 1.0))
|
|
667
|
+
assert {ys.at(1).x - 3.0}.abs() < 1e-6
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
- **What moves:** passing a value to an `own` parameter, calling an `own self` method, returning it, storing it in a field or list, and `match`ing on an owned value (the match consumes it). **What borrows:** plain parameters and plain `self`, and reads such as `s.x`.
|
|
671
|
+
- **Spread writes in place:** `Cell(..s, x: s.x + 1.0)` where `s` is owned reuses `s`'s storage and writes the changed fields into it, so no new record is allocated. Spreading a borrowed or ordinary record still copies.
|
|
672
|
+
- **Swap form:** you can't move a field out of a record, so replace it with a method call that takes and returns it: `w.cell = w.cell.step()` (in a game, `w.stage = w.stage.frame(dt, inputs).intoStage()`).
|
|
673
|
+
- **Lists:** `List.mapInPlace` rewrites every element, and `takeAt(i)` / `putAt(i, v)` move one element out and back. See [`plum-examples/own_list.plum`](plum-examples/own_list.plum).
|
|
674
|
+
|
|
675
|
+
The ownership checker reports mistakes at compile time:
|
|
676
|
+
|
|
677
|
+
```
|
|
678
|
+
"s" used after it was moved at 12:3
|
|
679
|
+
cannot assign field "x" of borrowed "s"
|
|
680
|
+
cannot store borrowed "s"; pass it with own or move it
|
|
681
|
+
cannot move out of field "stage" of "w"; use the swap form
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Embedding an own record or trait makes the embedding record own.
|
|
685
|
+
|
|
634
686
|
### Methods, `self` and field access
|
|
635
687
|
|
|
636
688
|
A `fun` declared indented directly inside an `enum`, `struct` or `record` body whose first parameter is `self` is a method, called on a value (`c.getAge()`). One without `self` is a static function, called on the type (`Cat.named("Tom")`), and can't use `self`. Calling a static on a value, or a method on the type, is an error:
|
|
@@ -1246,7 +1298,7 @@ The page calls `init` once after the assets are loaded, then `update` and `draw`
|
|
|
1246
1298
|
|
|
1247
1299
|
### Pure scene graph
|
|
1248
1300
|
|
|
1249
|
-
The pure scene graph is the same kind of tree as `std/game/Scene`, but
|
|
1301
|
+
The pure scene graph is the same kind of tree as `std/game/Scene`, but a function never changes a value it was only given: updates take an owned actor (`own self`, `own` parameters, see [Own records](#own-records)), change it in place and hand it back, so no `clone()` is needed. `Actor` is an `own trait` and `Group`, `Sprite`, `Label`, `Button` and `Stage` are `own record`s. The graph is split into four modules: `std/game/Actions` (timed actions such as `forever(rotateBy(...))`), `std/game/PureScene` (the `Actor` trait and its leaves, `group`, `sprite`, `button`, and the draw walk), `std/game/SceneTree` (update, hit testing, paths and event routing) and `std/game/SceneInput` (`Stage`, pointer capture and `UiEvent`). A game imports them by name:
|
|
1250
1302
|
|
|
1251
1303
|
```
|
|
1252
1304
|
import std/game/Actions
|
|
@@ -1272,8 +1324,7 @@ export fun init() -> World =
|
|
|
1272
1324
|
export fun update(w: World, dt: Float) =
|
|
1273
1325
|
inputs := SceneInput.pollPointer(w.wasDown)
|
|
1274
1326
|
w.wasDown = isMouseDown(0)
|
|
1275
|
-
|
|
1327
|
+
w.stage = w.stage.frame(dt, inputs).intoStage()
|
|
1276
|
-
w.stage = h.stage
|
|
1277
1328
|
|
|
1278
1329
|
export fun draw(w: World) =
|
|
1279
1330
|
w.stage.draw()
|
|
@@ -1281,7 +1332,8 @@ export fun draw(w: World) =
|
|
|
1281
1332
|
|
|
1282
1333
|
- **Immediate mode** ([`game.plum`](plum-examples/game.plum)): one state record and a `draw` that issues the calls every frame. Use it for flat arcade games with a few kinds of thing.
|
|
1283
1334
|
- **Scene** ([`game_scene.plum`](plum-examples/game_scene.plum)): a retained tree of mutable actors. Use it when you want objects that change in place and a familiar `act`/`draw` object model.
|
|
1284
|
-
- **Pure scene** ([`game_pure_scene.plum`](plum-examples/game_pure_scene.plum)): every update
|
|
1335
|
+
- **Pure scene** ([`game_pure_scene.plum`](plum-examples/game_pure_scene.plum)): every update takes an owned value and hands it back, pointer clicks arrive as `UiEvent`s, and the rules can be tested without a host. Use it when you want pure updates and tests that run without a game host.
|
|
1336
|
+
- **State/view** ([`game_state_view.plum`](plum-examples/game_state_view.plum)): the game state is plain data and a `view` turns it into `std/game/Picture`, a recursive picture type drawn by recursive functions. Use it when you want to draw from state instead of keeping a tree of actors.
|
|
1285
1337
|
|
|
1286
1338
|
**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; `--browser webkit` drives Playwright's WebKit, the Safari engine, instead) 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` (with `--json`, `draws` is the GPU draw calls per frame), `--headed`, `--dpr N` (a device pixel ratio of N), `--lose-device` (destroys the GPU device after a few frames and requires rendering to resume) and `--expect-pixel x,y,r,g,b[,tol]` (repeatable; the screenshot pixel at CSS (x, y)), `--expect-unsupported-webgpu` (hides `navigator.gpu` and requires the page's "WebGPU is not supported" message while plum.wasm still loads) and `--no-module-introspection` (makes `WebAssembly.Module.imports/exports/customSections` throw, as Safari does). `plum-game/web-test/fixtures/` has `interleave.plum`, `text_sizes.plum` and `rendertarget.plum` for these. 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.
|
|
1287
1339
|
|
docs/plans/2026-11-03-own-record.benchmark.md
CHANGED
|
@@ -12,6 +12,17 @@ with the current debug `plum`, so phase B includes the in-place spread codegen a
|
|
|
12
12
|
| stress_pure_scene, phase B (own record) | 2.107 / 1.931 | 2.00 / 1.90 | 2.70 / 2.30 | 4.10 / 2.50 | 0.343 / 0.316 | 1.26 |
|
|
13
13
|
| stress_state_view (Picture, tree rebuilt per frame) | 2.132 / 2.066 | 2.10 / 2.00 | 2.30 / 2.20 | 4.30 / 4.20 | 0.08 | 1.61 |
|
|
14
14
|
|
|
15
|
+
After the rebase onto master 05f8c38, recursive state/view (T14/T15: inline-variant `SceneNode`, `Nodes = End | More`, recursive `draw`/`step`/`view`, state in `Link[T]` lists of at most 700), same session:
|
|
16
|
+
|
|
17
|
+
| fixture | avg ms | p50 | p95 | p99 | update ms | draw ms |
|
|
18
|
+
|---|---|---|---|---|---|---|
|
|
19
|
+
| stress_scene (mutable) | 1.729 | 1.70 | 1.90 | 2.00 | 0.040 | 1.28 |
|
|
20
|
+
| stress_state_view, recursive | 1.905 / 1.908 | 1.90 | 2.30 | 2.40 | 0.05 | 1.49 |
|
|
21
|
+
|
|
22
|
+
The recursive version is faster than the List-based state/view (avg 1.91 vs 2.07–2.13), and its p99 tail is gone (2.4 vs 4.2 ms).
|
|
23
|
+
Against the mutable scene: avg +10%, p95 +21%. The remaining gap is the per-frame tree rebuild in draw (+0.2 ms).
|
|
24
|
+
Tail calls and, later, a supercompiler (fusing `view` into `draw`, so no tree is built) are the next levers.
|
|
25
|
+
|
|
15
26
|
Bar (review plan): average within 10% and p95 within 15% of the mutable scene, same session.
|
|
16
27
|
|
|
17
28
|
- Phase B: avg +11% to +21%, p95 +21% to +42%: **missed**. Update time is unchanged from phase A (about 0.32 ms).
|
docs/plans/2026-11-03-own-record.incidents.jsonl
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; ./target/debug/plum compile plum-game/web-test/fixtur"}
|
|
2
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-game --test host_test pureScene 2"}
|
|
3
|
+
{"file": "docs/", "line": 0, "context": "T1: change outside Owns blocked by checkpoint; nothing saved: 1 files under docs/, first 1: docs/plans/2026-11-02-own-record.review.md", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "14:40:41"}
|
|
4
|
+
{"file": "plum-core/", "line": 0, "context": "T1: change outside Owns blocked by checkpoint; nothing saved: 5 files under plum-core/, first 5: plum-core/src/ctors.rs, plum-core/src/embeds.rs, plum-core/src/forms.rs, plum-core/src/traits.rs, plum…", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "14:40:52"}
|
|
5
|
+
{"file": "-", "line": 0, "context": "exit code 101: error: could not compile `plum-wasm-codegen` (lib) due to 2 previous errors", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:41:14"}
|
|
6
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-core 21 | grep -E test result|FA"}
|
|
7
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-core --test own_parse_test own_rec"}
|
|
8
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-core --test own_parse_test 21 | g"}
|
|
9
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-core --test own_parse_test > .supe"}
|
|
10
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-core --test own_parse_test 21 | g"}
|
|
11
|
+
{"file": "-", "line": 0, "context": "exit code 1: For more information, try '--help'.", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:42:19"}
|
|
12
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T2", "agent": "pt_review", "kind": "verified", "time": "14:43:13"}
|
|
13
|
+
{"file": "docs/plans/2026-11-03-own-record.md", "line": 177, "context": "seam T10↔T11: T10 Produces: Picture.draw: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "14:45:30"}
|
|
14
|
+
{"file": "docs/plans/2026-11-03-own-record.md", "line": 177, "context": "seam T10↔T11: T10 Produces: Picture.bounds: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "14:45:30"}
|
|
15
|
+
{"file": "docs/plans/2026-11-03-own-record.md", "line": 177, "context": "seam T10↔T11: T10 Produces: Picture.hitPath: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "14:45:30"}
|
|
16
|
+
{"file": "plum-core/", "line": 0, "context": "T12: change outside Owns blocked by checkpoint; nothing saved: 2 files under plum-core/, first 2: plum-core/src/embeds.rs, plum-core/src/traits.rs", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "14:47:49"}
|
|
17
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-checker --test ownership_moves_tes"}
|
|
18
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-checker --test ownership_moves_tes"}
|
|
19
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: touch plum-wasm-codegen/src/lib.rs && cargo test -q -p plum-wasm-codegen --test own_inplace_e2e_test"}
|
|
20
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values/plum-wasm-codegen && cargo test -q -p plum-wasm-codege"}
|
|
21
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values/plum-wasm-codegen && cargo test -q -p plum-wasm-codege"}
|
|
22
|
+
{"file": "plum-checker/src/ownership.rs", "line": 1, "context": "Assign and ClassCall carry no source position; move/use errors use the nearest FnCall or statement position, columns approximate", "agent": "T4", "kind": "workaround", "time": "14:50:38"}
|
|
23
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T4", "agent": "pt_review", "kind": "verified", "time": "14:50:51"}
|
|
24
|
+
{"file": "", "line": 0, "context": "verified: seam T11↔T12", "agent": "pt_review", "kind": "verified", "time": "14:50:51"}
|
|
25
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cargo test -q -p plum-checker --test ownership_moves_test 21 | tail -8"}
|
|
26
|
+
{"file": "-", "line": 0, "context": "own_spread_reuses_object: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:52:35"}
|
|
27
|
+
{"file": "-", "line": 0, "context": "exit code 1: For more information, try '--help'.", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:52:40"}
|
|
28
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cargo test -p plum-checker --test ownership_flow_test 21 | grep -E ^test |panicked|error | head -"}
|
|
29
|
+
{"file": "-", "line": 0, "context": "own_spread_reuses_object: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:53:03"}
|
|
30
|
+
{"file": "-", "line": 0, "context": "own_self_field_write: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:53:03"}
|
|
31
|
+
{"file": "-", "line": 0, "context": "own_spread_sets_fields: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:53:03"}
|
|
32
|
+
{"file": "-", "line": 0, "context": "own_spread_swap_reads_old_values: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:53:03"}
|
|
33
|
+
{"file": "-", "line": 0, "context": "own_trait_record_spread_in_place: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:53:03"}
|
|
34
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cargo test -q -p plum-wasm-codegen --test own_inplace_e2e_test own_spread_sets_fields 21 | tail -20"}
|
|
35
|
+
{"file": "-", "line": 0, "context": "exit code 101: warning: [email protected]: plum-tooling/vscode-plum/out/extension.js not found (run `npm install && npm run build` there); `plum editor vscode` wil", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:55:03"}
|
|
36
|
+
{"file": "-", "line": 0, "context": "exit code 101: warning: [email protected]: plum-tooling/vscode-plum/out/extension.js not found (run `npm install && npm run build` there); `plum editor vscode` wil", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:55:10"}
|
|
37
|
+
{"file": "-", "line": 0, "context": "variadic_own_param_takes_every_arg: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:56:31"}
|
|
38
|
+
{"file": "-", "line": 0, "context": "own_spread_reuses_object: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:56:44"}
|
|
39
|
+
{"file": "-", "line": 0, "context": "own_spread_sets_fields: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:56:44"}
|
|
40
|
+
{"file": "-", "line": 0, "context": "own_spread_swap_reads_old_values: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:56:44"}
|
|
41
|
+
{"file": "-", "line": 0, "context": "own_spread_reuses_object: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:57:15"}
|
|
42
|
+
{"file": "-", "line": 0, "context": "own_spread_swap_reads_old_values: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:57:15"}
|
|
43
|
+
{"file": "-", "line": 0, "context": "own_spread_sets_fields: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "14:57:15"}
|
|
44
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T4", "agent": "pt_review", "kind": "verified", "time": "14:58:35"}
|
|
45
|
+
{"file": "", "line": 0, "context": "verified: seam T4↔T5", "agent": "pt_review", "kind": "verified", "time": "14:58:35"}
|
|
46
|
+
{"file": "plum-checker/src/ownership.rs", "line": 700, "context": "Any program containing an own record makes checker flag Int/Float params stored into locals in std/Str.plum etc: cannot store borrowed charIndex (k := charIndex). Store check must apply only to own-t…", "agent": "T6", "kind": "exception", "time": "15:04:21"}
|
|
47
|
+
{"file": "-", "line": 0, "context": "own_spread_reuses_object: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "15:06:00"}
|
|
48
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cargo test -p plum-checker --test ownership_flow_test --test ownership_moves_test 21 | grep -E tes"}
|
|
49
|
+
{"file": "-", "line": 0, "context": "exit code 1: For more information, try '--help'.", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "15:06:18"}
|
|
50
|
+
{"file": "-", "line": 0, "context": "own_spread_reuses_object: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "15:06:20"}
|
|
51
|
+
{"file": "-", "line": 0, "context": "own_spread_reuses_object: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "15:06:47"}
|
|
52
|
+
{"file": "-", "line": 0, "context": "std_str_checks_with_own_present: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "15:07:09"}
|
|
53
|
+
{"file": "-", "line": 0, "context": "own_trait_record_spread_in_place: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "15:07:46"}
|
|
54
|
+
{"file": "", "line": 0, "context": "verified: seam T4↔T5", "agent": "pt_review", "kind": "verified", "time": "15:07:59"}
|
|
55
|
+
{"file": "-", "line": 0, "context": "exit code 1: For more information, try '--help'.", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "15:08:25"}
|
|
56
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-wasm-codegen --test own_inplace_e2"}
|
|
57
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-wasm-codegen --no-run 21 | grep E"}
|
|
58
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T6", "agent": "pt_review", "kind": "verified", "time": "15:11:59"}
|
|
59
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: sed -i 's/if let Some((true, field)) = accessorField(&c.name) {/if let Some((true, field)) = accesso"}
|
|
60
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cargo test -p plum-wasm-codegen --test own_inplace_e2e_test 21 | grep -E ^test |test result|panick"}
|
|
61
|
+
{"file": "-", "line": 0, "context": "exit code 1: For more information, try '--help'.", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "15:15:26"}
|
|
62
|
+
{"file": "", "line": 0, "context": "verified: seam T6↔T7", "agent": "pt_review", "kind": "verified", "time": "15:15:42"}
|
|
63
|
+
{"file": "", "line": 0, "context": "verified: seam T5↔T7", "agent": "pt_review", "kind": "verified", "time": "15:15:42"}
|
|
64
|
+
{"file": "", "line": 0, "context": "verified: seam T4↔T5", "agent": "pt_review", "kind": "verified", "time": "15:16:54"}
|
|
65
|
+
{"file": "", "line": 0, "context": "verified: seam T5↔T7", "agent": "pt_review", "kind": "verified", "time": "15:16:54"}
|
|
66
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T6", "agent": "pt_review", "kind": "verified", "time": "15:16:54"}
|
|
67
|
+
{"file": "", "line": 0, "context": "verified: seam T6↔T7", "agent": "pt_review", "kind": "verified", "time": "15:16:54"}
|
|
68
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T8", "agent": "pt_review", "kind": "verified", "time": "15:20:37"}
|
|
69
|
+
{"file": "docs/plans/2026-11-03-own-record.md", "line": 140, "context": "seam T7↔T8: T7 Produces: List.take: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "15:20:37"}
|
|
70
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; cargo test -p plum-checker 21 | grep -E test result"}
|
|
71
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum-pure-scene-values; ./target/debug/plum test plum-std/game/PureScene.plum"}
|
|
72
|
+
{"file": "", "line": 0, "context": "verified: seam T4↔T5", "agent": "pt_review", "kind": "verified", "time": "15:24:18"}
|
|
73
|
+
{"file": "", "line": 0, "context": "verified: seam T5↔T7", "agent": "pt_review", "kind": "verified", "time": "15:24:18"}
|
|
74
|
+
{"file": "", "line": 0, "context": "verified: seam T8↔T9", "agent": "pt_review", "kind": "verified", "time": "15:24:40"}
|
|
75
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cargo test -p plum-core -p plum-checker -p plum-wasm-codegen 21 | grep -E '^test result|FAILED|pani"}
|
|
76
|
+
{"file": "", "line": 0, "context": "verified: seam T12↔T14", "agent": "pt_review", "kind": "verified", "time": "15:35:01"}
|
|
77
|
+
{"file": "", "line": 0, "context": "verified: seam T9↔T13", "agent": "pt_review", "kind": "verified", "time": "15:35:36"}
|
|
78
|
+
{"file": "", "line": 0, "context": "verified: seam T11↔T15", "agent": "pt_review", "kind": "verified", "time": "15:40:19"}
|
|
79
|
+
{"file": "", "line": 0, "context": "verified: seam T12↔T15", "agent": "pt_review", "kind": "verified", "time": "15:40:19"}
|
|
80
|
+
{"file": "docs/plans/2026-11-03-own-record.md", "line": 221, "context": "seam T14↔T15: T14 Produces: Picture.draw: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "15:40:19"}
|
|
81
|
+
{"file": "docs/plans/2026-11-03-own-record.md", "line": 221, "context": "seam T14↔T15: T14 Produces: Picture.bounds: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "15:40:19"}
|
|
82
|
+
{"file": "docs/plans/2026-11-03-own-record.md", "line": 221, "context": "seam T14↔T15: T14 Produces: Picture.hitPath: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "15:40:19"}
|
|
83
|
+
{"file": "", "line": 0, "context": "verified: seam T6↔T7", "agent": "pt_review", "kind": "verified", "time": "15:41:35"}
|
|
84
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T6", "agent": "pt_review", "kind": "verified", "time": "15:41:35"}
|
docs/plans/2026-11-03-own-record.md
CHANGED
|
@@ -47,6 +47,8 @@ flowchart LR
|
|
|
47
47
|
T11[state/view game]
|
|
48
48
|
T12[state/view fixture]
|
|
49
49
|
T13[docs]
|
|
50
|
+
T14[recursive Picture]
|
|
51
|
+
T15[recursive state/view]
|
|
50
52
|
%% taskgraph
|
|
51
53
|
T1 --> T2
|
|
52
54
|
T3 --> T4
|
|
@@ -60,6 +62,10 @@ flowchart LR
|
|
|
60
62
|
T10 --> T11
|
|
61
63
|
T11 --> T12
|
|
62
64
|
T9 --> T13
|
|
65
|
+
T12 --> T14
|
|
66
|
+
T11 --> T15
|
|
67
|
+
T12 --> T15
|
|
68
|
+
T14 --> T15
|
|
63
69
|
```
|
|
64
70
|
|
|
65
71
|
## Migration
|
|
@@ -210,6 +216,31 @@ e.g. `Picture.hitPath(Group([], [RectShape(0.0, 0.0, 10.0, 10.0, c)]), 50.0, 50.
|
|
|
210
216
|
**Review Focus:** numeric: same actor count (about 5,750) and visual density as stress_pure_scene.
|
|
211
217
|
**Test code:** web_check.rs `state_view_renders_and_animates` (mirrors pure_scene_renders_and_animates) passes in Chromium.
|
|
212
218
|
|
|
219
|
+
## Recursive state/view (added after rebase; user request)
|
|
220
|
+
|
|
221
|
+
### T14 recursive Picture · Complexity 4 · Est. context 40k
|
|
222
|
+
**Owns:** plum-std/game/Picture.plum
|
|
223
|
+
**Components:** 1
|
|
224
|
+
**LOC owned:** 200
|
|
225
|
+
**Tests:** fast
|
|
226
|
+
**Model:** sonnet
|
|
227
|
+
**Consumes:** none
|
|
228
|
+
**Produces:** Picture.draw(node: SceneNode) · Picture.bounds(node: SceneNode) -> Rect · Picture.hitPath(node: SceneNode, x: Float, y: Float) -> List[Int]
|
|
229
|
+
e.g. `Picture.hitPath(Group(Translate(10.0, 0.0), More(RectShape(0.0, 0.0, 10.0, 10.0, c), End)), 15.0, 5.0) -> [0]`
|
|
230
|
+
**Review Focus:** deep: structural recursion only (no List indexing, no matrix stack list); parent matrix and alpha passed as args. numeric: same bounds/hit results as before.
|
|
231
|
+
**Test code:** inline-variant enums: `enum SceneNode = Group(transform: Transform, children: Nodes) | Filter(effect: Effect, target: Int, child: SceneNode) | Sprite(..) | Text(..) | RectShape(..) | CircleShape(..) | LineShape(..)`, `enum Nodes = End | More(head: SceneNode, tail: Nodes)`, `enum Transform = Translate(..) | Rotate(..) | Scale(..) | Compose(first: Transform, then: Transform)`. draw/bounds/hitPath are recursive `= match` fns. Existing 5 test names keep their asserted values.
|
|
232
|
+
|
|
233
|
+
### T15 recursive state/view game and fixture · Complexity 5 · Est. context 50k
|
|
234
|
+
**Owns:** plum-examples/game_state_view.plum, plum-game/web-test/fixtures/stress_state_view.plum
|
|
235
|
+
**Components:** 2
|
|
236
|
+
**LOC owned:** 300
|
|
237
|
+
**Tests:** slow
|
|
238
|
+
**Model:** sonnet
|
|
239
|
+
**Consumes:** Picture.hitPath(node: SceneNode, x: Float, y: Float) -> List[Int]
|
|
240
|
+
**Produces:** none
|
|
241
|
+
**Review Focus:** state: `Msg` inline-variant enum; `step(state, msg)` and `view(state)` recursive `= match`; id-keyed state folded recursively. Same gameplay and test names.
|
|
242
|
+
**Test code:** game_state_view.plum keeps its 22 test names and values; stress_state_view.plum keeps 2 tests and 5,750 actors; web_check `state_view_renders_and_animates` passes.
|
|
243
|
+
|
|
213
244
|
## Docs
|
|
214
245
|
|
|
215
246
|
### T13 docs · Complexity 2 · Est. context 30k
|
|
@@ -229,3 +260,4 @@ e.g. `Picture.hitPath(Group([], [RectShape(0.0, 0.0, 10.0, 10.0, c)]), 50.0, 50.
|
|
|
229
260
|
- std/game + examples: state, numeric. Risks: behaviour drift from phase A; Picture matrix order.
|
|
230
261
|
**Probe entry:** run=./target/debug/plum test
|
|
231
262
|
R4 gate: coordinator benchmarks T2; if it misses the bar, stop and report.
|
|
263
|
+
Follow-ups (user, not in this plan): tail calls for the recursive Picture/state/view; a supercompiler later.
|
docs/plans/2026-11-03-own-record.progress.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Phase B own-record run
|
|
2
|
+
- start ea04e6cc77a758820e11f3544ec36d10ce77c6f2
|
|
3
|
+
- Ruling: skip T0 Wave 0 skeleton - stubs cannot be generated for Plum/Rust compiler interfaces; consumers test real code behind taskgraph edges - cost if wrong: a consumer waits on its producer
|
|
4
|
+
- Ruling: archived phase A run state to .superlite/archive/pure-scene-phase-a - lint froze T1-T9 against phase A's ledger - cost if wrong: none, files kept
|
|
5
|
+
- Ruling: plan 16.7k chars over the 15k cap, accepted - 13 tasks of contracts; prime stays slim - cost if wrong: larger prime
|
|
6
|
+
- unblocked: dispatched T1 T3 T10 at 14:38:12; waiting on fixes: none
|
|
7
|
+
- Ruling: T3 may add isOwn/ownSelf: false at AST construction sites outside its Owns (plum-core ctors/embeds/forms/traits/union_lower, others) - adding struct fields breaks those literals - cost if wrong: checkpoint needs a manual docs-free commit of those sites
|
|
8
|
+
- Ruling: T2 sent to T1's agent before T1's checkpoint - checkpoint blocked by T3's in-flight construction-site edits; T1 tests green - cost if wrong: T1 re-checkpointed later
|
|
9
|
+
- T1 DONE_WITH_CONCERNS: host_test pureScene not run by agent (hook); coordinator runs it
|
|
10
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=2.4
|
|
11
|
+
- Ruling: T10 Filter carries target: Int (game-created render target) - per-draw textureCreate leaked a texture per filter per frame, no destroy extern - cost if wrong: games manage one texture per filter
|
|
12
|
+
- Ruling: T10 SceneNode flattened (RectShape|CircleShape|LineShape|Sprite|Text|Group|Filter), BlendFx name - nested Shape enum fails to type-check; Blend collides with Game - cost if wrong: none, same expressiveness
|
|
13
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=0.0
|
|
14
|
+
- checkpoint T1 a0d9f9fe12 migrate phase A to master syntax
|
|
15
|
+
- checkpoint T3 1b68156d83 own syntax
|
|
16
|
+
- checkpoint T10 4b3d53c2cb std/game/Picture
|
|
17
|
+
- unblocked: dispatched T4 T6 at 14:43:02 (T2 T11 already sent); waiting on fixes: none
|
|
18
|
+
- Ruling: committed T3's 13 one-line construction-site defaults separately (1 commit) - checkpoint blocks files outside Owns - cost if wrong: one extra commit
|
|
19
|
+
- checkpoint T2 03d3d3fb2a upper-bound probe fixture
|
|
20
|
+
- checkpoint T11 c9368a7cda state/view Orb Run
|
|
21
|
+
- R4 GATE TRIPPED 14:45: headed M3 Pro, measure 10. mutable avg 1.737/1.689 p95 1.9/1.8 upd 0.037; phase A avg 2.012 p95 2.5 upd 0.321; in-place upper bound avg 1.862/1.883 p95 2.3/2.3 upd 0.275/0.277. Bar avg<=+10% p95<=+15%: upper bound avg +7..11%, p95 +21..28% MISS; in-place removes only ~14% of update cost -> copies are not the main cost.
|
|
22
|
+
- Ruling: per R4 stop compiler chain (T4 stopped, T6 not sent, T5 T7 T8 T9 held); continue state/view T11-T12 (independent) for the 5-row table; report to user - cost if wrong: compiler work delayed one round
|
|
23
|
+
- T4 stopped: plum-checker/src/ownership.rs draft (uncompiled, unwired, untested); lowerings still drop isOwn; Assign/ClassCall lack positions (move error positions approximate)
|
|
24
|
+
- Ruling: [scope] user chose 'build own record' after R4 gate tripped - resume T4, dispatch T6; continue full compiler chain - cost if wrong: phase B may still miss the bar (upper bound p95 +21..28%)
|
|
25
|
+
- T12 DONE (checkpoint waits on T4 in-flight plum-core edits). state/view headed: avg 2.132/2.066 p95 2.3/2.2 p99 4.3/4.2 upd 0.081/0.074 draw 1.621/1.595 (view rebuild counted in draw)
|
|
26
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=54.4
|
|
27
|
+
- checkpoint T4 d692a4a4f7 ownership checker: bindings and moves
|
|
28
|
+
- checkpoint T12 f0933bd6ff state/view fixture and web check
|
|
29
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=42.3
|
|
30
|
+
- unblocked: dispatched T5 (fresh) + package-reviewer on T4 at 14:51:21; waiting on fixes: none
|
|
31
|
+
- Ruling: T4 reviewer is findings-only for ownership.rs (T5 edits it in parallel); fixes route to T5 - cost if wrong: one extra T5 round
|
|
32
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=55.7
|
|
33
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=4.3
|
|
34
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=5.4
|
|
35
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=0.0
|
|
36
|
+
- T4 review DONE_WITH_CONCERNS: no straight-line bugs; plum-std+examples 579/0; 6 holes (field move-out, yield, ternary tails, variadic own, Bang/Catch/Try, inherited own self) routed to T5
|
|
37
|
+
- Ruling: moving an own value out of a field (non-swap) errors 'cannot move out of field "<f>" of "<name>"; use the swap form' - partial moves untracked otherwise - cost if wrong: one more message text
|
|
38
|
+
- tests fast passed=19 failed=5 skipped=0 seconds=15.6
|
|
39
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=15.6
|
|
40
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=0.3
|
|
41
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=1.6
|
|
42
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=1.2
|
|
43
|
+
- tests fast passed=8 failed=1 skipped=0 seconds=0.4
|
|
44
|
+
- tests fast passed=0 failed=3 skipped=0 seconds=9.8
|
|
45
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=52.4
|
|
46
|
+
- tests fast passed=0 failed=3 skipped=0 seconds=5.1
|
|
47
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=4.0
|
|
48
|
+
- checkpoint T4 95d5739e16 ownership checker: bindings and moves
|
|
49
|
+
- checkpoint T5 5e9fcc30d1 ownership checker: branches, loops, closures, fields
|
|
50
|
+
- T5 DONE: flow 16/16, moves 13/13. Ruling: accept deviations (loop error at move site; closures borrow-only incl. returning captures; field moves except exact swap; R2 only at/get/first/last on List[Own] params/annotated locals; R5 wasm test moved to T6 scope) - cost if wrong: narrower R2 coverage
|
|
51
|
+
- T6 BLOCKED: checker false positive 'cannot store borrowed' on Int bindings whenever an own type exists -> F-T5-1 to fixer-sonnet (tier 1, known symptom, one module)
|
|
52
|
+
- NOTE: single-letter uppercase type names (S) parse as generics; tests use Cell
|
|
53
|
+
- tests fast passed=2 failed=1 skipped=0 seconds=6.8
|
|
54
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=40.1
|
|
55
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.3
|
|
56
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=0.0
|
|
57
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=2.0
|
|
58
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.3
|
|
59
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=2.7
|
|
60
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.3
|
|
61
|
+
- tests fast passed=10 failed=1 skipped=0 seconds=15.0
|
|
62
|
+
- tests fast passed=3 failed=0 skipped=0 seconds=24.0
|
|
63
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=2.0
|
|
64
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.5
|
|
65
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.4
|
|
66
|
+
- checkpoint T5 86efd073f5 ownership checker: branches, loops, closures, fields
|
|
67
|
+
- F-T5-1 FIXED (params of non-own type no longer tracked), T5 re-checkpointed 86efd07
|
|
68
|
+
- F-T6-1 to fixer-sonnet-high: finish/verify T6 (implementer over 80k context); replace Debug-text hazard check with AST walk; add R5 match-arm test
|
|
69
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.3
|
|
70
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=0.1
|
|
71
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=19.5
|
|
72
|
+
- checkpoint T6 575a969416 in-place spread codegen
|
|
73
|
+
- T6 checkpointed 575a969 (own_inplace 8 pass 1 ignored; codegen suite green)
|
|
74
|
+
- F-T6-2 to fixer-sonnet-high: own trait field setters (__set_) flagged as borrowed self; __set_ calls must count as field writes on receiver
|
|
75
|
+
- unblocked: dispatched T7 to a53be28 at 15:12:21; waiting on fixes: F-T6-2
|
|
76
|
+
- Ruling: [scope] List.take/put renamed takeAt/putAt - List.take shadowed the lazy Seq take adapter (forms.rs methodStar), breaking seq.plum - cost if wrong: naming only; T8 consumes takeAt/putAt
|
|
77
|
+
- T7 sweep: at/get/first/last/head/findValue/sample alias; set old slot not invalidated; slice shares storage; removeAt/pop/takeAt leave stale slots; push/add/init/remove take plain T (follow-up: own-aware List API)
|
|
78
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=0.0
|
|
79
|
+
- checkpoint T7 a167991594 List for owned elements
|
|
80
|
+
- checkpoint T5 5b664337f4 ownership checker: branches, loops, closures, fields
|
|
81
|
+
- checkpoint T6 58a9182174 in-place spread codegen
|
|
82
|
+
- F-T6-2 FIXED, T5/T6 re-checkpointed
|
|
83
|
+
- Ruling: T8 Consumes cleared (T7 block frozen with take/put names); T8 notes name takeAt/putAt; edge T7->T8 kept - cost if wrong: none
|
|
84
|
+
- unblocked: dispatched T8 (fresh) at 15:17:17; waiting on fixes: none
|
|
85
|
+
- checkpoint T8 968ef98c72 scene modules on own
|
|
86
|
+
- T8 checkpointed 968ef98 (38 tests; clone test removed; 3 leaves_input_unchanged tests replaced by in-place equivalents; Handled/StageHandled/Stage own records)
|
|
87
|
+
- F-T8-1 to fixer-sonnet-high: match pattern bindings untracked (move-out hole)
|
|
88
|
+
- unblocked: dispatched T9 to a7cd687 at 15:20:50; waiting on fixes: F-T8-1
|
|
89
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=37.4
|
|
90
|
+
- checkpoint T5 b8ee44d8b1 ownership checker: branches, loops, closures, fields
|
|
91
|
+
- F-T8-1 FIXED: match on owned scrutinee moves it, pattern names owned; borrowed -> names borrowed; synthesized union forwarders skipped. flow 27/27
|
|
92
|
+
- checkpoint T9 d692c712f3 example and fixture on own
|
|
93
|
+
- T9 checkpointed d692c71. Benchmark phase B: avg 2.107/1.931 p95 2.7/2.3 upd 0.343/0.316 vs mutable 1.736/1.738 p95 1.9 -> MISSED; written docs/plans/2026-11-03-own-record.benchmark.md
|
|
94
|
+
- Rebase onto master 9117837 (inline record variants + = match bodies): phase B committed 6525e4c; conflicts ast.rs parser.rs parser.c -> fixer-sonnet-high. Reverted superlite's .gitignore additions (ignored committed design docs)
|
|
95
|
+
- Ruling: [scope] user approved T14: Picture SceneNode + state/view Msg on inline-variant enums with = match bodies, after the rebase - cost if wrong: one task
|
|
96
|
+
- Rebase DONE onto master 05f8c38 (512 Rust / 593 Plum / 118 corpus green); hidden inline-variant records inherit isOwn
|
|
97
|
+
- Ruling: [scope] T14 recursive Picture + T15 recursive state/view added (user: recursive type + recursive fns; tail calls / supercompiler later, out of scope); children as recursive Nodes = End | More - cost if wrong: deep recursion slower until tail calls
|
|
98
|
+
- unblocked: dispatched T13 T14 (fresh) at 15:32:51; T15 after T14
|
|
99
|
+
- checkpoint T14 e54f685579 recursive Picture
|
|
100
|
+
- T14 checkpointed e54f685 (Picture recursive: inline-variant SceneNode/Transform(+Identity)/Effect, Nodes=End|More; 6/6)
|
|
101
|
+
- unblocked: dispatched T15 to a7ed9fc at 15:35:12
|
|
102
|
+
- checkpoint T13 88103bd15c docs
|
|
103
|
+
- T13 checkpointed 88103bd (README Own records; example uses t := Cell(..s, ...))
|
|
104
|
+
- F-T6-3 to fixer-opus: s = Cell(..s, ...) on own param traps (wasm cast failure), cause unknown
|
|
105
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=57.4
|
|
106
|
+
- checkpoint T15 b689125069 recursive state/view game and fixture
|
|
107
|
+
- checkpoint T6 017bd757c0 in-place spread codegen
|
|
108
|
+
- F-T6-3 FIXED (pre-existing): assigning to any param created a shadow local read for the whole body (Int params silently read 0, records trapped); now reuses the param slot. 594 Plum tests green
|
|
109
|
+
- T15 checkpointed b689125. Recursive state/view headed: avg 1.905/1.908 p95 2.3 p99 2.4 upd 0.05 draw 1.49 vs mutable 1.729 p95 1.9
|
|
110
|
+
- tests full passed=1765 failed=0 skipped=0 seconds=110.6
|
|
111
|
+
- Final review CHANGES_REQUESTED; ruled after direct checks: full suite 1765/0 (run_tests --full --wait).
|
|
112
|
+
- Ruling: finding "codegen red / own_spread_reuses_object" - stale incident [1]; suite green, own_inplace_e2e 12 pass - cost if wrong: none
|
|
113
|
+
- Ruling: finding "Int false positive" - fixed by F-T5-1, regression int_param_alias_not_tracked_with_own_present - cost if wrong: none
|
|
114
|
+
- Ruling: finding "approximate move positions" - real; deferred follow-up (add line/col to Stmt::Assign, Expr::ClassCall) - cost if wrong: error columns approximate (lines correct)
|
|
115
|
+
- Ruling: finding "state not probed" - flow tests cover joins/loops/closures/early return (27 pass) - cost if wrong: none
|
|
116
|
+
- Ruling: finding "Picture fns missing" - tooling cannot parse Plum; Picture.plum:217-227 present, 6 tests - cost if wrong: none
|
|
117
|
+
- Ruling: finding "T7 Produces take/put" - block frozen; rename recorded in T8 notes and rulings - cost if wrong: doc drift only
|
docs/plans/2026-11-03-own-record.report.json
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
{
|
|
2
|
+
"projection_note": "projection: fixed forecast saved at lint 2026-11-03T14:37:44 (plan edited since)",
|
|
3
|
+
"headline": {
|
|
4
|
+
"wall_min": {
|
|
5
|
+
"projected": 13.605,
|
|
6
|
+
"actual": 61.825
|
|
7
|
+
},
|
|
8
|
+
"cost_usd_agents": {
|
|
9
|
+
"projected": 1.742609,
|
|
10
|
+
"actual": 9.027237
|
|
11
|
+
},
|
|
12
|
+
"task_min": {
|
|
13
|
+
"projected": 25.758,
|
|
14
|
+
"actual": 83.348
|
|
15
|
+
},
|
|
16
|
+
"agents": {
|
|
17
|
+
"projected": 13,
|
|
18
|
+
"actual": 20
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"feature": null,
|
|
22
|
+
"cost_split": {
|
|
23
|
+
"agents_usd": 6.859187,
|
|
24
|
+
"rework_usd": 2.16805,
|
|
25
|
+
"coordinator_usd": 25.293098,
|
|
26
|
+
"total_usd": 34.320335
|
|
27
|
+
},
|
|
28
|
+
"concurrency": {
|
|
29
|
+
"peak": 4,
|
|
30
|
+
"projected_peak": 4
|
|
31
|
+
},
|
|
32
|
+
"critical_path": {
|
|
33
|
+
"actual": [
|
|
34
|
+
"T3",
|
|
35
|
+
"T6"
|
|
36
|
+
],
|
|
37
|
+
"projected": [
|
|
38
|
+
"T3",
|
|
39
|
+
"T4",
|
|
40
|
+
"T5",
|
|
41
|
+
"T7",
|
|
42
|
+
"T8",
|
|
43
|
+
"T9",
|
|
44
|
+
"T13"
|
|
45
|
+
],
|
|
46
|
+
"projected_min": 13.605
|
|
47
|
+
},
|
|
48
|
+
"model_mix": {
|
|
49
|
+
"sonnet": {
|
|
50
|
+
"tasks": 10,
|
|
51
|
+
"cost_usd": 9.027237
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"coordinator_cost_by_model": {
|
|
55
|
+
"opus": 25.293098
|
|
56
|
+
},
|
|
57
|
+
"quality": {
|
|
58
|
+
"tests": {
|
|
59
|
+
"passed": 1765,
|
|
60
|
+
"failed": 0,
|
|
61
|
+
"skipped": 0,
|
|
62
|
+
"seconds": 110.6,
|
|
63
|
+
"mode": "full",
|
|
64
|
+
"failures": [],
|
|
65
|
+
"skipped_tests": []
|
|
66
|
+
},
|
|
67
|
+
"first_time_pass": 0,
|
|
68
|
+
"first_time_n": 1,
|
|
69
|
+
"rework_dispatches": 5,
|
|
70
|
+
"open_incidents": 38
|
|
71
|
+
},
|
|
72
|
+
"tasks": [
|
|
73
|
+
{
|
|
74
|
+
"task": "T6",
|
|
75
|
+
"model": "sonnet",
|
|
76
|
+
"pred_min": 1.612,
|
|
77
|
+
"actual_min": 32.548,
|
|
78
|
+
"delta": {
|
|
79
|
+
"abs": 30.9363,
|
|
80
|
+
"pct": 1919.1
|
|
81
|
+
},
|
|
82
|
+
"cost_usd": 3.216926,
|
|
83
|
+
"first_time_pass": false
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"task": "T5",
|
|
87
|
+
"model": "sonnet",
|
|
88
|
+
"pred_min": 1.769,
|
|
89
|
+
"actual_min": 7.252,
|
|
90
|
+
"delta": {
|
|
91
|
+
"abs": 5.4827,
|
|
92
|
+
"pct": 309.9
|
|
93
|
+
},
|
|
94
|
+
"cost_usd": 1.200695,
|
|
95
|
+
"first_time_pass": null
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"task": "T3",
|
|
99
|
+
"model": "sonnet",
|
|
100
|
+
"pred_min": 2.094,
|
|
101
|
+
"actual_min": 7.223,
|
|
102
|
+
"delta": {
|
|
103
|
+
"abs": 5.1293,
|
|
104
|
+
"pct": 245.0
|
|
105
|
+
},
|
|
106
|
+
"cost_usd": 0.755352,
|
|
107
|
+
"first_time_pass": null
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"task": "T8",
|
|
111
|
+
"model": "sonnet",
|
|
112
|
+
"pred_min": 2.459,
|
|
113
|
+
"actual_min": 7.138,
|
|
114
|
+
"delta": {
|
|
115
|
+
"abs": 4.6793,
|
|
116
|
+
"pct": 190.3
|
|
117
|
+
},
|
|
118
|
+
"cost_usd": 1.148973,
|
|
119
|
+
"first_time_pass": null
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"task": "T2",
|
|
123
|
+
"model": "sonnet",
|
|
124
|
+
"pred_min": 2.178,
|
|
125
|
+
"actual_min": 6.643,
|
|
126
|
+
"delta": {
|
|
127
|
+
"abs": 4.4653,
|
|
128
|
+
"pct": 205.0
|
|
129
|
+
},
|
|
130
|
+
"cost_usd": 0.290503,
|
|
131
|
+
"first_time_pass": null
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"task": "T10",
|
|
135
|
+
"model": "sonnet",
|
|
136
|
+
"pred_min": 1.691,
|
|
137
|
+
"actual_min": 5.927,
|
|
138
|
+
"delta": {
|
|
139
|
+
"abs": 4.2357,
|
|
140
|
+
"pct": 250.5
|
|
141
|
+
},
|
|
142
|
+
"cost_usd": 0.715954,
|
|
143
|
+
"first_time_pass": null
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"task": "T4",
|
|
147
|
+
"model": "sonnet",
|
|
148
|
+
"pred_min": 1.847,
|
|
149
|
+
"actual_min": 4.74,
|
|
150
|
+
"delta": {
|
|
151
|
+
"abs": 2.893,
|
|
152
|
+
"pct": 156.6
|
|
153
|
+
},
|
|
154
|
+
"cost_usd": 0.393466,
|
|
155
|
+
"first_time_pass": null
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"task": "T13",
|
|
159
|
+
"model": "sonnet",
|
|
160
|
+
"pred_min": 1.4,
|
|
161
|
+
"actual_min": 2.523,
|
|
162
|
+
"delta": {
|
|
163
|
+
"abs": 1.1233,
|
|
164
|
+
"pct": 80.2
|
|
165
|
+
},
|
|
166
|
+
"cost_usd": 0.24823,
|
|
167
|
+
"first_time_pass": null
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"task": "T1",
|
|
171
|
+
"model": "sonnet",
|
|
172
|
+
"pred_min": 1.937,
|
|
173
|
+
"actual_min": 2.073,
|
|
174
|
+
"delta": {
|
|
175
|
+
"abs": 0.1363,
|
|
176
|
+
"pct": 7.0
|
|
177
|
+
},
|
|
178
|
+
"cost_usd": 0.211794,
|
|
179
|
+
"first_time_pass": null
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"task": "T14",
|
|
183
|
+
"model": "sonnet",
|
|
184
|
+
"pred_min": null,
|
|
185
|
+
"actual_min": 7.28,
|
|
186
|
+
"delta": {
|
|
187
|
+
"abs": null,
|
|
188
|
+
"pct": null
|
|
189
|
+
},
|
|
190
|
+
"cost_usd": 0.845344,
|
|
191
|
+
"first_time_pass": null
|
|
192
|
+
}
|
|
193
|
+
],
|
|
194
|
+
"cost_table": {
|
|
195
|
+
"source": "est",
|
|
196
|
+
"usd": 9.3617,
|
|
197
|
+
"tokens": 30849464,
|
|
198
|
+
"wall_min": 119580025.0231,
|
|
199
|
+
"rows": [
|
|
200
|
+
{
|
|
201
|
+
"agent": "a01a9e03e2acb835b",
|
|
202
|
+
"model": "sonnet",
|
|
203
|
+
"wall_min": 1.5649,
|
|
204
|
+
"tokens": 231841,
|
|
205
|
+
"usd": 0.1368
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"agent": "a0e8dea8f85f13dbf",
|
|
209
|
+
"model": "sonnet",
|
|
210
|
+
"wall_min": 9.2805,
|
|
211
|
+
"tokens": 1529221,
|
|
212
|
+
"usd": 0.5023
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"agent": "a1f4933624de7b5ef",
|
|
216
|
+
"model": "sonnet",
|
|
217
|
+
"wall_min": 6.0213,
|
|
218
|
+
"tokens": 1085351,
|
|
219
|
+
"usd": 0.716
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"agent": "a3e35069781629003",
|
|
223
|
+
"model": "sonnet",
|
|
224
|
+
"wall_min": 4.4801,
|
|
225
|
+
"tokens": 1579541,
|
|
226
|
+
"usd": 0.447
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"agent": "a440ab2702c72dcc7",
|
|
230
|
+
"model": "sonnet",
|
|
231
|
+
"wall_min": 3.066,
|
|
232
|
+
"tokens": 1243873,
|
|
233
|
+
"usd": 0.3122
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"agent": "a53be28dbfee6f9f8",
|
|
237
|
+
"model": "sonnet",
|
|
238
|
+
"wall_min": 32.6157,
|
|
239
|
+
"tokens": 9405486,
|
|
240
|
+
"usd": 1.6496
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"agent": "a645d85b6578c5875",
|
|
244
|
+
"model": "sonnet",
|
|
245
|
+
"wall_min": 29894955.8928,
|
|
246
|
+
"tokens": 0,
|
|
247
|
+
"usd": 0.0
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"agent": "a68b5d7f23754c57c",
|
|
251
|
+
"model": "sonnet",
|
|
252
|
+
"wall_min": 12.2574,
|
|
253
|
+
"tokens": 3610245,
|
|
254
|
+
"usd": 1.012
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"agent": "a7cd68767266f8c93",
|
|
258
|
+
"model": "sonnet",
|
|
259
|
+
"wall_min": 7.1753,
|
|
260
|
+
"tokens": 2932728,
|
|
261
|
+
"usd": 0.8604
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"agent": "a7ed9fc0c1c782ec6",
|
|
265
|
+
"model": "sonnet",
|
|
266
|
+
"wall_min": 7.3337,
|
|
267
|
+
"tokens": 1950255,
|
|
268
|
+
"usd": 0.8453
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"agent": "a83ddb1893bfc4ac1",
|
|
272
|
+
"model": "sonnet",
|
|
273
|
+
"wall_min": 29894955.9872,
|
|
274
|
+
"tokens": 0,
|
|
275
|
+
"usd": 0.0
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"agent": "a8aad43f5eeb9f328",
|
|
279
|
+
"model": "sonnet",
|
|
280
|
+
"wall_min": 2.6288,
|
|
281
|
+
"tokens": 733083,
|
|
282
|
+
"usd": 0.2482
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"agent": "a95910dd785a9fb35",
|
|
286
|
+
"model": "sonnet",
|
|
287
|
+
"wall_min": 7.2874,
|
|
288
|
+
"tokens": 3048672,
|
|
289
|
+
"usd": 0.8885
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"agent": "a969fabbbf56da79e",
|
|
293
|
+
"model": "opus",
|
|
294
|
+
"wall_min": 0.4953,
|
|
295
|
+
"tokens": 44207,
|
|
296
|
+
"usd": 0.2016
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"agent": "a9cb67589cc0e1ed2",
|
|
300
|
+
"model": "sonnet",
|
|
301
|
+
"wall_min": 3.6224,
|
|
302
|
+
"tokens": 1278155,
|
|
303
|
+
"usd": 0.3731
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"agent": "ac171c85cba4b85f2",
|
|
307
|
+
"model": "opus",
|
|
308
|
+
"wall_min": 4.4896,
|
|
309
|
+
"tokens": 1108958,
|
|
310
|
+
"usd": 0.7473
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"agent": "acd825d12312cafcd",
|
|
314
|
+
"model": "sonnet",
|
|
315
|
+
"wall_min": 4.1557,
|
|
316
|
+
"tokens": 319327,
|
|
317
|
+
"usd": 0.1328
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"agent": "ad18889d1756b4c8d",
|
|
321
|
+
"model": "sonnet",
|
|
322
|
+
"wall_min": 29894997.312,
|
|
323
|
+
"tokens": 0,
|
|
324
|
+
"usd": 0.0
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"agent": "ad338bf16e18c83e2",
|
|
328
|
+
"model": "sonnet",
|
|
329
|
+
"wall_min": 29895006.0052,
|
|
330
|
+
"tokens": 0,
|
|
331
|
+
"usd": 0.0
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"agent": "af3bca1f1f48b0c08",
|
|
335
|
+
"model": "sonnet",
|
|
336
|
+
"wall_min": 3.3518,
|
|
337
|
+
"tokens": 748521,
|
|
338
|
+
"usd": 0.2886
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
}
|
docs/plans/2026-11-03-own-record.report.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# superlite build report
|
|
2
|
+
|
|
3
|
+
projection: fixed forecast saved at lint 2026-11-03T14:37:44 (plan edited since)
|
|
4
|
+
|
|
5
|
+
Projected Actual Delta
|
|
6
|
+
wall-clock (min) 13.6 61.8 +48.2 (+354%)
|
|
7
|
+
agent cost (USD, +rework) 1.74 9.03 +7.28 (+418%)
|
|
8
|
+
task-minutes 25.8 83.3 +57.6 (+224%)
|
|
9
|
+
agents 13 20 +7 (+54%)
|
|
10
|
+
cost split: agents $6.86, rework $2.17, coordinator $25.29, total $34.32
|
|
11
|
+
concurrency: peak 4 at once (projected 4); critical path actual T3 T6; projected T3 > T4 > T5 > T7 > T8 > T9 > T13 (13.6 min)
|
|
12
|
+
model mix: sonnet 10 tasks $9.03, coordinator opus $25.29
|
|
13
|
+
quality: tests 1765 passed, 0 failed; first-time pass 0/1 (0%); rework dispatches 5; open incidents 38
|
|
14
|
+
|
|
15
|
+
task model pred actual delta cost
|
|
16
|
+
T6 sonnet 1.6 32.5 +30.9 (+1919%) $3.217 first-time miss
|
|
17
|
+
T5 sonnet 1.8 7.3 +5.5 (+310%) $1.201
|
|
18
|
+
T3 sonnet 2.1 7.2 +5.1 (+245%) $0.755
|
|
19
|
+
T8 sonnet 2.5 7.1 +4.7 (+190%) $1.149
|
|
20
|
+
T2 sonnet 2.2 6.6 +4.5 (+205%) $0.291
|
|
21
|
+
(+5 smaller misses; `superlite-cli report --json` lists all)
|
|
22
|
+
|
|
23
|
+
agent cost table:
|
|
24
|
+
| agent | model | wall_min | tokens | usd |
|
|
25
|
+
| --- | --- | --- | --- | --- |
|
|
26
|
+
| a01a9e03e2acb835b | sonnet | 1.5649 | 231841 | 0.1368 |
|
|
27
|
+
| a0e8dea8f85f13dbf | sonnet | 9.2805 | 1529221 | 0.5023 |
|
|
28
|
+
| a1f4933624de7b5ef | sonnet | 6.0213 | 1085351 | 0.716 |
|
|
29
|
+
| a3e35069781629003 | sonnet | 4.4801 | 1579541 | 0.447 |
|
|
30
|
+
| a440ab2702c72dcc7 | sonnet | 3.066 | 1243873 | 0.3122 |
|
|
31
|
+
| a53be28dbfee6f9f8 | sonnet | 32.6157 | 9405486 | 1.6496 |
|
|
32
|
+
| a645d85b6578c5875 | sonnet | 29894955.8928 | 0 | 0.0 |
|
|
33
|
+
| a68b5d7f23754c57c | sonnet | 12.2574 | 3610245 | 1.012 |
|
|
34
|
+
| a7cd68767266f8c93 | sonnet | 7.1753 | 2932728 | 0.8604 |
|
|
35
|
+
| a7ed9fc0c1c782ec6 | sonnet | 7.3337 | 1950255 | 0.8453 |
|
|
36
|
+
| a83ddb1893bfc4ac1 | sonnet | 29894955.9872 | 0 | 0.0 |
|
|
37
|
+
| a8aad43f5eeb9f328 | sonnet | 2.6288 | 733083 | 0.2482 |
|
|
38
|
+
| a95910dd785a9fb35 | sonnet | 7.2874 | 3048672 | 0.8885 |
|
|
39
|
+
| a969fabbbf56da79e | opus | 0.4953 | 44207 | 0.2016 |
|
|
40
|
+
| a9cb67589cc0e1ed2 | sonnet | 3.6224 | 1278155 | 0.3731 |
|
|
41
|
+
| ac171c85cba4b85f2 | opus | 4.4896 | 1108958 | 0.7473 |
|
|
42
|
+
| acd825d12312cafcd | sonnet | 4.1557 | 319327 | 0.1328 |
|
|
43
|
+
| ad18889d1756b4c8d | sonnet | 29894997.312 | 0 | 0.0 |
|
|
44
|
+
| ad338bf16e18c83e2 | sonnet | 29895006.0052 | 0 | 0.0 |
|
|
45
|
+
| af3bca1f1f48b0c08 | sonnet | 3.3518 | 748521 | 0.2886 |
|
|
46
|
+
| totals | | 119580025.0231 | 30849464 | 9.3617 |
|
|
47
|
+
cost source: est
|
|
48
|
+
|
|
49
|
+
takeaway: ran 354% slower than projected; cost 418% over; biggest miss T6
|
plum-examples/game_state_view.plum
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
# WASD, collect the drifting orbs and avoid the mines. Three lives; Enter, R or the
|
|
5
5
|
# "Again" button restarts after the game is over.
|
|
6
6
|
#
|
|
7
|
-
# The game is three plain pieces. `State` is
|
|
7
|
+
# The game is three plain pieces. `State` is plain data: the ship, the orbs and the
|
|
8
|
-
# mines keyed by id, the score, the random seed and the screen
|
|
8
|
+
# mines (recursive `Link` lists keyed by id), the score, the random seed and the screen
|
|
9
|
-
# can happen to it (a tick of time, a resize, a restart)
|
|
9
|
+
# size. `Msg` is what can happen to it (a tick of time, a resize, a restart), an enum
|
|
10
|
-
#
|
|
10
|
+
# with inline variants. `step(state, msg)` is pure and returns the next `State`;
|
|
11
|
-
# `
|
|
11
|
+
# `view(state)` is pure too and builds a fresh `SceneNode` picture every frame, which
|
|
12
|
+
# `Picture.draw` interprets. Both are structural recursion over `Link` and `Nodes`:
|
|
13
|
+
# no loop runs in `step` or `view`. There is no
|
|
12
14
|
# retained scene: the restart button is found by `Picture.hitPath` on the picture
|
|
13
15
|
# the player is looking at. The `World` record is the host shell: it holds the
|
|
14
16
|
# current `State`, reads the keyboard and mouse into messages, and plays sounds.
|
|
@@ -70,8 +72,8 @@ struct Mine(id: Int, x: Float, y: Float, vx: Float, vy: Float, rotation: Float,
|
|
|
70
72
|
# the view needs: the sprite sheet and the render target of the game-over filter.
|
|
71
73
|
record State(
|
|
72
74
|
ship: ShipState,
|
|
73
|
-
orbs:
|
|
75
|
+
orbs: Link[Orb],
|
|
74
|
-
mines:
|
|
76
|
+
mines: Link[Mine],
|
|
75
77
|
seed: Int,
|
|
76
78
|
score: Int,
|
|
77
79
|
best: Int,
|
|
@@ -85,14 +87,41 @@ record State(
|
|
|
85
87
|
target: Int)
|
|
86
88
|
|
|
87
89
|
# What can happen to the state.
|
|
90
|
+
enum Msg =
|
|
88
|
-
|
|
91
|
+
| Tick(dt: Float, ax: Float, ay: Float)
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
| Resize(w: Float, h: Float)
|
|
91
|
-
|
|
92
|
-
# A new run with `seed`, keeping the best score.
|
|
93
|
-
|
|
93
|
+
| Restart(seed: Int)
|
|
94
|
+
|
|
94
|
-
|
|
95
|
+
# A recursive list: the empty `Nil`, or a head in front of another list.
|
|
95
|
-
enum
|
|
96
|
+
enum Link[T] = Nil | Cons(head: T, tail: Link[T])
|
|
97
|
+
|
|
98
|
+
fun mapLink(xs: Link[A], f: fn(A) -> B) -> Link[B] = match xs
|
|
99
|
+
Nil => Nil
|
|
100
|
+
Cons(h, t) => Cons(head: f(h), tail: mapLink(t, f))
|
|
101
|
+
|
|
102
|
+
fun lenLink(xs: Link[T]) -> Int = match xs
|
|
103
|
+
Nil => 0
|
|
104
|
+
Cons(_, t) => 1 + lenLink(t)
|
|
105
|
+
|
|
106
|
+
# `xs` with `x` added at the end.
|
|
107
|
+
fun snoc(xs: Link[T], x: T) -> Link[T] = match xs
|
|
108
|
+
Nil => Cons(head: x, tail: Nil)
|
|
109
|
+
Cons(h, t) => Cons(head: h, tail: snoc(t, x))
|
|
110
|
+
|
|
111
|
+
# `xs` with the element at `i` replaced by `x`.
|
|
112
|
+
fun replaceNth(xs: Link[T], i: Int, x: T) -> Link[T] = match xs
|
|
113
|
+
Nil => Nil
|
|
114
|
+
Cons(h, t) => if i <= 0 then Cons(head: x, tail: t) else Cons(head: h, tail: replaceNth(t, i - 1, x))
|
|
115
|
+
|
|
116
|
+
# The element at `i`, or `dflt` past the end.
|
|
117
|
+
fun nthOr(xs: Link[T], i: Int, dflt: T) -> T = match xs
|
|
118
|
+
Nil => dflt
|
|
119
|
+
Cons(h, t) => if i <= 0 then h else nthOr(t, i - 1, dflt)
|
|
120
|
+
|
|
121
|
+
# The nodes f(x) for every x of `xs`, followed by `rest`.
|
|
122
|
+
fun toNodes(xs: Link[T], f: fn(T) -> SceneNode, rest: Nodes) -> Nodes = match xs
|
|
123
|
+
Nil => rest
|
|
124
|
+
Cons(h, t) => More(head: f(h), tail: toNodes(t, f, rest))
|
|
96
125
|
|
|
97
126
|
# --- small pure helpers ---
|
|
98
127
|
|
|
@@ -193,88 +222,108 @@ fun mineMove(k: Mine, dt: Float) -> Mine =
|
|
|
193
222
|
|
|
194
223
|
# --- the state ---
|
|
195
224
|
|
|
225
|
+
# A built list of orbs and the seed to continue from.
|
|
226
|
+
record OrbsMade(orbs: Link[Orb], seed: Int)
|
|
227
|
+
|
|
228
|
+
record MinesMade(mines: Link[Mine], seed: Int)
|
|
229
|
+
|
|
230
|
+
# Orbs `i` and up to OrbCount, each at a random spot away from the ship.
|
|
231
|
+
fun makeOrbs(i: Int, seed: Int, ship: ShipState) -> OrbsMade =
|
|
232
|
+
if i >= Rules.OrbCount
|
|
233
|
+
return OrbsMade(orbs: Nil, seed: seed)
|
|
234
|
+
sp := spawn(seed, ship.x, ship.y, 120.0, 40.0)
|
|
235
|
+
rest := makeOrbs(i + 1, sp.seed, ship)
|
|
236
|
+
OrbsMade(orbs: Cons(head: orbAt(Id.Orb + i, Float(i) * 0.1, 0.0, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)), tail: rest.orbs), seed: rest.seed)
|
|
237
|
+
|
|
238
|
+
# Mines `i` and up to `count`.
|
|
239
|
+
fun makeMines(i: Int, count: Int, seed: Int, ship: ShipState) -> MinesMade =
|
|
240
|
+
if i >= count
|
|
241
|
+
return MinesMade(mines: Nil, seed: seed)
|
|
242
|
+
sp := spawn(seed, ship.x, ship.y, 220.0, 90.0)
|
|
243
|
+
rest := makeMines(i + 1, count, sp.seed, ship)
|
|
244
|
+
MinesMade(mines: Cons(head: mineAt(Id.Mine + i, 0.0, 2.0, 0.0, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)), tail: rest.mines), seed: rest.seed)
|
|
245
|
+
|
|
196
246
|
fun newState(seed: Int, best: Int, sheet: Int, target: Int, sw: Float, sh: Float) -> State =
|
|
197
247
|
ship := newShip()
|
|
198
|
-
sd := seed
|
|
199
|
-
orbs := List[Orb]()
|
|
200
|
-
|
|
248
|
+
made := makeOrbs(0, seed, ship)
|
|
201
|
-
|
|
249
|
+
mm := makeMines(0, mineCount(0), made.seed, ship)
|
|
202
|
-
sd = sp.seed
|
|
203
|
-
orbs.push(orbAt(Id.Orb + i, Float(i) * 0.1, 0.0, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)))
|
|
204
|
-
mines := List[Mine]()
|
|
205
|
-
for i := range mineCount(0)
|
|
206
|
-
sp := spawn(sd, ship.x, ship.y, 220.0, 90.0)
|
|
207
|
-
sd = sp.seed
|
|
208
|
-
mines.push(mineAt(Id.Mine + i, 0.0, 2.0, 0.0, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)))
|
|
209
250
|
State(
|
|
210
|
-
ship: ship, orbs: orbs, mines: mines, seed:
|
|
251
|
+
ship: ship, orbs: made.orbs, mines: mm.mines, seed: mm.seed, score: 0, best: best, lives: Rules.StartLives,
|
|
211
252
|
over: false, clock: 0.0, overClock: 0.0, sw: sw, sh: sh, sheet: sheet, target: target)
|
|
212
253
|
|
|
254
|
+
fun orbStill(o: Orb) -> Orb =
|
|
255
|
+
orbAt(o.id, o.phase, o.clock, Motion(x: o.x, y: o.y, vx: 0.0, vy: 0.0))
|
|
256
|
+
|
|
257
|
+
fun mineStill(k: Mine) -> Mine =
|
|
258
|
+
mineAt(k.id, k.rotation, 0.0, k.clock, Motion(x: k.x, y: k.y, vx: 0.0, vy: 0.0))
|
|
259
|
+
|
|
213
260
|
# Stops every orb and mine: after game over they only animate.
|
|
214
261
|
fun freeze(s: State) -> State =
|
|
215
262
|
n := State(..s)
|
|
216
|
-
orbs := List[Orb]()
|
|
217
|
-
|
|
263
|
+
n.orbs = mapLink(s.orbs, |o| orbStill(o))
|
|
218
|
-
o := s.orbs.at(i)
|
|
219
|
-
orbs.push(orbAt(o.id, o.phase, o.clock, Motion(x: o.x, y: o.y, vx: 0.0, vy: 0.0)))
|
|
220
|
-
mines := List[Mine]()
|
|
221
|
-
|
|
264
|
+
n.mines = mapLink(s.mines, |k| mineStill(k))
|
|
222
|
-
k := s.mines.at(i)
|
|
223
|
-
mines.push(mineAt(k.id, k.rotation, 0.0, k.clock, Motion(x: k.x, y: k.y, vx: 0.0, vy: 0.0)))
|
|
224
|
-
n.orbs = orbs
|
|
225
|
-
n.mines = mines
|
|
226
265
|
n
|
|
227
266
|
|
|
267
|
+
# The orbs after the ship collected the ones it touches, with the score and seed.
|
|
268
|
+
record Collected(orbs: Link[Orb], seed: Int, score: Int)
|
|
269
|
+
|
|
270
|
+
fun collectOrbs(orbs: Link[Orb], ship: ShipState, seed: Int, score: Int) -> Collected = match orbs
|
|
271
|
+
Nil => Collected(orbs: Nil, seed: seed, score: score)
|
|
272
|
+
Cons(o, rest) =>
|
|
273
|
+
if overlaps(ship.x, ship.y, Rules.ShipRadius, o.x, o.y, Rules.OrbRadius)
|
|
274
|
+
sp := spawn(seed, ship.x, ship.y, 120.0, 40.0)
|
|
275
|
+
later := collectOrbs(rest, ship, sp.seed, score + Rules.OrbPoints)
|
|
276
|
+
moved := orbAt(o.id, o.phase, o.clock, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy))
|
|
277
|
+
Collected(orbs: Cons(head: moved, tail: later.orbs), seed: later.seed, score: later.score)
|
|
278
|
+
else
|
|
279
|
+
kept := collectOrbs(rest, ship, seed, score)
|
|
280
|
+
Collected(orbs: Cons(head: o, tail: kept.orbs), seed: kept.seed, score: kept.score)
|
|
281
|
+
|
|
282
|
+
# `mines` grown (new mines go at the end) until there are `want`.
|
|
283
|
+
fun growMines(mines: Link[Mine], have: Int, want: Int, seed: Int, ship: ShipState) -> MinesMade =
|
|
284
|
+
if have >= want
|
|
285
|
+
return MinesMade(mines: mines, seed: seed)
|
|
286
|
+
sp := spawn(seed, ship.x, ship.y, 220.0, 90.0)
|
|
287
|
+
added := snoc(mines, mineAt(Id.Mine + have, 0.0, 2.0, 0.0, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)))
|
|
288
|
+
growMines(added, have + 1, want, sp.seed, ship)
|
|
289
|
+
|
|
290
|
+
fun touchesMine(mines: Link[Mine], ship: ShipState) -> Bool = match mines
|
|
291
|
+
Nil => false
|
|
292
|
+
Cons(k, rest) => overlaps(ship.x, ship.y, Rules.ShipRadius, k.x, k.y, Rules.MineRadius) || touchesMine(rest, ship)
|
|
293
|
+
|
|
228
294
|
# One frame of time. Once the game is over the actors only animate.
|
|
229
295
|
fun tick(s: State, dt: Float, ax: Float, ay: Float) -> State =
|
|
230
296
|
n := State(..s)
|
|
231
297
|
n.clock = s.clock + dt
|
|
232
|
-
orbs := List[Orb]()
|
|
233
|
-
for i := range s.orbs.length()
|
|
234
|
-
|
|
298
|
+
n.orbs = mapLink(s.orbs, |o| orbMove(o, dt))
|
|
235
|
-
mines := List[Mine]()
|
|
236
|
-
for i := range s.mines.length()
|
|
237
|
-
|
|
299
|
+
n.mines = mapLink(s.mines, |k| mineMove(k, dt))
|
|
238
|
-
n.orbs = orbs
|
|
239
|
-
n.mines = mines
|
|
240
300
|
if s.over
|
|
241
301
|
n.overClock = s.overClock + dt
|
|
242
302
|
return n
|
|
243
303
|
ship := shipStep(s.ship, ax, ay, dt)
|
|
244
304
|
n.ship = ship
|
|
245
|
-
collected := List[Orb]()
|
|
246
|
-
for i := range orbs.length()
|
|
247
|
-
o := orbs.at(i)
|
|
248
|
-
if overlaps(ship.x, ship.y, Rules.ShipRadius, o.x, o.y, Rules.OrbRadius)
|
|
249
|
-
n.score = n.score + Rules.OrbPoints
|
|
250
|
-
|
|
305
|
+
got := collectOrbs(n.orbs, ship, n.seed, n.score)
|
|
306
|
+
n.orbs = got.orbs
|
|
251
|
-
|
|
307
|
+
n.seed = got.seed
|
|
252
|
-
collected.push(orbAt(o.id, o.phase, o.clock, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)))
|
|
253
|
-
else
|
|
254
|
-
collected.push(o)
|
|
255
|
-
n.
|
|
308
|
+
n.score = got.score
|
|
256
|
-
|
|
309
|
+
grown := growMines(n.mines, lenLink(n.mines), mineCount(n.score), n.seed, ship)
|
|
257
|
-
|
|
310
|
+
n.mines = grown.mines
|
|
258
|
-
|
|
311
|
+
n.seed = grown.seed
|
|
259
|
-
n.mines.push(mineAt(Id.Mine + n.mines.length(), 0.0, 2.0, 0.0, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)))
|
|
260
|
-
for i := range n.mines.length()
|
|
261
|
-
k := n.mines.at(i)
|
|
262
|
-
|
|
312
|
+
if n.ship.hurt <= 0.0 && touchesMine(n.mines, ship)
|
|
263
|
-
|
|
313
|
+
n.lives = n.lives - 1
|
|
264
|
-
|
|
314
|
+
n.ship = ShipState(x: ship.x, y: ship.y, vx: ship.vx, vy: ship.vy, rotation: ship.rotation, hurt: Rules.HurtTime)
|
|
265
|
-
|
|
315
|
+
if n.lives <= 0
|
|
266
|
-
|
|
316
|
+
n.over = true
|
|
267
|
-
|
|
317
|
+
n.best = n.best.max(n.score)
|
|
268
318
|
if n.over
|
|
269
319
|
return freeze(n)
|
|
270
320
|
n
|
|
271
321
|
|
|
272
322
|
# The next state: pure, nothing that is passed in is changed.
|
|
273
|
-
fun step(s: State, msg: Msg) -> State =
|
|
323
|
+
fun step(s: State, msg: Msg) -> State = match msg
|
|
274
|
-
match msg
|
|
275
|
-
|
|
324
|
+
Tick(dt, ax, ay) => tick(s, dt, ax, ay)
|
|
276
|
-
|
|
325
|
+
Resize(w, h) => resized(s, w, h)
|
|
277
|
-
|
|
326
|
+
Restart(seed) => newState(seed, s.best.max(s.score), s.sheet, s.target, s.sw, s.sh)
|
|
278
327
|
|
|
279
328
|
fun resized(s: State, w: Float, h: Float) -> State =
|
|
280
329
|
n := State(..s)
|
|
@@ -304,86 +353,90 @@ fun starView(i: Int, clock: Float) -> SceneNode =
|
|
|
304
353
|
twinkle := 0.25 + 0.2 * Math.sin(clock * 2.0 + Float(i))
|
|
305
354
|
CircleShape(cx: Float({i * 7919} % 800), cy: Float({i * 104729} % 600), r: r, color: Color(r: 0.7, g: 0.8, b: 1.0, a: twinkle))
|
|
306
355
|
|
|
356
|
+
# The stars `i` and up to 40.
|
|
357
|
+
fun starNodes(i: Int, clock: Float) -> Nodes =
|
|
358
|
+
if i >= 40 then End else More(head: starView(i, clock), tail: starNodes(i + 1, clock))
|
|
359
|
+
|
|
307
360
|
fun starsView(clock: Float) -> SceneNode =
|
|
308
|
-
kids := List[SceneNode]()
|
|
309
|
-
for i := range 40
|
|
310
|
-
kids.push(starView(i, clock))
|
|
311
|
-
Group(transform:
|
|
361
|
+
Group(transform: Identity, children: starNodes(0, clock))
|
|
312
362
|
|
|
313
363
|
fun mineView(sheet: Int, k: Mine) -> SceneNode =
|
|
314
364
|
Group(
|
|
315
|
-
transform:
|
|
365
|
+
transform: Compose(first: Translate(dx: k.x, dy: k.y), then: Rotate(radians: k.rotation)),
|
|
316
|
-
children:
|
|
366
|
+
children: More(head: cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(k.clock, 4, 4.0), 2), tail: End))
|
|
317
367
|
|
|
318
368
|
# The glow is drawn additively, then the sprite on top.
|
|
319
369
|
fun orbView(sheet: Int, o: Orb) -> SceneNode =
|
|
320
370
|
glow := Group(
|
|
321
|
-
transform:
|
|
371
|
+
transform: Identity,
|
|
322
|
-
children:
|
|
372
|
+
children: More(
|
|
323
|
-
CircleShape(cx: 0.0, cy: 0.0, r: 26.0, color: Color(r: 0.35, g: 0.22, b: 0.0, a: 0.5)),
|
|
373
|
+
head: CircleShape(cx: 0.0, cy: 0.0, r: 26.0, color: Color(r: 0.35, g: 0.22, b: 0.0, a: 0.5)),
|
|
324
|
-
CircleShape(cx: 0.0, cy: 0.0, r: 17.0, color: Color(r: 0.6, g: 0.4, b: 0.05, a: 0.5))))
|
|
374
|
+
tail: More(head: CircleShape(cx: 0.0, cy: 0.0, r: 17.0, color: Color(r: 0.6, g: 0.4, b: 0.05, a: 0.5)), tail: End)))
|
|
325
375
|
Group(
|
|
326
|
-
transform:
|
|
376
|
+
transform: Translate(dx: o.x, dy: o.y),
|
|
327
|
-
children:
|
|
377
|
+
children: More(
|
|
328
|
-
Filter(effect: BlendFx(mode: Blend.Add), child: glow, target: 0 - 1),
|
|
378
|
+
head: Filter(effect: BlendFx(mode: Blend.Add), child: glow, target: 0 - 1),
|
|
329
|
-
cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(o.clock + o.phase, 4, 8.0), 1)))
|
|
379
|
+
tail: More(head: cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(o.clock + o.phase, 4, 8.0), 1), tail: End)))
|
|
330
380
|
|
|
331
381
|
# The ship blinks while hurt and is gone once the game is over (an empty group keeps the slots).
|
|
332
382
|
fun shipView(s: State) -> SceneNode =
|
|
333
383
|
blink := s.ship.hurt > 0.0 && frameOf(s.clock, 2, 10.0) == 0
|
|
334
384
|
if s.over || blink
|
|
335
|
-
return Group(transform:
|
|
385
|
+
return Group(transform: Identity, children: End)
|
|
336
386
|
Group(
|
|
337
|
-
transform:
|
|
387
|
+
transform: Compose(first: Translate(dx: s.ship.x, dy: s.ship.y), then: Rotate(radians: s.ship.rotation)),
|
|
338
|
-
children:
|
|
388
|
+
children: More(
|
|
339
|
-
CircleShape(cx: 0.0, cy: 0.0, r: 17.0, color: Color(r: 0.4, g: 0.8, b: 1.0, a: 0.12)),
|
|
389
|
+
head: CircleShape(cx: 0.0, cy: 0.0, r: 17.0, color: Color(r: 0.4, g: 0.8, b: 1.0, a: 0.12)),
|
|
340
|
-
cell(s.sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(s.clock, 4, 14.0), 0)))
|
|
390
|
+
tail: More(head: cell(s.sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(s.clock, 4, 14.0), 0), tail: End)))
|
|
391
|
+
|
|
392
|
+
# One life icon per remaining life, from the right edge going left.
|
|
393
|
+
fun lifeNodes(i: Int, lives: Int, sheet: Int) -> Nodes =
|
|
394
|
+
if i >= lives
|
|
395
|
+
End
|
|
396
|
+
else
|
|
397
|
+
More(head: cell(sheet, Rules.ArenaW - 40.0 - Float(i) * 36.0, 12.0, 32.0, 0, 0), tail: lifeNodes(i + 1, lives, sheet))
|
|
341
398
|
|
|
342
399
|
fun hudView(s: State) -> SceneNode =
|
|
400
|
+
Group(
|
|
401
|
+
transform: Identity,
|
|
343
|
-
|
|
402
|
+
children: More(
|
|
344
|
-
|
|
403
|
+
head: Text(text: "Score {s.score}", x: 16.0, y: 12.0, size: 28.0, color: Color.White),
|
|
345
|
-
|
|
404
|
+
tail: More(head: Text(text: "Best {s.best.max(s.score)}", x: 16.0, y: 44.0, size: 20.0, color: Color.Yellow), tail: lifeNodes(0, s.lives, s.sheet))))
|
|
346
|
-
for i := range s.lives
|
|
347
|
-
kids.push(cell(s.sheet, Rules.ArenaW - 40.0 - Float(i) * 36.0, 12.0, 32.0, 0, 0))
|
|
348
|
-
Group(transform: List[Transform](), children: kids)
|
|
349
405
|
|
|
350
406
|
# The game-over panel with the restart button, faded in through a render-target filter.
|
|
351
407
|
# While playing it is an empty group, so nothing in it can be hit.
|
|
352
408
|
fun overlayView(s: State) -> SceneNode =
|
|
353
409
|
if !s.over
|
|
354
|
-
return Group(transform:
|
|
410
|
+
return Group(transform: Identity, children: End)
|
|
355
411
|
button := Group(
|
|
356
|
-
transform:
|
|
412
|
+
transform: Identity,
|
|
357
|
-
children:
|
|
413
|
+
children: More(
|
|
358
|
-
RectShape(x: 340.0, y: 342.0, w: 120.0, h: 44.0, color: Color(r: 0.5, g: 0.8, b: 1.0, a: 0.9)),
|
|
414
|
+
head: RectShape(x: 340.0, y: 342.0, w: 120.0, h: 44.0, color: Color(r: 0.5, g: 0.8, b: 1.0, a: 0.9)),
|
|
359
|
-
Text(text: "Again", x: 372.0, y: 352.0, size: 24.0, color: Color.White)))
|
|
415
|
+
tail: More(head: Text(text: "Again", x: 372.0, y: 352.0, size: 24.0, color: Color.White), tail: End)))
|
|
360
416
|
panel := Group(
|
|
361
|
-
transform:
|
|
417
|
+
transform: Identity,
|
|
362
|
-
children:
|
|
418
|
+
children: More(
|
|
363
|
-
RectShape(x: 0.0, y: 230.0, w: Rules.ArenaW, h: 170.0, color: Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6)),
|
|
419
|
+
head: RectShape(x: 0.0, y: 230.0, w: Rules.ArenaW, h: 170.0, color: Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6)),
|
|
420
|
+
tail: More(
|
|
364
|
-
|
|
421
|
+
head: Text(text: "Game over", x: 300.0, y: 244.0, size: 48.0, color: Color.Red),
|
|
422
|
+
tail: More(
|
|
365
|
-
|
|
423
|
+
head: Text(text: "Press Enter to play again", x: 270.0, y: 300.0, size: 24.0, color: Color.White),
|
|
366
|
-
|
|
424
|
+
tail: More(head: button, tail: End)))))
|
|
367
425
|
Filter(effect: Alpha(alpha: {s.overClock * 2.0}.min(1.0)), child: panel, target: s.target)
|
|
368
426
|
|
|
369
427
|
# The whole picture, in drawing order, under the camera that fits the arena on screen.
|
|
370
428
|
fun view(s: State) -> SceneNode =
|
|
371
|
-
mines := List[SceneNode]()
|
|
372
|
-
for i := range s.mines.length()
|
|
373
|
-
mines.push(mineView(s.sheet, s.mines.at(i)))
|
|
374
|
-
orbs := List[SceneNode]()
|
|
375
|
-
for i := range s.orbs.length()
|
|
376
|
-
orbs.push(orbView(s.sheet, s.orbs.at(i)))
|
|
377
|
-
layers := List[SceneNode]()
|
|
378
|
-
layers.push(RectShape(x: 0.0, y: 0.0, w: Rules.ArenaW, h: Rules.ArenaH, color: Color.rgb(0.07, 0.08, 0.16)))
|
|
379
|
-
layers.push(starsView(s.clock))
|
|
380
|
-
layers.push(Group(transform: List[Transform](), children: mines))
|
|
381
|
-
layers.push(Group(transform: List[Transform](), children: orbs))
|
|
382
|
-
layers.push(shipView(s))
|
|
383
|
-
layers.push(hudView(s))
|
|
384
|
-
layers.push(overlayView(s))
|
|
385
429
|
z := zoomOf(s)
|
|
430
|
+
layers := More(
|
|
431
|
+
head: RectShape(x: 0.0, y: 0.0, w: Rules.ArenaW, h: Rules.ArenaH, color: Color.rgb(0.07, 0.08, 0.16)),
|
|
432
|
+
tail: More(
|
|
433
|
+
head: starsView(s.clock),
|
|
434
|
+
tail: More(
|
|
435
|
+
head: Group(transform: Identity, children: toNodes(s.mines, |k| mineView(s.sheet, k), End)),
|
|
436
|
+
tail: More(
|
|
437
|
+
head: Group(transform: Identity, children: toNodes(s.orbs, |o| orbView(s.sheet, o), End)),
|
|
438
|
+
tail: More(head: shipView(s), tail: More(head: hudView(s), tail: More(head: overlayView(s), tail: End)))))))
|
|
386
|
-
Group(transform:
|
|
439
|
+
Group(transform: Compose(first: Translate(dx: offsetX(s), dy: offsetY(s)), then: Scale(sx: z, sy: z)), children: layers)
|
|
387
440
|
|
|
388
441
|
# True when (x, y) in screen pixels is on the restart button of the picture `view` builds.
|
|
389
442
|
fun onRestart(s: State, x: Float, y: Float) -> Bool =
|
|
@@ -461,24 +514,12 @@ fun away() -> Motion =
|
|
|
461
514
|
|
|
462
515
|
fun withOrb(s: State, i: Int, m: Motion) -> State =
|
|
463
516
|
n := State(..s)
|
|
464
|
-
orbs := List[Orb]()
|
|
465
|
-
for j := range s.orbs.length()
|
|
466
|
-
if j == i
|
|
467
|
-
|
|
517
|
+
n.orbs = replaceNth(s.orbs, i, orbAt(Id.Orb + i, Float(i) * 0.1, 0.0, m))
|
|
468
|
-
else
|
|
469
|
-
orbs.push(s.orbs.at(j))
|
|
470
|
-
n.orbs = orbs
|
|
471
518
|
n
|
|
472
519
|
|
|
473
520
|
fun withMine(s: State, i: Int, m: Motion) -> State =
|
|
474
521
|
n := State(..s)
|
|
475
|
-
mines := List[Mine]()
|
|
476
|
-
for j := range s.mines.length()
|
|
477
|
-
if j == i
|
|
478
|
-
|
|
522
|
+
n.mines = replaceNth(s.mines, i, mineAt(Id.Mine + i, 0.0, 2.0, 0.0, m))
|
|
479
|
-
else
|
|
480
|
-
mines.push(s.mines.at(j))
|
|
481
|
-
n.mines = mines
|
|
482
523
|
n
|
|
483
524
|
|
|
484
525
|
# A state with the orbs and mines moved out of the way.
|
|
@@ -486,10 +527,16 @@ fun emptyState() -> State =
|
|
|
486
527
|
s := newState(7, 0, -1, -1, Rules.ArenaW, Rules.ArenaH)
|
|
487
528
|
for i := range Rules.OrbCount
|
|
488
529
|
s = withOrb(s, i, away())
|
|
489
|
-
for i := range s.mines
|
|
530
|
+
for i := range lenLink(s.mines)
|
|
490
531
|
s = withMine(s, i, away())
|
|
491
532
|
s
|
|
492
533
|
|
|
534
|
+
fun orbN(s: State, i: Int) -> Orb =
|
|
535
|
+
nthOr(s.orbs, i, orbAt(0, 0.0, 0.0, away()))
|
|
536
|
+
|
|
537
|
+
fun mineN(s: State, i: Int) -> Mine =
|
|
538
|
+
nthOr(s.mines, i, mineAt(0, 0.0, 0.0, 0.0, away()))
|
|
539
|
+
|
|
493
540
|
fun onShip(s: State) -> Motion =
|
|
494
541
|
Motion(x: s.ship.x, y: s.ship.y, vx: 0.0, vy: 0.0)
|
|
495
542
|
|
|
@@ -500,15 +547,14 @@ fun forceOver(s: State) -> State =
|
|
|
500
547
|
n.best = s.best.max(s.score)
|
|
501
548
|
n
|
|
502
549
|
|
|
503
|
-
fun countNodes(n: SceneNode) -> Int =
|
|
550
|
+
fun countNodes(n: SceneNode) -> Int = match n
|
|
504
|
-
match n
|
|
505
|
-
Group =>
|
|
506
|
-
total := 1
|
|
507
|
-
for i := range n.children.length()
|
|
508
|
-
|
|
551
|
+
Group(_, kids) => 1 + countKids(kids)
|
|
509
|
-
total
|
|
510
|
-
|
|
552
|
+
Filter(_, child, _) => 1 + countNodes(child)
|
|
511
|
-
|
|
553
|
+
_ => 1
|
|
554
|
+
|
|
555
|
+
fun countKids(ks: Nodes) -> Int = match ks
|
|
556
|
+
End => 0
|
|
557
|
+
More(h, t) => countNodes(h) + countKids(t)
|
|
512
558
|
|
|
513
559
|
test "steering accelerates and drag slows the ship"
|
|
514
560
|
s := shipStep(newShip(), 1.0, 0.0, 0.1)
|
|
@@ -551,11 +597,11 @@ test "a mine spins as it drifts"
|
|
|
551
597
|
test "collecting an orb scores and respawns it elsewhere"
|
|
552
598
|
s := withOrb(emptyState(), 0, onShip(emptyState()))
|
|
553
599
|
t := tickOf(s, 0.0, 0.0, 0.016)
|
|
554
|
-
orb :=
|
|
600
|
+
orb := orbN(t, 0)
|
|
555
601
|
assert t.score == Rules.OrbPoints
|
|
556
602
|
assert Math.hypot(orb.x - t.ship.x, orb.y - t.ship.y) >= 120.0
|
|
557
603
|
assert orb.x < 5000.0
|
|
558
|
-
assert t.orbs
|
|
604
|
+
assert lenLink(t.orbs) == Rules.OrbCount
|
|
559
605
|
|
|
560
606
|
test "a mine costs one life and makes the ship flash"
|
|
561
607
|
e := emptyState()
|
|
@@ -604,15 +650,15 @@ test "after game over nothing moves but the animations keep running"
|
|
|
604
650
|
s.lives = 1
|
|
605
651
|
t := tickOf(withMine(s, 0, Motion(x: s.ship.x, y: s.ship.y, vx: 10.0, vy: 0.0)), 0.0, 0.0, 0.016)
|
|
606
652
|
assert t.over
|
|
607
|
-
mine :=
|
|
653
|
+
mine := mineN(t, 0)
|
|
608
654
|
for i := range 10
|
|
609
655
|
t = tickOf(t, 1.0, 0.0, 0.1)
|
|
610
|
-
after :=
|
|
656
|
+
after := mineN(t, 0)
|
|
611
657
|
assert after.x == mine.x
|
|
612
658
|
assert after.rotation == mine.rotation
|
|
613
659
|
assert t.ship.x == Rules.ArenaW / 2.0
|
|
614
660
|
assert after.clock > mine.clock + 0.9
|
|
615
|
-
assert
|
|
661
|
+
assert orbN(t, 0).clock > 0.9
|
|
616
662
|
assert t.clock > 0.9
|
|
617
663
|
assert t.score == 0
|
|
618
664
|
|
|
@@ -627,7 +673,7 @@ test "restart resets the run and keeps the best score"
|
|
|
627
673
|
assert r.best == 90
|
|
628
674
|
assert r.lives == Rules.StartLives
|
|
629
675
|
assert r.ship.x == Rules.ArenaW / 2.0
|
|
630
|
-
assert r.mines
|
|
676
|
+
assert lenLink(r.mines) == mineCount(0)
|
|
631
677
|
assert countNodes(shipView(r)) == 3
|
|
632
678
|
assert countNodes(overlayView(r)) == 1
|
|
633
679
|
|
|
@@ -639,9 +685,9 @@ test "more mines appear as the score grows and restart removes them"
|
|
|
639
685
|
s := State(..e)
|
|
640
686
|
s.score = 100
|
|
641
687
|
t := tickOf(s, 0.0, 0.0, 0.016)
|
|
642
|
-
assert t.mines
|
|
688
|
+
assert lenLink(t.mines) == 5
|
|
643
689
|
r := step(t, Restart(seed: 3))
|
|
644
|
-
assert r.mines
|
|
690
|
+
assert lenLink(r.mines) == 3
|
|
645
691
|
|
|
646
692
|
test "the same seed plays out the same way"
|
|
647
693
|
a := newState(11, 0, -1, -1, Rules.ArenaW, Rules.ArenaH)
|
|
@@ -651,9 +697,9 @@ test "the same seed plays out the same way"
|
|
|
651
697
|
b = tickOf(b, 1.0, -1.0, 0.016)
|
|
652
698
|
assert a.ship.x == b.ship.x
|
|
653
699
|
assert a.score == b.score
|
|
654
|
-
assert
|
|
700
|
+
assert mineN(a, 0).x == mineN(b, 0).x
|
|
655
701
|
other := newState(12, 0, -1, -1, Rules.ArenaW, Rules.ArenaH)
|
|
656
|
-
assert
|
|
702
|
+
assert orbN(other, 0).x != orbN(a, 0).x
|
|
657
703
|
|
|
658
704
|
test "fit centres the arena on the screen"
|
|
659
705
|
s := step(emptyState(), Resize(w: 1600.0, h: 600.0))
|
|
@@ -697,14 +743,14 @@ test "freezing at game over keeps the orb and mine animation clocks"
|
|
|
697
743
|
t := emptyState()
|
|
698
744
|
for i := range 5
|
|
699
745
|
t = tickOf(t, 0.0, 0.0, 0.1)
|
|
700
|
-
orbClock :=
|
|
746
|
+
orbClock := orbN(t, 0).clock
|
|
701
|
-
mineClock :=
|
|
747
|
+
mineClock := mineN(t, 0).clock
|
|
702
748
|
assert orbClock > 0.4
|
|
703
749
|
t.lives = 1
|
|
704
750
|
t = tickOf(withMine(t, 1, onShip(t)), 0.0, 0.0, 0.1)
|
|
705
751
|
assert t.over
|
|
706
|
-
assert
|
|
752
|
+
assert orbN(t, 0).clock > orbClock
|
|
707
|
-
assert
|
|
753
|
+
assert mineN(t, 0).clock > mineClock
|
|
708
754
|
|
|
709
755
|
test "view_is_pure"
|
|
710
756
|
s := emptyState()
|
plum-game/web-test/fixtures/stress_state_view.plum
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
# The stress scene of stress_pure_scene.plum written as state and view
|
|
1
|
+
# The stress scene of stress_pure_scene.plum written as state and view, now recursive:
|
|
2
|
-
# 50 slowly rotating groups, 500 rects and 200 circles (sprites of a
|
|
2
|
+
# 5000 sprites in 50 slowly rotating groups, 500 rects and 200 circles (sprites of a
|
|
3
|
-
# 50 texts with a changing number, drawn through a 0.8 camera.
|
|
3
|
+
# 1x1 white texture), 50 texts with a changing number, drawn through a 0.8 camera.
|
|
4
|
+
# `State` is recursive `Link` lists (each spinner owns the `Link` of its 100 dancers);
|
|
4
|
-
#
|
|
5
|
+
# `step` is pure, a structural recursion that returns the next state; `view` is pure
|
|
5
|
-
#
|
|
6
|
+
# and rebuilds the whole SceneNode tree every frame with recursive functions, which
|
|
6
|
-
# `Picture.draw` interprets. Nothing is retained between frames but `State`.
|
|
7
|
+
# `Picture.draw` interprets. Nothing is retained between frames but `State`. The
|
|
8
|
+
# deepest recursion is the 700 drifters; no loops run in step or view.
|
|
7
9
|
module stress_state_view
|
|
8
10
|
import std/Str
|
|
9
11
|
import std/Int
|
|
@@ -23,21 +25,47 @@ primitive Counts =
|
|
|
23
25
|
WorldW = 1600
|
|
24
26
|
WorldH = 900
|
|
25
27
|
|
|
28
|
+
enum Link[T] = Nil | Cons(head: T, tail: Link[T])
|
|
29
|
+
|
|
30
|
+
fun mapLink(xs: Link[A], f: fn(A) -> B) -> Link[B] = match xs
|
|
31
|
+
Nil => Nil
|
|
26
|
-
|
|
32
|
+
Cons(h, t) => Cons(head: f(h), tail: mapLink(t, f))
|
|
33
|
+
|
|
34
|
+
# The list f(i), f(i + 1) ... f(n - 1).
|
|
27
|
-
|
|
35
|
+
fun rangeLink(i: Int, n: Int, f: fn(Int) -> T) -> Link[T] =
|
|
36
|
+
if i >= n then Nil else Cons(head: f(i), tail: rangeLink(i + 1, n, f))
|
|
37
|
+
|
|
38
|
+
fun appendLink(xs: Link[T], ys: Link[T]) -> Link[T] = match xs
|
|
39
|
+
Nil => ys
|
|
40
|
+
Cons(h, t) => Cons(head: h, tail: appendLink(t, ys))
|
|
41
|
+
|
|
42
|
+
# The element at `i`, or `dflt` past the end.
|
|
43
|
+
fun nthOr(xs: Link[T], i: Int, dflt: T) -> T = match xs
|
|
44
|
+
Nil => dflt
|
|
45
|
+
Cons(h, t) => if i <= 0 then h else nthOr(t, i - 1, dflt)
|
|
46
|
+
|
|
47
|
+
# The nodes f(x) for every x of `xs`, followed by `rest`.
|
|
48
|
+
fun toNodes(xs: Link[T], f: fn(T) -> SceneNode, rest: Nodes) -> Nodes = match xs
|
|
49
|
+
Nil => rest
|
|
50
|
+
Cons(h, t) => More(head: f(h), tail: toNodes(t, f, rest))
|
|
51
|
+
|
|
52
|
+
fun countNodes(ns: Nodes) -> Int = match ns
|
|
53
|
+
End => 0
|
|
54
|
+
More(_, t) => 1 + countNodes(t)
|
|
28
55
|
|
|
29
56
|
# A sprite of the sheet inside its group, spinning about its centre.
|
|
30
57
|
struct Dancer(id: Int, x: Float, y: Float, rotation: Float, srcX: Float)
|
|
31
58
|
|
|
59
|
+
# A rotating group of dancers: top-left (x, y), pivot (75, 85).
|
|
60
|
+
record Spinner(id: Int, x: Float, y: Float, rotation: Float, dancers: Link[Dancer])
|
|
61
|
+
|
|
32
62
|
# A square of the white texture that moves by (dx, dy) per frame and wraps.
|
|
33
63
|
struct Drifter(id: Int, x: Float, y: Float, w: Float, h: Float, dx: Float, dy: Float, color: Color)
|
|
34
64
|
|
|
35
65
|
# Shows "n <count>" and counts up each frame.
|
|
36
66
|
struct Counter(id: Int, x: Float, y: Float, n: Int)
|
|
37
67
|
|
|
38
|
-
record State(
|
|
39
|
-
sheet: Int, white: Int,
|
|
40
|
-
|
|
68
|
+
record State(sheet: Int, white: Int, spinners: Link[Spinner], drifters: Link[Drifter], counters: Link[Counter])
|
|
41
69
|
|
|
42
70
|
fun hue(i: Int) -> Color =
|
|
43
71
|
Color(r: Float(i % 7) / 7.0, g: Float(i % 5) / 5.0, b: 0.4 + Float(i % 3) / 5.0, a: 0.8)
|
|
@@ -45,26 +73,20 @@ fun hue(i: Int) -> Color =
|
|
|
45
73
|
fun newDrifter(id: Int, c: Color, x: Float, y: Float, w: Float, h: Float, dx: Float, dy: Float) -> Drifter =
|
|
46
74
|
Drifter(id: id, x: x, y: y, w: w, h: h, dx: dx, dy: dy, color: c)
|
|
47
75
|
|
|
76
|
+
fun newDancer(gi: Int, j: Int) -> Dancer =
|
|
77
|
+
i := gi * {Counts.Sprites / Counts.Groups} + j
|
|
78
|
+
Dancer(id: 1000 + i, x: Float({j * j * 13 + j * 37} % 150), y: Float({j * j * 29 + j * 53} % 170), rotation: Float(i), srcX: Float(i % 4 * 16))
|
|
79
|
+
|
|
80
|
+
fun newSpinner(gi: Int) -> Spinner =
|
|
81
|
+
Spinner(id: 100 + gi, x: Float(gi % 10 * 160), y: Float(gi / 10 * 180), rotation: 0.0, dancers: rangeLink(0, Counts.Sprites / Counts.Groups, |j| newDancer(gi, j)))
|
|
82
|
+
|
|
48
83
|
fun newState(sheet: Int, white: Int) -> State =
|
|
49
|
-
per := Counts.Sprites / Counts.Groups
|
|
50
|
-
spinners := List[Spinner]()
|
|
51
|
-
dancers := List[Dancer]()
|
|
52
|
-
for gi := range Counts.Groups
|
|
53
|
-
spinners.push(Spinner(id: 100 + gi, x: Float(gi % 10 * 160), y: Float(gi / 10 * 180), rotation: 0.0))
|
|
54
|
-
for j := range per
|
|
55
|
-
i := gi * per + j
|
|
56
|
-
dancers.push(Dancer(
|
|
57
|
-
id: 1000 + i, x: Float({j * j * 13 + j * 37} % 150), y: Float({j * j * 29 + j * 53} % 170),
|
|
58
|
-
rotation: Float(i), srcX: Float(i % 4 * 16)))
|
|
59
|
-
drifters := List[Drifter]()
|
|
60
|
-
for i := range Counts.Rects
|
|
61
|
-
|
|
84
|
+
rects := rangeLink(0, Counts.Rects, |i| newDrifter(10000 + i, hue(i), Float(i * 61 % Counts.WorldW), Float(i * 29 % Counts.WorldH), 12.0, 8.0, 2.0, 0.0))
|
|
62
|
-
for i := range Counts.Circles
|
|
63
|
-
|
|
85
|
+
circles := rangeLink(0, Counts.Circles, |i| newDrifter(20000 + i, hue(i + 3), Float(i * 83 % Counts.WorldW), Float(i * 47 % Counts.WorldH), 12.0, 12.0, 3.0, 1.0))
|
|
86
|
+
State(
|
|
87
|
+
sheet: sheet, white: white, spinners: rangeLink(0, Counts.Groups, |gi| newSpinner(gi)),
|
|
64
|
-
|
|
88
|
+
drifters: appendLink(rects, circles),
|
|
65
|
-
for i := range Counts.Texts
|
|
66
|
-
counters
|
|
89
|
+
counters: rangeLink(0, Counts.Texts, |i| Counter(id: 30000 + i, x: Float(i * 30 % Counts.WorldW), y: Float(i * 17 % Counts.WorldH), n: i)))
|
|
67
|
-
State(sheet: sheet, white: white, spinners: spinners, dancers: dancers, drifters: drifters, counters: counters)
|
|
68
90
|
|
|
69
91
|
# --- step: pure ---
|
|
70
92
|
|
|
@@ -77,56 +99,46 @@ fun stepDrifter(d: Drifter) -> Drifter =
|
|
|
77
99
|
ny = ny - Float(Counts.WorldH)
|
|
78
100
|
Drifter(id: d.id, x: nx, y: ny, w: d.w, h: d.h, dx: d.dx, dy: d.dy, color: d.color)
|
|
79
101
|
|
|
102
|
+
fun stepDancer(d: Dancer, dt: Float) -> Dancer =
|
|
103
|
+
Dancer(id: d.id, x: d.x, y: d.y, rotation: d.rotation + 1.2 * dt, srcX: d.srcX)
|
|
104
|
+
|
|
105
|
+
fun stepSpinner(p: Spinner, dt: Float) -> Spinner =
|
|
106
|
+
Spinner(id: p.id, x: p.x, y: p.y, rotation: p.rotation + 0.24 * dt, dancers: mapLink(p.dancers, |d| stepDancer(d, dt)))
|
|
107
|
+
|
|
108
|
+
fun stepCounter(c: Counter) -> Counter =
|
|
109
|
+
Counter(id: c.id, x: c.x, y: c.y, n: c.n + 1)
|
|
110
|
+
|
|
80
111
|
fun step(s: State, dt: Float) -> State =
|
|
81
|
-
spinners := List[Spinner]()
|
|
82
|
-
for i := range s.spinners.length()
|
|
83
|
-
p := s.spinners.at(i)
|
|
84
|
-
spinners.push(Spinner(id: p.id, x: p.x, y: p.y, rotation: p.rotation + 0.24 * dt))
|
|
85
|
-
dancers := List[Dancer]()
|
|
86
|
-
for i := range s.dancers.length()
|
|
87
|
-
|
|
112
|
+
State(
|
|
88
|
-
dancers.push(Dancer(id: d.id, x: d.x, y: d.y, rotation: d.rotation + 1.2 * dt, srcX: d.srcX))
|
|
89
|
-
drifters := List[Drifter]()
|
|
90
|
-
for i := range s.drifters.length()
|
|
91
|
-
drifters.push(stepDrifter(s.drifters.at(i)))
|
|
92
|
-
counters := List[Counter]()
|
|
93
|
-
for i := range s.counters.length()
|
|
94
|
-
c := s.counters.at(i)
|
|
95
|
-
counters.push(Counter(id: c.id, x: c.x, y: c.y, n: c.n + 1))
|
|
96
|
-
|
|
113
|
+
sheet: s.sheet, white: s.white, spinners: mapLink(s.spinners, |p| stepSpinner(p, dt)),
|
|
114
|
+
drifters: mapLink(s.drifters, |d| stepDrifter(d)), counters: mapLink(s.counters, |c| stepCounter(c)))
|
|
97
115
|
|
|
98
116
|
# --- view: pure, the whole tree every frame ---
|
|
99
117
|
|
|
100
118
|
fun dancerView(sheet: Int, d: Dancer) -> SceneNode =
|
|
101
119
|
Group(
|
|
102
|
-
transform:
|
|
120
|
+
transform: Compose(first: Translate(dx: d.x + 8.0, dy: d.y + 8.0), then: Compose(first: Rotate(radians: d.rotation), then: Translate(dx: 0.0 - 8.0, dy: 0.0 - 8.0))),
|
|
103
|
-
children:
|
|
121
|
+
children: More(head: Sprite(
|
|
104
122
|
tex: sheet, x: 0.0, y: 0.0, w: 16.0, h: 16.0, srcX: d.srcX, srcY: 0.0, srcW: 16.0, srcH: 16.0,
|
|
105
|
-
tint: Color.White)))
|
|
123
|
+
tint: Color.White), tail: End))
|
|
106
|
-
|
|
124
|
+
|
|
107
|
-
fun spinnerView(
|
|
125
|
+
fun spinnerView(sheet: Int, p: Spinner) -> SceneNode =
|
|
108
|
-
per := Counts.Sprites / Counts.Groups
|
|
109
|
-
p := s.spinners.at(gi)
|
|
110
|
-
kids := List[SceneNode]()
|
|
111
|
-
for j := range per
|
|
112
|
-
kids.push(dancerView(s.sheet, s.dancers.at(gi * per + j)))
|
|
113
126
|
Group(
|
|
114
|
-
transform:
|
|
127
|
+
transform: Compose(first: Translate(dx: p.x + 75.0, dy: p.y + 85.0), then: Compose(first: Rotate(radians: p.rotation), then: Translate(dx: 0.0 - 75.0, dy: 0.0 - 85.0))),
|
|
128
|
+
children: toNodes(p.dancers, |d| dancerView(sheet, d), End))
|
|
129
|
+
|
|
130
|
+
fun drifterView(white: Int, d: Drifter) -> SceneNode =
|
|
131
|
+
Sprite(
|
|
132
|
+
tex: white, x: d.x, y: d.y, w: d.w, h: d.h, srcX: 0.0, srcY: 0.0, srcW: 0.0, srcH: 0.0,
|
|
115
|
-
|
|
133
|
+
tint: d.color)
|
|
134
|
+
|
|
135
|
+
fun counterView(c: Counter) -> SceneNode =
|
|
136
|
+
Text(text: "n {c.n}", x: c.x, y: c.y, size: 14.0, color: Color.White)
|
|
116
137
|
|
|
117
138
|
fun view(s: State) -> SceneNode =
|
|
118
|
-
kids := List[SceneNode]()
|
|
119
|
-
for gi := range Counts.Groups
|
|
120
|
-
kids.push(spinnerView(s, gi))
|
|
121
|
-
for i := range s.drifters.length()
|
|
122
|
-
d := s.drifters.at(i)
|
|
123
|
-
kids.push(Sprite(
|
|
124
|
-
tex: s.white, x: d.x, y: d.y, w: d.w, h: d.h, srcX: 0.0, srcY: 0.0, srcW: 0.0, srcH: 0.0,
|
|
125
|
-
tint: d.color))
|
|
126
|
-
|
|
139
|
+
texts := toNodes(s.counters, |c| counterView(c), End)
|
|
127
|
-
c := s.counters.at(i)
|
|
128
|
-
|
|
140
|
+
drifts := toNodes(s.drifters, |d| drifterView(s.white, d), texts)
|
|
129
|
-
Group(transform:
|
|
141
|
+
Group(transform: Scale(sx: 0.8, sy: 0.8), children: toNodes(s.spinners, |p| spinnerView(s.sheet, p), drifts))
|
|
130
142
|
|
|
131
143
|
fun whiteTexture() -> Int =
|
|
132
144
|
t := textureCreate(1, 1)
|
|
@@ -151,15 +163,16 @@ test "ten frames of step run"
|
|
|
151
163
|
s := newState(0, 0)
|
|
152
164
|
for i := range 10
|
|
153
165
|
s = step(s, 0.016)
|
|
154
|
-
assert s.counters
|
|
166
|
+
assert nthOr(s.counters, 3, Counter(id: 0, x: 0.0, y: 0.0, n: 0)).n == 13
|
|
155
|
-
assert {s.drifters
|
|
167
|
+
assert {nthOr(s.drifters, 0, newDrifter(0, Color.White, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)).x - 20.0}.abs() < 0.000001
|
|
168
|
+
first := nthOr(s.spinners, 0, newSpinner(0))
|
|
156
|
-
assert {
|
|
169
|
+
assert {nthOr(first.dancers, 0, newDancer(0, 0)).rotation - 0.192}.abs() < 0.000001
|
|
157
170
|
|
|
158
171
|
test "view has one node per actor"
|
|
159
172
|
s := newState(0, 0)
|
|
160
173
|
v := view(s)
|
|
161
174
|
match v
|
|
162
|
-
Group =>
|
|
175
|
+
Group(_, kids) =>
|
|
163
|
-
assert
|
|
176
|
+
assert countNodes(kids) == 50 + 500 + 200 + 50
|
|
164
177
|
_ =>
|
|
165
178
|
assert false
|
plum-std/game/Picture.plum
CHANGED
|
@@ -9,63 +9,51 @@ import std/game/Color
|
|
|
9
9
|
import std/game/Affine
|
|
10
10
|
import std/game/Game
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# A recursive picture: a SceneNode is plain data (shapes, sprites, text, groups
|
|
13
|
-
# with
|
|
13
|
+
# with a transform, filters) and `Picture` is a set of structurally recursive
|
|
14
|
+
# functions that draw it, measure it and hit-test it. Children are a recursive
|
|
15
|
+
# `Nodes` list, so every operation recurses over SceneNode and Nodes, passing
|
|
14
|
-
#
|
|
16
|
+
# the parent matrix and alpha down as arguments. Nothing here mutates the picture.
|
|
15
|
-
# Transforms in a group's list compose in order: [Translate, Scale] scales first,
|
|
16
|
-
# then
|
|
17
|
+
# `Compose(first, then)` applies `then` first, then `first` (`first` is the outermost).
|
|
18
|
+
|
|
17
|
-
|
|
19
|
+
enum Transform =
|
|
20
|
+
| Identity
|
|
18
|
-
|
|
21
|
+
| Translate(dx: Float, dy: Float)
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
| Rotate(radians: Float)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
| Scale(sx: Float, sy: Float)
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
| Compose(first: Transform, then: Transform)
|
|
25
|
-
|
|
26
|
-
struct RectShape(x: Float, y: Float, w: Float, h: Float, color: Color)
|
|
27
|
-
|
|
28
|
-
struct CircleShape(cx: Float, cy: Float, r: Float, color: Color)
|
|
29
|
-
|
|
30
|
-
struct LineShape(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, color: Color)
|
|
31
|
-
|
|
32
|
-
# A texture region `w` by `h` with its top-left at (x, y).
|
|
33
|
-
struct Sprite(tex: Int, x: Float, y: Float, w: Float, h: Float, srcX: Float, srcY: Float, srcW: Float, srcH: Float, tint: Color)
|
|
34
|
-
|
|
35
|
-
record Text(text: Str, x: Float, y: Float, size: Float, color: Color)
|
|
36
|
-
|
|
37
|
-
struct Tint(color: Color)
|
|
38
|
-
|
|
39
|
-
struct Alpha(alpha: Float)
|
|
40
25
|
|
|
41
26
|
# `mode` is a `Blend` constant from std/game/Game.
|
|
27
|
+
enum Effect =
|
|
28
|
+
| Tint(color: Color)
|
|
29
|
+
| Alpha(alpha: Float)
|
|
42
|
-
|
|
30
|
+
| BlendFx(mode: Int)
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
# A Sprite is a texture region `w` by `h` with its top-left at (x, y).
|
|
47
|
-
|
|
48
|
-
#
|
|
33
|
+
# A Filter draws its child into `target`, then composites it with the effect.
|
|
49
|
-
# texture the game creates ONCE at load with `textureCreate`; draw
|
|
34
|
+
# `target` is a texture the game creates ONCE at load with `textureCreate`; draw
|
|
50
|
-
# so a view does not leak a texture per frame. The size of
|
|
35
|
+
# creates nothing, so a view does not leak a texture per frame. The size of
|
|
51
|
-
# filter can show: child pixels beyond it are cut off.
|
|
36
|
+
# `target` bounds what the filter can show: child pixels beyond it are cut off.
|
|
37
|
+
# A negative target draws nothing.
|
|
38
|
+
enum SceneNode =
|
|
39
|
+
| Group(transform: Transform, children: Nodes)
|
|
52
|
-
|
|
40
|
+
| Filter(effect: Effect, child: SceneNode, target: Int)
|
|
53
|
-
|
|
41
|
+
| Sprite(tex: Int, x: Float, y: Float, w: Float, h: Float, srcX: Float, srcY: Float, srcW: Float, srcH: Float, tint: Color)
|
|
42
|
+
| Text(text: Str, x: Float, y: Float, size: Float, color: Color)
|
|
54
|
-
|
|
43
|
+
| RectShape(x: Float, y: Float, w: Float, h: Float, color: Color)
|
|
44
|
+
| CircleShape(cx: Float, cy: Float, r: Float, color: Color)
|
|
45
|
+
| LineShape(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, color: Color)
|
|
46
|
+
|
|
47
|
+
enum Nodes = End | More(head: SceneNode, tail: Nodes)
|
|
55
48
|
|
|
56
49
|
struct Rect(x: Float, y: Float, w: Float, h: Float)
|
|
57
50
|
|
|
58
|
-
fun
|
|
51
|
+
fun picMatrix(t: Transform) -> Affine = match t
|
|
59
|
-
|
|
52
|
+
Identity => Affine.Identity
|
|
60
|
-
|
|
53
|
+
Translate(dx, dy) => Affine(a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: dx, f: dy)
|
|
61
|
-
|
|
54
|
+
Rotate(rad) => Affine(a: Math.cos(rad), b: Math.sin(rad), c: 0.0 - Math.sin(rad), d: Math.cos(rad), e: 0.0, f: 0.0)
|
|
62
|
-
|
|
55
|
+
Scale(sx, sy) => Affine(a: sx, b: 0.0, c: 0.0, d: sy, e: 0.0, f: 0.0)
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
Compose(first, then) => picMatrix(first).mul(picMatrix(then))
|
|
65
|
-
out := m
|
|
66
|
-
for i := range ts.length()
|
|
67
|
-
out = out.mul(picMatrixOf(ts.at(i)))
|
|
68
|
-
out
|
|
69
57
|
|
|
70
58
|
# The bounding box, in the outer space, of the box (x, y, w, h) seen through `m`.
|
|
71
59
|
fun picBox(x: Float, y: Float, w: Float, h: Float, m: Affine) -> Rect =
|
|
@@ -88,138 +76,144 @@ fun picUnion(a: Rect?, b: Rect) -> Rect =
|
|
|
88
76
|
return Rect(x: x0, y: y0, w: x1 - x0, h: y1 - y0)
|
|
89
77
|
b
|
|
90
78
|
|
|
79
|
+
fun picBoxLine(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, m: Affine) -> Rect =
|
|
80
|
+
lx := x1.min(x2) - thickness / 2.0
|
|
81
|
+
ly := y1.min(y2) - thickness / 2.0
|
|
82
|
+
picBox(lx, ly, {x1 - x2}.abs() + thickness, {y1 - y2}.abs() + thickness, m)
|
|
83
|
+
|
|
91
84
|
# The bounds of `node` drawn under `m`, or null when it has no extent.
|
|
92
|
-
fun picBounds(node: SceneNode, m: Affine) -> Rect? =
|
|
85
|
+
fun picBounds(node: SceneNode, m: Affine) -> Rect? = match node
|
|
93
|
-
match node
|
|
94
|
-
|
|
86
|
+
RectShape(x, y, w, h, _) => picBox(x, y, w, h, m)
|
|
95
|
-
|
|
87
|
+
CircleShape(cx, cy, r, _) => picBox(cx - r, cy - r, r * 2.0, r * 2.0, m)
|
|
96
|
-
LineShape =>
|
|
97
|
-
lx := node.x1.min(node.x2) - node.thickness / 2.0
|
|
98
|
-
ly := node.y1.min(node.y2) - node.thickness / 2.0
|
|
99
|
-
picBox(lx, ly, {node.x1 - node.x2}.abs() + node.thickness, {node.y1 - node.y2}.abs() + node.thickness, m)
|
|
100
|
-
|
|
88
|
+
LineShape(x1, y1, x2, y2, th, _) => picBoxLine(x1, y1, x2, y2, th, m)
|
|
89
|
+
Sprite(_, sx, sy, sw, sh, _, _, _, _, _) => picBox(sx, sy, sw, sh, m)
|
|
101
|
-
|
|
90
|
+
Text(txt, tx, ty, size, _) => picBox(tx, ty, size * 0.6 * Float(txt.length()), size, m)
|
|
102
|
-
Group =>
|
|
103
|
-
gm := picCompose(m, node.transform)
|
|
104
|
-
acc: Rect? := null
|
|
105
|
-
for i := range node.children.length()
|
|
106
|
-
|
|
91
|
+
Group(tr, kids) => picBoundsKids(kids, m.mul(picMatrix(tr)), null)
|
|
107
|
-
if cr := cb
|
|
108
|
-
acc = picUnion(acc, cr)
|
|
109
|
-
acc
|
|
110
|
-
|
|
92
|
+
Filter(_, child, _) => picBounds(child, m)
|
|
93
|
+
|
|
94
|
+
fun picBoundsKids(kids: Nodes, m: Affine, acc: Rect?) -> Rect? = match kids
|
|
95
|
+
End => acc
|
|
96
|
+
More(head, tail) =>
|
|
97
|
+
if cr := picBounds(head, m)
|
|
98
|
+
picBoundsKids(tail, m, picUnion(acc, cr))
|
|
99
|
+
else
|
|
100
|
+
picBoundsKids(tail, m, acc)
|
|
111
101
|
|
|
112
102
|
fun picInBox(x: Float, y: Float, w: Float, h: Float, px: Float, py: Float) -> Bool =
|
|
113
103
|
px >= x && px <= x + w && py >= y && py <= y + h
|
|
114
104
|
|
|
115
|
-
fun picNearLine(
|
|
105
|
+
fun picNearLine(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, px: Float, py: Float) -> Bool =
|
|
116
|
-
dx :=
|
|
106
|
+
dx := x2 - x1
|
|
117
|
-
dy :=
|
|
107
|
+
dy := y2 - y1
|
|
118
108
|
len2 := dx * dx + dy * dy
|
|
119
|
-
t := if len2 == 0.0 then 0.0 else {{px -
|
|
109
|
+
t := if len2 == 0.0 then 0.0 else {{px - x1} * dx + {py - y1} * dy} / len2
|
|
120
110
|
tc := t.max(0.0).min(1.0)
|
|
121
|
-
qx :=
|
|
111
|
+
qx := x1 + dx * tc
|
|
122
|
-
qy :=
|
|
112
|
+
qy := y1 + dy * tc
|
|
123
|
-
Math.hypot(px - qx, py - qy) <=
|
|
113
|
+
Math.hypot(px - qx, py - qy) <= thickness.max(1.0) / 2.0
|
|
124
114
|
|
|
125
115
|
fun picLeaf(hit: Bool) -> List[Int]? =
|
|
126
116
|
if hit
|
|
127
117
|
return List[Int]()
|
|
128
118
|
null
|
|
129
119
|
|
|
120
|
+
# `i` followed by the path `sub`.
|
|
121
|
+
fun picPrepend(i: Int, sub: List[Int]) -> List[Int] =
|
|
122
|
+
path := List(i)
|
|
123
|
+
for j := range sub.length()
|
|
124
|
+
path.push(sub.at(j))
|
|
125
|
+
path
|
|
126
|
+
|
|
130
127
|
# The child indices from `node` down to the hit leaf, or null on a miss. A leaf
|
|
131
128
|
# that is hit is an empty path. (px, py) is in `node`'s own space.
|
|
132
|
-
fun picHit(node: SceneNode, px: Float, py: Float) -> List[Int]? =
|
|
129
|
+
fun picHit(node: SceneNode, px: Float, py: Float) -> List[Int]? = match node
|
|
133
|
-
match node
|
|
134
|
-
|
|
130
|
+
RectShape(x, y, w, h, _) => picLeaf(picInBox(x, y, w, h, px, py))
|
|
135
|
-
|
|
131
|
+
CircleShape(cx, cy, r, _) => picLeaf(Math.hypot(px - cx, py - cy) <= r)
|
|
136
|
-
|
|
132
|
+
LineShape(x1, y1, x2, y2, th, _) => picLeaf(picNearLine(x1, y1, x2, y2, th, px, py))
|
|
137
|
-
|
|
133
|
+
Sprite(_, sx, sy, sw, sh, _, _, _, _, _) => picLeaf(picInBox(sx, sy, sw, sh, px, py))
|
|
138
|
-
|
|
134
|
+
Text(txt, tx, ty, size, _) => picLeaf(picInBox(tx, ty, size * 0.6 * Float(txt.length()), size, px, py))
|
|
139
|
-
|
|
135
|
+
Group(tr, kids) =>
|
|
140
|
-
inv := picCompose(Affine.Identity, node.transform).invert()
|
|
141
|
-
|
|
136
|
+
if gi := picMatrix(tr).invert()
|
|
142
|
-
|
|
137
|
+
lp := gi.apply(Vec2(x: px, y: py))
|
|
143
|
-
i := node.children.length() - 1
|
|
144
|
-
while i >= 0
|
|
145
|
-
|
|
138
|
+
picHitKids(kids, 0, lp.x, lp.y)
|
|
146
|
-
if sp := sub
|
|
147
|
-
path := List(i)
|
|
148
|
-
for j := range sp.length()
|
|
149
|
-
path.push(sp.at(j))
|
|
150
|
-
|
|
139
|
+
else
|
|
151
|
-
i = i - 1
|
|
152
140
|
null
|
|
153
|
-
|
|
141
|
+
Filter(_, child, _) =>
|
|
154
|
-
|
|
142
|
+
if fp := picHit(child, px, py)
|
|
155
|
-
if fp := fsub
|
|
156
|
-
fpath := List(0)
|
|
157
|
-
for k := range fp.length()
|
|
158
|
-
|
|
143
|
+
picPrepend(0, fp)
|
|
159
|
-
|
|
144
|
+
else
|
|
160
145
|
null
|
|
161
146
|
|
|
147
|
+
# Tries the later children first (they are drawn on top); `i` is the index of
|
|
148
|
+
# `kids`' head. The first hit found from the back wins.
|
|
149
|
+
fun picHitKids(kids: Nodes, i: Int, px: Float, py: Float) -> List[Int]? = match kids
|
|
150
|
+
End => null
|
|
151
|
+
More(head, tail) =>
|
|
152
|
+
if later := picHitKids(tail, i + 1, px, py)
|
|
153
|
+
later
|
|
154
|
+
else
|
|
155
|
+
if sub := picHit(head, px, py)
|
|
156
|
+
picPrepend(i, sub)
|
|
157
|
+
else
|
|
158
|
+
null
|
|
159
|
+
|
|
162
|
-
fun picDraw(node: SceneNode, m: Affine, alpha: Float, target: Int) =
|
|
160
|
+
fun picDraw(node: SceneNode, m: Affine, alpha: Float, target: Int) = match node
|
|
163
|
-
match node
|
|
164
|
-
|
|
161
|
+
RectShape(x, y, w, h, color) =>
|
|
165
|
-
|
|
162
|
+
sx := w * m.scaleX()
|
|
166
|
-
|
|
163
|
+
sy := h * m.scaleY()
|
|
167
|
-
|
|
164
|
+
c := m.apply(Vec2(x: x + w / 2.0, y: y + h / 2.0))
|
|
168
|
-
|
|
165
|
+
drawRect(c.x - sx / 2.0, c.y - sy / 2.0, sx, sy, color.withAlpha(color.a * alpha))
|
|
169
|
-
|
|
166
|
+
CircleShape(cx, cy, r, ccolor) =>
|
|
170
|
-
|
|
167
|
+
cc := m.apply(Vec2(x: cx, y: cy))
|
|
171
|
-
|
|
168
|
+
drawCircle(cc.x, cc.y, r * m.scaleX(), ccolor.withAlpha(ccolor.a * alpha))
|
|
172
|
-
|
|
169
|
+
LineShape(x1, y1, x2, y2, th, lcolor) =>
|
|
173
|
-
|
|
170
|
+
p := m.apply(Vec2(x: x1, y: y1))
|
|
174
|
-
|
|
171
|
+
q := m.apply(Vec2(x: x2, y: y2))
|
|
175
|
-
|
|
172
|
+
drawLine(p.x, p.y, q.x, q.y, th * m.scaleX(), lcolor.withAlpha(lcolor.a * alpha))
|
|
176
|
-
|
|
173
|
+
Sprite(tex, sx0, sy0, sw0, sh0, srcX, srcY, srcW, srcH, tint) =>
|
|
177
|
-
|
|
174
|
+
sw := sw0 * m.scaleX()
|
|
178
|
-
|
|
175
|
+
sh := sh0 * m.scaleY()
|
|
179
|
-
|
|
176
|
+
sc := m.apply(Vec2(x: sx0 + sw0 / 2.0, y: sy0 + sh0 / 2.0))
|
|
180
|
-
|
|
177
|
+
drawTextureEx(tex, srcX, srcY, srcW, srcH, sc.x - sw / 2.0, sc.y - sh / 2.0, sw, sh, m.rotation(), false, tint.withAlpha(tint.a * alpha))
|
|
181
|
-
|
|
178
|
+
Text(txt, tx, ty, size, tcolor) =>
|
|
182
|
-
|
|
179
|
+
tp := m.apply(Vec2(x: tx, y: ty))
|
|
183
|
-
|
|
180
|
+
drawText(txt, tp.x, tp.y, size * m.scaleY(), tcolor.withAlpha(tcolor.a * alpha))
|
|
184
|
-
Group =>
|
|
185
|
-
gm := picCompose(m, node.transform)
|
|
186
|
-
for i := range node.children.length()
|
|
187
|
-
|
|
181
|
+
Group(tr, kids) => picDrawKids(kids, m.mul(picMatrix(tr)), alpha, target)
|
|
188
|
-
|
|
182
|
+
Filter(effect, child, tex) => picFilter(effect, child, tex, m, alpha, target)
|
|
189
|
-
|
|
183
|
+
|
|
190
|
-
fun
|
|
184
|
+
fun picDrawKids(kids: Nodes, m: Affine, alpha: Float, target: Int) = match kids
|
|
191
|
-
|
|
185
|
+
End => 0
|
|
192
|
-
|
|
186
|
+
More(head, tail) =>
|
|
187
|
+
picDraw(head, m, alpha, target)
|
|
188
|
+
picDrawKids(tail, m, alpha, target)
|
|
189
|
+
|
|
190
|
+
fun picFilter(effect: Effect, child: SceneNode, tex: Int, m: Affine, alpha: Float, target: Int) = match effect
|
|
193
|
-
|
|
191
|
+
BlendFx(mode) =>
|
|
194
|
-
|
|
192
|
+
blendMode(mode)
|
|
195
|
-
|
|
193
|
+
picDraw(child, m, alpha, target)
|
|
196
|
-
|
|
194
|
+
blendMode(Blend.Normal)
|
|
197
|
-
|
|
195
|
+
_ =>
|
|
198
|
-
|
|
196
|
+
if b := picBounds(child, m)
|
|
199
|
-
if b := fb
|
|
200
|
-
tex := f.target
|
|
201
|
-
|
|
197
|
+
if tex >= 0
|
|
202
|
-
return
|
|
203
198
|
w := {Int(b.w) + 1}.min(textureWidth(tex))
|
|
204
199
|
h := {Int(b.h) + 1}.min(textureHeight(tex))
|
|
205
200
|
renderTarget(tex)
|
|
206
201
|
clear(Color.Transparent)
|
|
207
202
|
shift := Affine(a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: 0.0 - b.x, f: 0.0 - b.y)
|
|
208
|
-
picDraw(
|
|
203
|
+
picDraw(child, shift.mul(m), 1.0, tex)
|
|
209
204
|
renderTarget(target)
|
|
210
|
-
picComposite(
|
|
205
|
+
picComposite(effect, tex, b, alpha, w, h)
|
|
211
206
|
|
|
212
|
-
fun picTint(e: Effect, alpha: Float) -> Color =
|
|
207
|
+
fun picTint(e: Effect, alpha: Float) -> Color = match e
|
|
213
|
-
match e
|
|
214
|
-
|
|
208
|
+
Tint(color) => color.withAlpha(color.a * alpha)
|
|
215
|
-
|
|
209
|
+
Alpha(a) => Color.White.withAlpha(a * alpha)
|
|
216
|
-
|
|
210
|
+
_ => Color.White.withAlpha(alpha)
|
|
217
211
|
|
|
218
212
|
fun picComposite(e: Effect, tex: Int, b: Rect, alpha: Float, w: Int, h: Int) =
|
|
219
213
|
drawTextureEx(tex, 0.0, 0.0, Float(w), Float(h), b.x, b.y, Float(w), Float(h), 0.0, false, picTint(e, alpha))
|
|
220
214
|
|
|
221
215
|
primitive Picture =
|
|
222
|
-
# Draws the picture to the current target with
|
|
216
|
+
# Draws the picture to the current target, recursing with the parent matrix and alpha.
|
|
223
217
|
fun draw(node: SceneNode) =
|
|
224
218
|
picDraw(node, Affine.Identity, 1.0, 0 - 1)
|
|
225
219
|
|
|
@@ -238,8 +232,11 @@ primitive Picture =
|
|
|
238
232
|
fun picNear(a: Float, b: Float) -> Bool =
|
|
239
233
|
{a - b}.abs() < 1e-6
|
|
240
234
|
|
|
235
|
+
fun picRect(x: Float, y: Float, w: Float, h: Float, c: Color) -> SceneNode =
|
|
236
|
+
RectShape(x: x, y: y, w: w, h: h, color: c)
|
|
237
|
+
|
|
241
238
|
test "bounds_translate_scale"
|
|
242
|
-
g := Group(transform:
|
|
239
|
+
g := Group(transform: Compose(first: Translate(dx: 5.0, dy: 5.0), then: Scale(sx: 2.0, sy: 2.0)), children: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: End))
|
|
243
240
|
r := Picture.bounds(g)
|
|
244
241
|
assert picNear(r.x, 5.0)
|
|
245
242
|
assert picNear(r.y, 5.0)
|
|
@@ -247,7 +244,7 @@ test "bounds_translate_scale"
|
|
|
247
244
|
assert picNear(r.h, 20.0)
|
|
248
245
|
|
|
249
246
|
test "bounds_rotate_quarter"
|
|
250
|
-
g := Group(transform:
|
|
247
|
+
g := Group(transform: Rotate(radians: 1.5707963267948966), children: More(head: picRect(0.0, 0.0, 10.0, 4.0, Color.Red), tail: End))
|
|
251
248
|
r := Picture.bounds(g)
|
|
252
249
|
assert picNear(r.x, 0.0 - 4.0)
|
|
253
250
|
assert picNear(r.y, 0.0)
|
|
@@ -255,7 +252,8 @@ test "bounds_rotate_quarter"
|
|
|
255
252
|
assert picNear(r.h, 10.0)
|
|
256
253
|
|
|
257
254
|
test "hit_path_nested"
|
|
258
|
-
|
|
255
|
+
inner := Group(transform: Translate(dx: 10.0, dy: 0.0), children: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Blue), tail: End))
|
|
256
|
+
g := Group(transform: Identity, children: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: More(head: inner, tail: End)))
|
|
259
257
|
p := Picture.hitPath(g, 15.0, 5.0)
|
|
260
258
|
assert p.length() == 2
|
|
261
259
|
assert p.at(0) == 1
|
|
@@ -264,12 +262,18 @@ test "hit_path_nested"
|
|
|
264
262
|
assert q.length() == 1
|
|
265
263
|
assert q.at(0) == 0
|
|
266
264
|
|
|
265
|
+
test "hit_path_last_child_wins"
|
|
266
|
+
g := Group(transform: Identity, children: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Blue), tail: End)))
|
|
267
|
+
p := Picture.hitPath(g, 5.0, 5.0)
|
|
268
|
+
assert p.length() == 1
|
|
269
|
+
assert p.at(0) == 1
|
|
270
|
+
|
|
267
271
|
test "hit_miss_empty"
|
|
268
|
-
g := Group(transform:
|
|
272
|
+
g := Group(transform: Identity, children: More(head: picRect(0.0, 0.0, 10.0, 10.0, Color.Red), tail: End))
|
|
269
273
|
assert Picture.hitPath(g, 50.0, 50.0).length() == 0
|
|
270
274
|
|
|
271
275
|
test "filter_bounds_is_child_bounds"
|
|
272
|
-
g := Filter(effect: Alpha(alpha: 0.5), child:
|
|
276
|
+
g := Filter(effect: Alpha(alpha: 0.5), child: picRect(1.0, 2.0, 10.0, 4.0, Color.Red), target: 0 - 1)
|
|
273
277
|
r := Picture.bounds(g)
|
|
274
278
|
assert picNear(r.x, 1.0)
|
|
275
279
|
assert picNear(r.y, 2.0)
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -4745,8 +4745,16 @@ fn compileFnBodyImpl(
|
|
|
4745
4745
|
idx += leafCount(ty);
|
|
4746
4746
|
}
|
|
4747
4747
|
for (name, ty) in &collector.named {
|
|
4748
|
-
locals.insert(name.clone(), idx);
|
|
4749
4748
|
let vts = withGcTypes(|g| flatValtypes(ty, g));
|
|
4749
|
+
// `s = T(..s, ...)` on a param reassigns the param's own slot (F-T6-3): a
|
|
4750
|
+
// fresh local would shadow the param for the whole body, so every read of
|
|
4751
|
+
// `s` (the right-hand side included) would see the unset local.
|
|
4752
|
+
if let Some(pty) = decl_types.get(name).filter(|_| locals.contains_key(name)) {
|
|
4753
|
+
if withGcTypes(|g| flatValtypes(pty, g)) == vts {
|
|
4754
|
+
continue;
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
locals.insert(name.clone(), idx);
|
|
4750
4758
|
idx += vts.len() as u32;
|
|
4751
4759
|
groups.extend(vts);
|
|
4752
4760
|
}
|
plum-wasm-codegen/tests/own_inplace_e2e_test.rs
CHANGED
|
@@ -204,3 +204,53 @@ fun main() -> Int =
|
|
|
204
204
|
");
|
|
205
205
|
assert_eq!(runInt("match_rename", &src), 400 + 56);
|
|
206
206
|
}
|
|
207
|
+
|
|
208
|
+
/// F-T6-3: `s = Cell(..s, ...)` on a param must reassign the param's own slot, not
|
|
209
|
+
/// a fresh shadow local that every read of `s` (the spread source included) sees unset.
|
|
210
|
+
#[test]
|
|
211
|
+
fn own_spread_reassign_same_var() {
|
|
212
|
+
let body = "own record Cell(x: Int, y: Int)
|
|
213
|
+
|
|
214
|
+
fun bump(own s: Cell) -> Cell =
|
|
215
|
+
s = Cell(..s, x: s.x + 1)
|
|
216
|
+
s
|
|
217
|
+
|
|
218
|
+
fun inc(n: Int) -> Int =
|
|
219
|
+
n = n + 1
|
|
220
|
+
n
|
|
221
|
+
|
|
222
|
+
fun main() -> Int =
|
|
223
|
+
t := bump(Cell(x: 1, y: 7))
|
|
224
|
+
t.x * 100 + t.y + inc(40) * 1000
|
|
225
|
+
";
|
|
226
|
+
let own = prog(body);
|
|
227
|
+
assert_eq!(runInt("reassign_param", &own), 41000 + 207);
|
|
228
|
+
let plain = prog(&body.replace("own record", "record").replace("own s", "s"));
|
|
229
|
+
assert_eq!(runInt("reassign_param_plain", &plain), 41000 + 207);
|
|
230
|
+
assert_eq!(structNewCount("reassign_param_p", &plain), structNewCount("reassign_param_o", &own) + 1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn own_spread_reassign_local() {
|
|
235
|
+
let src = prog("own record Cell(x: Int, y: Int)
|
|
236
|
+
|
|
237
|
+
fun main() -> Int =
|
|
238
|
+
s := Cell(x: 1, y: 7)
|
|
239
|
+
s = Cell(..s, x: s.x + 1)
|
|
240
|
+
s = Cell(..s, y: s.y + s.x)
|
|
241
|
+
s.x * 100 + s.y
|
|
242
|
+
");
|
|
243
|
+
assert_eq!(runInt("reassign_local", &src), 200 + 9);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[test]
|
|
247
|
+
fn own_self_spread_return() {
|
|
248
|
+
let src = prog("own record Cell(x: Int, y: Int) =
|
|
249
|
+
fun step(own self) -> Cell = Cell(..self, x: self.x + 1)
|
|
250
|
+
|
|
251
|
+
fun main() -> Int =
|
|
252
|
+
t := Cell(x: 1, y: 7).step().step()
|
|
253
|
+
t.x * 100 + t.y
|
|
254
|
+
");
|
|
255
|
+
assert_eq!(runInt("self_spread", &src), 300 + 7);
|
|
256
|
+
}
|