diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index f3862bda01..bec509af20 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -68,7 +68,7 @@ the reels. Describing T27F as #1677 is the error this section exists to stop (ow | Renderer in the Trinity UI | gHashTag/trinity | **missing**; no T27F widget is live | | Application: topic map (#1677) | `specs/t27f/topic_engine.t27`, `topic_reel.t27` | spec; procedure below | | Application: reels and deck | `specs/t27f/reel.t27`, `deck.t27`, `reel_*.t27` | spec; two reels | -| Application: content factory (epic t27#10560) | `specs/t27f/reel_factory.t27` (t27#10561) | spec with tests: an item's code and title, read keyword by keyword, give 11 features. Those pick 1 of 12 scene archetypes (best pair first, cap 4, no repeat within 2 items, then the least-used generic scene) plus an accent, a motion, a layout and 8 output formats. No renderer draws a plan yet (R3), and `lesson_reel` still draws token_floor (R4). Lessons: read the plan's spread over the whole course before trusting it. A greedy walk in course order gave 7 lessons one scene. Code counts alone handed lesson 8 the tests scene over "A test block". A printing `main` makes t27b reject the spec | +| Application: content factory (epic t27#10560) | `specs/t27f/reel_factory.t27` (t27#10561) | spec with tests: an item's code and title, read keyword by keyword, give 11 features. Those pick 1 of 12 scene archetypes (best pair first, cap 4, no repeat within 2 items, then the least-used generic scene) plus an accent, a motion, a layout and 8 output formats. R3 (t27#10571) draws it: `scene_raster`, `scene_lex`, `scene_bg` plus one `scene_.t27` per archetype (code, graph, tests, trit, loop, branch, data, links, guard, title, task, map), each `_px(t, acc, n, x, y)` over the lesson's own content on a 360 x 640 canvas, dispatched by `scene_still.pixel`; `still_gate.t27` renders the 27 course-0 stills (108 x 192 luma), scores all 351 pairs with a block SSIM and refuses above 850 permille, measured worst pair lessons 8 and 20 at 607. `lesson_reel` still draws token_floor (R4). SSIM on luma sees layout and contrast, not meaning. The floor modulo is `fmod` (t27b rejects `@mod`). Lessons: read the plan's spread over the whole course before trusting it. A greedy walk in course order gave 7 lessons one scene. Code counts alone handed lesson 8 the tests scene over "A test block". A printing `main` makes t27b reject the spec | | Related | trinity#1647 dashboards, #1652 MVP research, #1653 three skills, #1655 evolution runtime | open | When asked "how is T27F going", report against the core rows first. Progress on the applications @@ -94,6 +94,22 @@ is not progress on the core. Anything the rules do not reach (SUGGESTED explanations, call-result flow, anything larger) is still a manual reading, and no host runs either pass yet. Name that gap rather than hide it. +## Visual in every output (Claude Code and TRI CLAW) + +**The rule is `specs/t27mcp/visual_output.t27`** (its `RULE_CLAUDE_CODE` and `RULE_TRI_CLAW` lines +are what each host reads). **Its particle-still layer is `specs/claw/visual_output.t27`.** This +section is the order of steps; which answer carries which visual and how it reaches each host are +the specs', not repeated here. + +1. Decide the answer's kind (`K_*` in the t27mcp spec). A one-sentence reply or a question back + goes without a visual. +2. Spec, flow or numbers: draw from t27f tool rows (`t27f_outline`, `t27f_graph`, `t27f_view`), + labelled, with the legend and the source file, as `answer_verdict` checks. +3. A report of a piece of work: a particle still (`takes_still`). Render the reel on the Railway lab + (never the Mac), take the frame at `STILL_MS` with `ffmpeg -ss 9 -frames:v 1`, and bring it into + the session folder (`out-reels/`, gitignored, never `git add`). +4. Anything not made: say which and why in the answer. Never show an older file as this turn's. + ## Application: topic map (gHashTag/trinity#1677) Use this part when the user gives a topic or a client brief, not a spec. diff --git a/.trinity/seals/claw_ClawReelStill.json b/.trinity/seals/claw_ClawReelStill.json new file mode 100644 index 0000000000..b4ee1a2cf2 --- /dev/null +++ b/.trinity/seals/claw_ClawReelStill.json @@ -0,0 +1,49 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+5e4d3b998", + "closure": [ + { + "spec": "specs/claw/visual_output.t27", + "spec_hash": "sha256:17137afe136e39910f38d7b031c659a672e009bfeef1bf4f9b7af7e2cb6cc628" + }, + { + "spec": "specs/t27f/frame_shard.t27", + "spec_hash": "sha256:1f04638a243b7a7f4d7a4480faf85f7878ca2e3365a2ec0f001a991fbd353da6" + }, + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + }, + { + "spec": "specs/t27f/token_floor_scenes.t27", + "spec_hash": "sha256:948cff4bd34341b40b43c8a170e2a37e3bca59f8d6bc8e35d7e5ee43fe89a4ea" + }, + { + "spec": "specs/t27mcp/service.t27", + "spec_hash": "sha256:3d7957837be8ac3de14b18ce1c0ecdde805f3f71d4d801b62d476b2e82f4a1ae" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:d32a87a4218806616b04b727d7e3f1bc76d6d33440f304f7beb42b566bb50437", + "gen_hash_rust": "sha256:5a1adebb39cdeb948aae1da51a64f43b93b1cd845f715389c30cf6785b701c30", + "gen_hash_verilog": "sha256:62df937cda4af6426ae5e1170bafa4cf124f5d952dea6b87ccf19e451968dc9f", + "gen_hash_zig": "sha256:b4e20621dcef2e6517948018e7f6aeb788da068df27beb9b6c2a485af15b492e", + "module": "ClawReelStill", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T17:06:53Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:4fc5fc3c835193faae7b312194e4f31bc6baa8b7da3f08b12b809cb4e5e293e5", + "spec_path": "specs/claw/reel_still.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 15, + "total": 15, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/claw_ClawVisualOutput.json b/.trinity/seals/claw_ClawVisualOutput.json new file mode 100644 index 0000000000..3556c08782 --- /dev/null +++ b/.trinity/seals/claw_ClawVisualOutput.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_marketing.t27", + "spec_hash": "sha256:ab07f6476d900cc0e089ba1d851f0b610a77b778f00fd906d1b98a29281941c7" + }, + { + "spec": "specs/t27mcp/visual_output.t27", + "spec_hash": "sha256:06c1c5e73142e2f79fa5d7be037eace63c1f20597a6e33274ebf8e1b428b7584" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:195d1b6aa754842316d3c8becc34ce6a0a90e5f7d4ddaebd490823103ed06c33", + "gen_hash_rust": "sha256:93ecd121c7efadf95e4ca54e1e750fb5c7a8159284cebe1cbb61d7065d1b7875", + "gen_hash_verilog": "sha256:794e86d5753efad530cd7732b2638ad3c3a5c4e6d8c9a851bd686663aa097282", + "gen_hash_zig": "sha256:d6204f6e99bc281a0163177b92ee4abf096ea32cb23cb3b817913d16c8d072b1", + "module": "ClawVisualOutput", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T16:42:30Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:17137afe136e39910f38d7b031c659a672e009bfeef1bf4f9b7af7e2cb6cc628", + "spec_path": "specs/claw/visual_output.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 6, + "total": 6, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_bg.json b/.trinity/seals/t27f_scene_bg.json new file mode 100644 index 0000000000..5454fa6399 --- /dev/null +++ b/.trinity/seals/t27f_scene_bg.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:af5c89e6bd6c81265d9d86c4f93eeb738ea91bfae40f294928bd586d5e98e7d0", + "gen_hash_rust": "sha256:b919e5036e999c0a6f329d928c954691083b2b139fe82ff160dabee993a6d1e8", + "gen_hash_verilog": "sha256:4470ad8b1d224134c143e03ce299f04c18dcfe61a47dfa036f9b7410f3568a6d", + "gen_hash_zig": "sha256:6b42744f7cacf6a9dd8efefee85de697a6a40137a0b4964285589c10e42f14c0", + "module": "scene_bg", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:09Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf", + "spec_path": "specs/t27f/scene_bg.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 5, + "total": 5, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_branch.json b/.trinity/seals/t27f_scene_branch.json new file mode 100644 index 0000000000..7b7a79a261 --- /dev/null +++ b/.trinity/seals/t27f_scene_branch.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:07f5247c4f8578519a9d66081d2622a8350a7fbd467c4bc7c81d8e21c3c0100e", + "gen_hash_rust": "sha256:812a8d57ed2554ba33bb71e8f0194f5c65c7db268713b10823c61cbc6eae7261", + "gen_hash_verilog": "sha256:f4f5ef5ca03bb0f14c3e6279f8134fdd5c83f4c5c9632fc2c55382415eee8855", + "gen_hash_zig": "sha256:928740ee46a8f715527f4e4f4476abc9f95d323ab016878d8f7054f0961850a3", + "module": "scene_branch", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:18Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:c4b584b8b5a4f5cc849dc9c1a4323609702adcdb710cb97b6441059210eccc15", + "spec_path": "specs/t27f/scene_branch.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_code.json b/.trinity/seals/t27f_scene_code.json new file mode 100644 index 0000000000..b4763bd79d --- /dev/null +++ b/.trinity/seals/t27f_scene_code.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + }, + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:24b083661da23623773e140dd892388a51a52bbea5c932ca1e511b14390f416a", + "gen_hash_rust": "sha256:0d695db4f92342098d31e91855036ce21ad328328c6092afec89db64ce52fb5a", + "gen_hash_verilog": "sha256:524b3fafa43e57c7e6473c002498409f9bd60707acb3de9ef44488ae5072227f", + "gen_hash_zig": "sha256:7aac819ff50f5239e48d46e351e9319c5acd054d21b2abfc0a2d44dff53ab722", + "module": "scene_code", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:11Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:1c5820fa846b8a40b309d6c4270a0f81823bd2876fa9f5532a2c41700a6f276f", + "spec_path": "specs/t27f/scene_code.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_data.json b/.trinity/seals/t27f_scene_data.json new file mode 100644 index 0000000000..4105757277 --- /dev/null +++ b/.trinity/seals/t27f_scene_data.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:a325d308d3fa644afc3a8c32572ee2c57454dd158b2f71334f15b019bccaa32b", + "gen_hash_rust": "sha256:f42475cad05b3992ff14f56c244a62e93569d442c823601e545b321b4592ce8b", + "gen_hash_verilog": "sha256:33650fcf0ac1cfc0c774466282467f6e6eb4c1ed0a39cf3cf5d671faac9fbf27", + "gen_hash_zig": "sha256:0071b9914207b81c1803d45cf4bce149535b8229ca2f7bf2f4fdf07a9323e8a1", + "module": "scene_data", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:19Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:7187e6822bce5126c69b0bfaa9b3fb7a48aa8dc29fe60f5367d4220144966534", + "spec_path": "specs/t27f/scene_data.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_graph.json b/.trinity/seals/t27f_scene_graph.json new file mode 100644 index 0000000000..f5ff22c638 --- /dev/null +++ b/.trinity/seals/t27f_scene_graph.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:970e740bc66feb58d25d6a6c00dbbb68f04591d579ff9653927ebe360fc5ddc5", + "gen_hash_rust": "sha256:df9e11ebaa9016d09dd8de06397d531fe0825509137183537e3afe922115ee4c", + "gen_hash_verilog": "sha256:5cae54c058c0dfc82c481516fee95ff46f84c92adef1c254ea3fbbeae244a948", + "gen_hash_zig": "sha256:19b0aec74782b50d6ea48603e4ce51e3317f44a5ae4cbcde63cb48ffd9965767", + "module": "scene_graph", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:12Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:ae37d6259842a3b6854548adff272260a35a8c7652c509f952ed1908b94d5706", + "spec_path": "specs/t27f/scene_graph.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_guard.json b/.trinity/seals/t27f_scene_guard.json new file mode 100644 index 0000000000..157e665a7b --- /dev/null +++ b/.trinity/seals/t27f_scene_guard.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:0b3c796b603a3031e2ff795e0f8286a608ab8f8247467a83dc6c680fc1aae5e4", + "gen_hash_rust": "sha256:d365039ff9ee9cf54ad434a7a2887b060cfcc34e9f45bee8efb49a9e95c3451e", + "gen_hash_verilog": "sha256:a6993136fc4f05166ed8686a2a700bc9744aaecee82a402ee024ea160102db0a", + "gen_hash_zig": "sha256:6c33b8b7421ed74d49a297149d9a890719cd1df230415dc42ee8a2bf66ed1eca", + "module": "scene_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:22Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:e11d00191157bb94a7172203748d0e9ff6accff74fac2b9c4fa9703128f1db81", + "spec_path": "specs/t27f/scene_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 5, + "total": 5, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_lex.json b/.trinity/seals/t27f_scene_lex.json new file mode 100644 index 0000000000..116ab0a03b --- /dev/null +++ b/.trinity/seals/t27f_scene_lex.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_factory.t27", + "spec_hash": "sha256:96099d764ba86fa3ef2c29d81c3838ce255bfd3b3abbd37b27cb86e4bca91012" + }, + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:6f713cff16f2fc94084f3f1788b3494ba884aff86c39b980b29b2e2744b53ffe", + "gen_hash_rust": "sha256:be4e95d181dcdc64a882675c549a8db971e0ed3c5ea63cf215fbc524e37380eb", + "gen_hash_verilog": "sha256:46f7ab4aa31ed2514ab47f637705e8a407a6693481156da2f21024a106035a75", + "gen_hash_zig": "sha256:a897632d32bb625e3ff691ed434c74b499d1bd1347b8d4b02641f5aa2481e732", + "module": "scene_lex", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:08Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9", + "spec_path": "specs/t27f/scene_lex.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 3, + "total": 3, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_links.json b/.trinity/seals/t27f_scene_links.json new file mode 100644 index 0000000000..2546749831 --- /dev/null +++ b/.trinity/seals/t27f_scene_links.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:a5b1ca5ee2ce803353dca6918927d7739de73cf9539692e6122840751be48b76", + "gen_hash_rust": "sha256:df6624e54fab79d94c23aaada294f4e624c278606d93d48a00a8e455bf0650c2", + "gen_hash_verilog": "sha256:16a4aaa084d781eeae0582e01ed8a5e6dc25245699dad0018f4425ce461a9370", + "gen_hash_zig": "sha256:5a112f82f3a2699019fd6c0144c911218575e3e044d0ea55267da4b2e60c26fa", + "module": "scene_links", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:21Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:d234cb4a2d21e841a8474aa66a70336f181ff3013ecbe6f7118d09f38cedc3a4", + "spec_path": "specs/t27f/scene_links.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_loop.json b/.trinity/seals/t27f_scene_loop.json new file mode 100644 index 0000000000..3878b20157 --- /dev/null +++ b/.trinity/seals/t27f_scene_loop.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:99b2f025b6a34b19c382f0d67f905f8ba0fb08b950cd97c77661b2fe36c9aa0d", + "gen_hash_rust": "sha256:29d7bd9c5374e271d831a1c4c3688b6383ba46496a7723bb87260a798015155b", + "gen_hash_verilog": "sha256:7987bd702aab49c102c8df26d79d19b13d41d2116c95e0ad9e5a2c4d5248beab", + "gen_hash_zig": "sha256:a76da017349e3953c89dc7c527ef53d78e76aa0298e72170f8c31ec63874e377", + "module": "scene_loop", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:16Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:92aab2767f95cbed855e32554255b74ba744f94bc0b7a1df2d8b2593beef89c8", + "spec_path": "specs/t27f/scene_loop.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_map.json b/.trinity/seals/t27f_scene_map.json new file mode 100644 index 0000000000..412266511b --- /dev/null +++ b/.trinity/seals/t27f_scene_map.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:d61115a7ade0e5d5e58ebf650a8bbe949200196ccbc9587cfc66b5ccdf6dd843", + "gen_hash_rust": "sha256:be73f1b88bd658f2f5e86dc093b349c1ccf895451cf38901c478c4a456bf5562", + "gen_hash_verilog": "sha256:171c6c2a9c7872a78d3c1497b6d6be2cc02c18c9a22a07ebcfa37198d2ae9b56", + "gen_hash_zig": "sha256:605ff9cea1a7df198f36fdb3172dafa70a3621a69bf4b9d40b2d6c1109cc635e", + "module": "scene_map", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:26Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:1c1239443601b49386b1ed81b4372fc66161dc8280dcc31cdc75e27667aec698", + "spec_path": "specs/t27f/scene_map.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_raster.json b/.trinity/seals/t27f_scene_raster.json new file mode 100644 index 0000000000..6c9e809bd9 --- /dev/null +++ b/.trinity/seals/t27f_scene_raster.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:368f171bbb4fd19a793a9d79b4f9dd5fd6a7802e8d2fd0691169adc8ef82ede5", + "gen_hash_rust": "sha256:4a1ec9110c4e1dab7e65810391d2bc7ab95eece804707b26e54f6ae6e0ef83cf", + "gen_hash_verilog": "sha256:2dd451aecebe4cb25dce4593b55762795e1512eb3824ca7b6dd7dfe10d491330", + "gen_hash_zig": "sha256:69a84b97ad599d29b4bb2a69e814816eec24657d00e9e09f3d8b9ccb766900f1", + "module": "scene_raster", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:06Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6", + "spec_path": "specs/t27f/scene_raster.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 6, + "total": 6, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_still.json b/.trinity/seals/t27f_scene_still.json new file mode 100644 index 0000000000..eb3217b7d2 --- /dev/null +++ b/.trinity/seals/t27f_scene_still.json @@ -0,0 +1,81 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_branch.t27", + "spec_hash": "sha256:c4b584b8b5a4f5cc849dc9c1a4323609702adcdb710cb97b6441059210eccc15" + }, + { + "spec": "specs/t27f/scene_code.t27", + "spec_hash": "sha256:1c5820fa846b8a40b309d6c4270a0f81823bd2876fa9f5532a2c41700a6f276f" + }, + { + "spec": "specs/t27f/scene_data.t27", + "spec_hash": "sha256:7187e6822bce5126c69b0bfaa9b3fb7a48aa8dc29fe60f5367d4220144966534" + }, + { + "spec": "specs/t27f/scene_graph.t27", + "spec_hash": "sha256:ae37d6259842a3b6854548adff272260a35a8c7652c509f952ed1908b94d5706" + }, + { + "spec": "specs/t27f/scene_guard.t27", + "spec_hash": "sha256:e11d00191157bb94a7172203748d0e9ff6accff74fac2b9c4fa9703128f1db81" + }, + { + "spec": "specs/t27f/scene_links.t27", + "spec_hash": "sha256:d234cb4a2d21e841a8474aa66a70336f181ff3013ecbe6f7118d09f38cedc3a4" + }, + { + "spec": "specs/t27f/scene_loop.t27", + "spec_hash": "sha256:92aab2767f95cbed855e32554255b74ba744f94bc0b7a1df2d8b2593beef89c8" + }, + { + "spec": "specs/t27f/scene_map.t27", + "spec_hash": "sha256:1c1239443601b49386b1ed81b4372fc66161dc8280dcc31cdc75e27667aec698" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + }, + { + "spec": "specs/t27f/scene_task.t27", + "spec_hash": "sha256:c785e4592d1b2438444de22c0a5828dfa66b63680315806e3a7a6f291c6737a7" + }, + { + "spec": "specs/t27f/scene_tests.t27", + "spec_hash": "sha256:4959ad64ac6816bc90ee71add5ccdd86750187f35edf3637d2a8de3747e464aa" + }, + { + "spec": "specs/t27f/scene_title.t27", + "spec_hash": "sha256:aa0364b409f322a41a9a9f043ab4dbd571dc6a7ba66e12e035146a0bc1c0d3ac" + }, + { + "spec": "specs/t27f/scene_trit.t27", + "spec_hash": "sha256:20c36623fd7582aa8a78b29b96e771cb5c4c06313c9f71884da52656f1df288a" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:1ad642a9965b62c599080af0497003423ca06acfa2a243f171eb9900939c637c", + "gen_hash_rust": "sha256:6d5416bfa6a52c004ae79464082c9d4fad5d6917f4a2a1f1a86599eea77b0a8c", + "gen_hash_verilog": "sha256:bbbf971bf846e160f9150a2a0a0f1a8b0ea2b3d51a94ee0c2f9f6efb837b850f", + "gen_hash_zig": "sha256:ef66a2371f51b9752916d3ccc3ea7e9f6899011d392089bee2ba64b2b5944bc3", + "module": "scene_still", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:28Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:c84d109c67bdf044a19ab106fbb1e6a714ef8dc1b1ef8af4808d6dbf63da5bfd", + "spec_path": "specs/t27f/scene_still.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_task.json b/.trinity/seals/t27f_scene_task.json new file mode 100644 index 0000000000..b17a8127f3 --- /dev/null +++ b/.trinity/seals/t27f_scene_task.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + }, + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:89ee693e2825324221262123a400370ddd768311e9783407f77ab1bd60509884", + "gen_hash_rust": "sha256:2a8e978599ed41a082156b220faec137d0a866ff2bb30bb7b9ac63ec9e4df795", + "gen_hash_verilog": "sha256:81aacc502e86e628c95798e3f4da86f68c227e65ac2918db3fc1590f77a1762b", + "gen_hash_zig": "sha256:2caedbeccad08b6d5a5d25a1772069f29db84cb71b6c143f3ae59b200189676c", + "module": "scene_task", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:25Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:c785e4592d1b2438444de22c0a5828dfa66b63680315806e3a7a6f291c6737a7", + "spec_path": "specs/t27f/scene_task.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_tests.json b/.trinity/seals/t27f_scene_tests.json new file mode 100644 index 0000000000..611b21ba6b --- /dev/null +++ b/.trinity/seals/t27f_scene_tests.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:101ae841df93b9d698c8fb6881b1a832f5e2609ad7eebbc806d508d2c51aec41", + "gen_hash_rust": "sha256:a1172f6ff538381383c5b9546c90974bd821d744d5fb693d92396f69e20296e6", + "gen_hash_verilog": "sha256:fe4dbba2fa1a548dc10f8f5caffb294e85db0579df5b3a4e68f97704997fa771", + "gen_hash_zig": "sha256:fd133cefd6a83aca05057c4dc82f6dc438418ce044f947af8c569f2ed90fc0c6", + "module": "scene_tests", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:13Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:4959ad64ac6816bc90ee71add5ccdd86750187f35edf3637d2a8de3747e464aa", + "spec_path": "specs/t27f/scene_tests.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 3, + "total": 3, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_title.json b/.trinity/seals/t27f_scene_title.json new file mode 100644 index 0000000000..bc88ae6e39 --- /dev/null +++ b/.trinity/seals/t27f_scene_title.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + }, + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:a170056cb79fcc50307006dd225d7fe12bb72083dd8ecedd6df9b7ec2516c9ea", + "gen_hash_rust": "sha256:08c40c2ea5a39cfaeab83bf2ed9cde165760d06425f020091038ef9004394b37", + "gen_hash_verilog": "sha256:e689f0dfe6644e57a3a000319eecadbc4f2706065023f2f239966a94a54d830b", + "gen_hash_zig": "sha256:fc860f5ec236cd94f8ccc98a696fca1fb2a663d145f0425c3310a4bafc89d852", + "module": "scene_title", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:24Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:aa0364b409f322a41a9a9f043ab4dbd571dc6a7ba66e12e035146a0bc1c0d3ac", + "spec_path": "specs/t27f/scene_title.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_scene_trit.json b/.trinity/seals/t27f_scene_trit.json new file mode 100644 index 0000000000..eaf5b5fec5 --- /dev/null +++ b/.trinity/seals/t27f_scene_trit.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/scene_bg.t27", + "spec_hash": "sha256:be66b1113d05d634b2bfb26a1b3d455e2a24b1fd5a4f98da28c9e821978be1cf" + }, + { + "spec": "specs/t27f/scene_lex.t27", + "spec_hash": "sha256:a531db018ba0776b1d044fe39b323ba9f8c739fc60b06b57f6bab538022585c9" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:fc1fc84cc15fbfa8af91e80fd60392ea1ffa8c424a190ded3accac91af52c70e", + "gen_hash_rust": "sha256:d4763a6a0aa3d28cd60e70bdbc9924e0b2dac0728b28e852037c07dbea531088", + "gen_hash_verilog": "sha256:6eb43dd9bc6ce9f3e60af19195867f9422a8a9a15f7f5c7a0c5dec923fc65771", + "gen_hash_zig": "sha256:41c7b925e41dd4f028473066afdcc980994476f4c231a39b8a16e229af0f67db", + "module": "scene_trit", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:15Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:20c36623fd7582aa8a78b29b96e771cb5c4c06313c9f71884da52656f1df288a", + "spec_path": "specs/t27f/scene_trit.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_still_gate.json b/.trinity/seals/t27f_still_gate.json new file mode 100644 index 0000000000..c8de730b5f --- /dev/null +++ b/.trinity/seals/t27f_still_gate.json @@ -0,0 +1,41 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_factory.t27", + "spec_hash": "sha256:96099d764ba86fa3ef2c29d81c3838ce255bfd3b3abbd37b27cb86e4bca91012" + }, + { + "spec": "specs/t27f/scene_raster.t27", + "spec_hash": "sha256:7be7118d865aa65fd1ccb989ab160df2feb46c9ded0a15ca6510b747301ee4a6" + }, + { + "spec": "specs/t27f/scene_still.t27", + "spec_hash": "sha256:c84d109c67bdf044a19ab106fbb1e6a714ef8dc1b1ef8af4808d6dbf63da5bfd" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:5f483f205bbc3e616336801d412c79d2bd30dcf93faab6f0345f4bf58795daa4", + "gen_hash_rust": "sha256:a054fa49e4106208a7d3fce480f1a1c061553eb35823fdcc3348071cd436d2bf", + "gen_hash_verilog": "sha256:44eed75d6a59908d4b5f9ee4d766b1d41f90b6daf373d53f2f47c9f28c840dcf", + "gen_hash_zig": "sha256:eebe29ed19d58887a567b7976967279512392f8932136842288cce55a3ecf967", + "module": "still_gate", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T18:19:31Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:ef0632aac687fc0b377829b79a64de221f9dbe54ea568820dea92c706718325f", + "spec_path": "specs/t27f/still_gate.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/claw/README.md b/docs/claw/README.md index 989fe8ff50..048cfd5a79 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -17,6 +17,8 @@ behind one front door, with the gate logic in `.t27` specs. Epic | Suggestion review on a pull request | `specs/claw/suggest_feed.t27` over `specs/claw/suggest.t27` and `specs/tri/fmt_source.t27`, the `claw-suggest` job of `gate-topology.yml` | #10365, live once the owner steps below are done | | Service skeleton | Railway, thin I/O glue only | T5, merged (spec contract) | | Model routing: Opus 5.5 orchestrates, Sonnet 5.5 runs sub-tasks | `specs/claw/model_route.t27`, skill `model-routing`, `.claude/settings.json` + `.claude/agents/*` | owner directive 2026-10-18 | +| A T27F visual in every answer that needs one | rule `specs/t27mcp/visual_output.t27`, particle-still layer `specs/claw/visual_output.t27`, procedure in skill `t27f`, "Visual in every output" | owner directive 2026-10-18 | +| `reel_still`: one particle still (PNG at STILL_MS of a token_floor reel) from TRI CLAW's `/mcp` | `specs/claw/reel_still.t27`, rendered on the Railway lab only | follow-up of the row above | | Positioning / pricing | `docs/claw/competitors.md` | T6, started | ## The T27F check in every `.t27` repository (#10268) diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 6e4b977d5f..d9708e4e55 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -43,6 +43,9 @@ {"path": "specs/agents/x.t27", "t27b": "pass"}, {"path": "specs/agents/y.t27", "t27b": "pass"}, {"path": "specs/agents/z.t27", "t27b": "pass"}, + {"path": "specs/analog/bench.t27", "t27b": "pass"}, + {"path": "specs/analog/linear.t27", "t27b": "pass"}, + {"path": "specs/analog/mos_table.t27", "t27b": "pass"}, {"path": "specs/api/c_abi.t27", "t27b": "pass"}, {"path": "specs/api/tri_api_context.t27", "t27b": "pass"}, {"path": "specs/api/tri_api_loop.t27", "t27b": "pass"}, @@ -192,6 +195,7 @@ {"path": "specs/claw/model_route.t27", "t27b": "pass"}, {"path": "specs/claw/preview.t27", "t27b": "pass"}, {"path": "specs/claw/recipes.t27", "t27b": "pass"}, + {"path": "specs/claw/reel_still.t27", "t27b": "pass"}, {"path": "specs/claw/refresh.t27", "t27b": "pass"}, {"path": "specs/claw/sarif.t27", "t27b": "pass"}, {"path": "specs/claw/sarif_feed.t27", "t27b": "pass"}, @@ -202,6 +206,7 @@ {"path": "specs/claw/t27f_gate.t27", "t27b": "pass"}, {"path": "specs/claw/t27f_gate_pins.t27", "t27b": "pass"}, {"path": "specs/claw/text.t27", "t27b": "pass"}, + {"path": "specs/claw/visual_output.t27", "t27b": "pass"}, {"path": "specs/compiler/c_array_repeat_wrapper.t27", "t27b": "pass"}, {"path": "specs/compiler/c_fixed_array_return.t27", "t27b": "pass"}, {"path": "specs/compiler/codegen_conformance.t27", "t27b": "pass"}, @@ -1454,12 +1459,29 @@ {"path": "specs/t27f/reel_particles_mux.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_t27_basics.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_topics.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_bg.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_branch.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_code.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_data.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_graph.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_guard.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_lex.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_links.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_loop.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_map.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_raster.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_still.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_task.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_tests.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_title.t27", "t27b": "pass"}, + {"path": "specs/t27f/scene_trit.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_derive.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_export.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_fold.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_outline.t27", "t27b": "pass"}, {"path": "specs/t27f/spec_graph_view.t27", "t27b": "pass"}, + {"path": "specs/t27f/still_gate.t27", "t27b": "pass"}, {"path": "specs/t27f/token_floor.t27", "t27b": "blocked", "blocker": "type [N]T", "reason": "unimplemented"}, {"path": "specs/t27f/token_floor_mux.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"}, {"path": "specs/t27f/token_floor_scenes.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"}, @@ -1475,6 +1497,7 @@ {"path": "specs/t27mcp/spec_brief.t27", "t27b": "pass"}, {"path": "specs/t27mcp/telegram.t27", "t27b": "pass"}, {"path": "specs/t27mcp/tool_schema.t27", "t27b": "codegen", "blocker": "t27b: unsupported construct FnDecl(frame or branch range) at line 0 (fn the round trip fixtures: a tuple, arrays, a boolean, nested objects: conditional branch out of range (function too large))", "reason": "unimplemented"}, + {"path": "specs/t27mcp/visual_output.t27", "t27b": "pass"}, {"path": "specs/ternary/activation_quantizer.t27", "t27b": "pass"}, {"path": "specs/ternary/bitnet_layer.t27", "t27b": "pass"}, {"path": "specs/ternary/bitnet_majority.t27", "t27b": "pass"}, diff --git a/specs/claw/reel_still.t27 b/specs/claw/reel_still.t27 new file mode 100644 index 0000000000..506224df07 --- /dev/null +++ b/specs/claw/reel_still.t27 @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/reel_still.t27 -- reel_still, the TRI CLAW /mcp tool that renders one particle still. +; specs/claw/visual_output.t27 says a report's visual is one PNG frame of a token_floor reel taken at +; STILL_MS, rendered on the Railway lab, and that no MCP tool renders one yet. This spec is that tool: +; what it takes, what it refuses, and the exact lab recipe it runs. +; -- +; Input (four fields): topic, lesson, lang, aspect. +; - topic "" or "token-floor" with lesson -1 is the physics reel (token_floor + token_floor_mux). +; - A lesson id from the t27-basics course ("what-a-spec-is", "the-module-line", ...; the ids are +; specs/t27f/reel_topics.t27's, so this tool and the reels never disagree about a lesson) is that +; lesson's reel (lesson_reel). The lesson index 0..TOPICS-1 names the same reel; giving both is +; allowed only when they agree. +; - lang 0 en, 1 ru. aspect 0 tall (1080x1920), 1 wide (1920x1080); one PNG is one aspect, so the +; "both" cut token_floor's TF_CUT also accepts is refused here. +; Output: one image/png, the frame the reel shows at STILL_MS. +; -- +; The recipe renders one frame, not the reel. frame_shard splits a reel into n shards; with +; n = SCENE_FRAMES every shard is one frame, so TF_SHARD = still_frame(), TF_SHARDS = SCENE_FRAMES +; draws exactly frame still_frame() (the physics still steps from frame 0; only drawing is skipped). +; The overlays (captions, labels, voice and music, which the still does not need but the filter graph +; reads) are the reel's own filter graph, and its drawtext overlays are keyed on the filter frame +; index n, not on time. A single input frame is n = 0 however it is offset in time, so `-itsoffset 9` +; draws the frame-0 overlays: measured, that still matched the raw particle frame (42.9 dB PSNR) and +; not the reel (23.3 dB). The recipe pads instead: the graph's `[0:v]` gets +; `tpad=start=pad_frames()` in front, so the one frame arrives as n = still_frame() at t = STILL_MS. +; The last frame of `[v]` goes to the PNG (`-update 1`), `[a]` to null. Measured on the lab +; 2026-10-18, lesson 1 en tall: rc 0, 2585 ms, 499707 bytes, 49.3 dB PSNR against frame 270 of the +; reel's own mp4 (21.7 dB against frame 269, 22.0 dB against 271). +; -- +; It runs on the Railway lab only (owner rule 2026-10-09); a call anywhere else is refused, never +; rendered. The name has no t27_ or t27f_ prefix, so the one registry (specs/t27mcp/service.t27) homes +; it in TRI CLAW and mounts it as it is. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawReelStill; + +use t27mcp::service::admit; +use t27mcp::service::home_of; +use t27mcp::service::name_ok; +use t27mcp::service::H_T27; +use t27mcp::service::H_TRI_CLAW; +use t27mcp::service::A_MOUNT; +use t27mcp::service::A_REFUSE_COLLISION; +use claw::visual_output::STILL_MS; +use claw::visual_output::still_ms_ok; +use t27f::token_floor_scenes::SCENE_FPS; +use t27f::token_floor_scenes::SCENE_FRAMES; +use t27f::frame_shard::first_of; +use t27f::frame_shard::end_of; +use t27f::reel_topics::TOPICS; +use t27f::reel_topics::id_item; +use t27f::reel_topics::item_len; +use t27f::reel_topics::cp; + +/// The tool's name in TRI CLAW's /mcp. +pub const TOOL_NAME : str = "reel_still"; +pub const MIME : str = "image/png"; + +/// The input fields, in the order the tool's schema lists them. +pub const INPUT_COUNT : u32 = 4; +pub const INPUT_NAMES : [4]str = ["topic", "lesson", "lang", "aspect"]; + +/// The physics reel's topic name, and the lesson field's "no lesson". +pub const PHYSICS_TOPIC : str = "token-floor"; +pub const LESSON_NONE : i32 = -1; + +/// Which reel a call resolves to. +pub const REEL_PHYSICS : i32 = -1; // otherwise the lesson index 0..TOPICS-1 +pub const REEL_NONE : i32 = -2; // the call is refused + +pub const LANG_EN : i32 = 0; +pub const LANG_RU : i32 = 1; +pub const LANG_NAMES : [2]str = ["en", "ru"]; + +/// The aspect is token_floor's TF_CUT value for that cut. +pub const ASPECT_TALL : i32 = 0; +pub const ASPECT_WIDE : i32 = 1; +pub const ASPECT_NAMES : [2]str = ["tall", "wide"]; + +/// What the tool answers. +pub const V_OK : u32 = 0; +pub const V_UNKNOWN_TOPIC : u32 = 1; +pub const V_BAD_LESSON : u32 = 2; +pub const V_DISAGREE : u32 = 3; +pub const V_BAD_LANG : u32 = 4; +pub const V_BAD_ASPECT : u32 = 5; +pub const V_NOT_ON_LAB : u32 = 6; + +/// The file the still is written to: STILL_PREFIX id - lang - aspect STILL_EXT. +pub const STILL_PREFIX : str = "still-"; +pub const STILL_EXT : str = ".png"; + +/// Whether lesson t's course id is s, code point by code point. +pub fn id_is(t: u32, s: str) -> bool { + let i : usize = id_item(t as usize); + let n : usize = item_len(i); + if (n != s.len) { return false; } + var k : usize = 0; + while (k < n) { + if ((s[k] as u32) != cp(i, k)) { return false; } + k = k + 1; + } + return true; +} + +/// The lesson whose id is s, or REEL_PHYSICS for the physics reel's names, or REEL_NONE. +pub fn topic_reel(s: str) -> i32 { + if (s.len == 0 or s == PHYSICS_TOPIC) { return REEL_PHYSICS; } + var t : u32 = 0; + while (t < (TOPICS as u32)) { + if (id_is(t, s)) { return t as i32; } + t = t + 1; + } + return REEL_NONE; +} + +/// Whether a lesson field is "none" or a lesson index. +pub fn lesson_ok(lesson: i32) -> bool { + return lesson == LESSON_NONE or (lesson >= 0 and lesson < (TOPICS as i32)); +} + +/// The reel a call names: from topic, from lesson, or from both when they agree. +pub fn reel_of(topic: str, lesson: i32) -> i32 { + if (lesson_ok(lesson) == false) { return REEL_NONE; } + let by_topic : i32 = topic_reel(topic); + if (by_topic == REEL_NONE) { return REEL_NONE; } + if (lesson == LESSON_NONE) { return by_topic; } + if (topic.len == 0) { return lesson; } + if (by_topic == lesson) { return lesson; } + return REEL_NONE; +} + +/// The tool's verdict on a call; only V_OK renders. +pub fn verdict(topic: str, lesson: i32, lang: i32, aspect: i32, on_lab: bool) -> u32 { + if (lesson_ok(lesson) == false) { return V_BAD_LESSON; } + if (topic_reel(topic) == REEL_NONE) { return V_UNKNOWN_TOPIC; } + if (reel_of(topic, lesson) == REEL_NONE) { return V_DISAGREE; } + if (lang != LANG_EN and lang != LANG_RU) { return V_BAD_LANG; } + if (aspect != ASPECT_TALL and aspect != ASPECT_WIDE) { return V_BAD_ASPECT; } + if (on_lab == false) { return V_NOT_ON_LAB; } + return V_OK; +} + +/// The frame the reel shows at STILL_MS. +pub fn still_frame() -> i32 { + return @divTrunc(STILL_MS * SCENE_FPS, 1000); +} + +/// The renderer's environment for that one frame: TF_SHARD, TF_SHARDS, TF_CUT. +pub fn env_shard() -> i32 { return still_frame(); } +pub fn env_shards() -> i32 { return SCENE_FRAMES; } +pub fn env_cut(aspect: i32) -> i32 { return aspect; } + +/// The frame's size and its rgb24 bytes, the length of the renderer's stream for one frame. +pub fn frame_w(aspect: i32) -> i32 { + if (aspect == ASPECT_WIDE) { return 1920; } + return 1080; +} +pub fn frame_h(aspect: i32) -> i32 { + if (aspect == ASPECT_WIDE) { return 1080; } + return 1920; +} +pub fn frame_bytes(aspect: i32) -> i32 { + return frame_w(aspect) * frame_h(aspect) * 3; +} + +/// The frames `tpad=start=` puts in front of the one frame, so the reel's filter graph sees it as +/// frame index n = still_frame(). +pub fn pad_frames() -> i32 { + return still_frame(); +} + +/// The filter frame index n the one input frame has after `pad` frames of padding. The overlays are +/// keyed on n, so this, not the frame's timestamp, decides what they draw. +pub fn graph_n(pad: i32) -> i32 { + return pad; +} + +/// The still's file name length: prefix, id, "-", lang, "-", aspect, extension. +pub fn name_len(reel: i32, lang: i32, aspect: i32) -> usize { + var id_n : usize = PHYSICS_TOPIC.len; + if (reel >= 0) { id_n = item_len(id_item(reel as usize)); } + return STILL_PREFIX.len + id_n + 1 + LANG_NAMES[lang as usize].len + 1 + ASPECT_NAMES[aspect as usize].len + STILL_EXT.len; +} + +test the_tool_is_tri_claws_and_mounted_as_it_is { + assert name_ok(TOOL_NAME); + assert home_of(TOOL_NAME) == H_TRI_CLAW; + assert admit(TOOL_NAME, H_TRI_CLAW) == A_MOUNT; + assert INPUT_NAMES.len == INPUT_COUNT as usize; +} + +test negative_control_under_a_t27f_name_it_would_be_refused { + assert home_of("t27f_reel_still") == H_T27; + assert admit("t27f_reel_still", H_TRI_CLAW) == A_REFUSE_COLLISION; +} + +test a_topic_names_the_physics_reel_or_a_lesson { + assert topic_reel("") == REEL_PHYSICS; + assert topic_reel("token-floor") == REEL_PHYSICS; + assert topic_reel("what-a-spec-is") == 0; + assert topic_reel("the-module-line") == 1; + assert topic_reel("constants") == 3; +} + +test every_lesson_id_resolves_to_its_own_index { + var t : u32 = 0; + var hits : u32 = 0; + while (t < (TOPICS as u32)) { + let i : usize = id_item(t as usize); + assert item_len(i) > 0; + if (id_is(t, "what-a-spec-is")) { hits = hits + 1; } + t = t + 1; + } + assert hits == 1; + assert TOPICS == 27; +} + +test negative_control_a_near_miss_topic_is_unknown { + assert topic_reel("what-a-spec") == REEL_NONE; + assert topic_reel("what-a-spec-is-") == REEL_NONE; + assert topic_reel("What-a-spec-is") == REEL_NONE; + assert topic_reel("token_floor") == REEL_NONE; + assert id_is(0, "the-module-line") == false; +} + +test topic_and_lesson_agree_or_the_call_is_refused { + assert reel_of("", LESSON_NONE) == REEL_PHYSICS; + assert reel_of("", 1) == 1; + assert reel_of("the-module-line", LESSON_NONE) == 1; + assert reel_of("the-module-line", 1) == 1; + assert reel_of("the-module-line", 2) == REEL_NONE; + assert reel_of("token-floor", 0) == REEL_NONE; + assert reel_of("", 27) == REEL_NONE; + assert reel_of("", -2) == REEL_NONE; +} + +test the_verdict_names_what_is_wrong_first { + assert verdict("the-module-line", LESSON_NONE, LANG_EN, ASPECT_TALL, true) == V_OK; + assert verdict("", LESSON_NONE, LANG_RU, ASPECT_WIDE, true) == V_OK; + assert verdict("nope", LESSON_NONE, LANG_EN, ASPECT_TALL, true) == V_UNKNOWN_TOPIC; + assert verdict("", 40, LANG_EN, ASPECT_TALL, true) == V_BAD_LESSON; + assert verdict("constants", 0, LANG_EN, ASPECT_TALL, true) == V_DISAGREE; + assert verdict("", 0, 2, ASPECT_TALL, true) == V_BAD_LANG; + assert verdict("", 0, LANG_EN, 2, true) == V_BAD_ASPECT; + assert verdict("", 0, LANG_EN, -1, true) == V_BAD_ASPECT; +} + +test negative_control_a_good_call_off_the_lab_is_refused_not_rendered { + assert verdict("the-module-line", LESSON_NONE, LANG_EN, ASPECT_TALL, false) == V_NOT_ON_LAB; + assert verdict("", LESSON_NONE, LANG_EN, ASPECT_WIDE, false) == V_NOT_ON_LAB; +} + +test the_still_is_frame_270_of_450_and_the_rule_allows_it { + assert still_frame() == 270; + assert still_frame() < SCENE_FRAMES; + assert SCENE_FPS == 30; + assert still_ms_ok(STILL_MS); + assert @rem(STILL_MS * SCENE_FPS, 1000) == 0; +} + +test the_padded_frame_is_the_overlays_frame_270 { + assert graph_n(pad_frames()) == still_frame(); + assert @divTrunc(pad_frames() * 1000, SCENE_FPS) == STILL_MS; +} + +test negative_control_an_unpadded_frame_draws_the_frame_0_overlays { + // -itsoffset moves the timestamp but not n; this is the recipe the lab measured wrong. + assert graph_n(0) == 0; + assert graph_n(0) != still_frame(); + assert graph_n(pad_frames() - 1) != still_frame(); +} + +test the_shard_env_draws_exactly_the_still_frame { + assert first_of(env_shard(), env_shards(), SCENE_FRAMES) == still_frame(); + assert end_of(env_shard(), env_shards(), SCENE_FRAMES) == still_frame() + 1; + assert env_cut(ASPECT_TALL) == 0; + assert env_cut(ASPECT_WIDE) == 1; +} + +test negative_control_a_halved_shard_count_renders_the_whole_reel { + let half : i32 = @divTrunc(env_shards(), 2); + assert first_of(env_shard(), half, SCENE_FRAMES) == 0; + assert end_of(env_shard(), half, SCENE_FRAMES) == SCENE_FRAMES; + assert first_of(env_shard(), 1, SCENE_FRAMES) == 0; +} + +test one_frame_is_6220800_bytes_in_either_aspect { + assert frame_w(ASPECT_TALL) == 1080; + assert frame_h(ASPECT_TALL) == 1920; + assert frame_w(ASPECT_WIDE) == 1920; + assert frame_h(ASPECT_WIDE) == 1080; + assert frame_bytes(ASPECT_TALL) == 6220800; + assert frame_bytes(ASPECT_WIDE) == frame_bytes(ASPECT_TALL); +} + +test the_file_name_is_prefix_id_lang_aspect { + // still-what-a-spec-is-en-tall.png + assert name_len(0, LANG_EN, ASPECT_TALL) == 32; + // still-token-floor-ru-wide.png + assert name_len(REEL_PHYSICS, LANG_RU, ASPECT_WIDE) == 29; + assert name_len(1, LANG_EN, ASPECT_TALL) != name_len(0, LANG_EN, ASPECT_TALL); +} diff --git a/specs/claw/visual_output.t27 b/specs/claw/visual_output.t27 new file mode 100644 index 0000000000..c275dc4276 --- /dev/null +++ b/specs/claw/visual_output.t27 @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/visual_output.t27 -- the particle-still layer of TRI CLAW's visual-output rule. +; Owner order 2026-10-18: every output in Claude Code carries a T27F visual, written into the TRI +; CLAW rules, and the T27F meant here is the particle video: the token_floor world +; (specs/t27f/token_floor.t27, token_floor_scenes.t27, token_floor_mux.t27, lesson_reel.t27). +; -- +; The rule itself has one home, specs/t27mcp/visual_output.t27 (owner pick 2026-10-18): which answers +; carry a visual (needs_visual), how it reaches each host (answer_channel), and the verdict on a +; drawing made from t27f tool rows (answer_verdict). This spec imports those and adds only what is +; particular to a particle still: +; - Which answer's visual is a still: a report of a piece of work (K_REPORT). A spec, a flow or +; numbers are drawn from t27f tool rows, as the t27mcp law says; a still backs no row, so it is +; not their visual. +; - The still is one PNG frame of a rendered token_floor reel, taken at STILL_MS: after the hook has +; written its statement and before the CTA hold. +; - It is rendered on the Railway lab, never on the owner's Mac (owner rule 2026-10-09). No MCP tool +; renders one yet; the lab recipe (render the reel, then `ffmpeg -ss -frames:v 1`) is the only +; path, and a reel_still tool in TRI CLAW's /mcp is the follow-up. +; - It is a file, not an inline widget, so its channel is answer_channel for a drawing on a host with +; no visual tool. On the desktop Code tab that is the file pane, which opens only files inside the +; session's folder (measured 2026-10-18), so the PNG goes there. +; - A still that could not be made is reported missing, with the reason; an older file is never shown +; as this turn's. +; Not measured: whether TRI CLAW's chat shows a PNG URL (the t27mcp header measured .svg only). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawVisualOutput; + +use t27mcp::visual_output::needs_visual; +use t27mcp::visual_output::answer_channel; +use t27mcp::visual_output::K_SPEC; +use t27mcp::visual_output::K_FLOW; +use t27mcp::visual_output::K_NUMBERS; +use t27mcp::visual_output::K_REPORT; +use t27mcp::visual_output::K_ONE_SENTENCE; +use t27mcp::visual_output::K_QUESTION; +use t27mcp::visual_output::HOST_CLAUDE_CODE_CLI; +use t27mcp::visual_output::HOST_CLAUDE_CODE_DESKTOP; +use t27mcp::visual_output::HOST_MCP_APPS; +use t27mcp::visual_output::HOST_TRI_CLAW; +use t27mcp::visual_output::CH_FILE_PATH; +use t27mcp::visual_output::CH_FILE_AND_PANE; +use t27mcp::visual_output::CH_UI_RESOURCE; +use t27mcp::visual_output::CH_IMAGE_URL; +use t27f::reel_marketing::REEL_HOOK_MS; +use t27f::reel_marketing::REEL_CTA_MIN_MS; +use t27f::reel_marketing::REEL_MIN_MS; + +/// The still is this frame of a reel: past the hook's decision window, before the CTA hold. +pub const STILL_MS : i32 = 9000; + +/// Where the still's file is put. +pub const PLACE_ANY : u32 = 0; // any path, printed in the answer +pub const PLACE_SESSION : u32 = 1; // inside the session's folder + +/// Whether an answer of this kind shows a particle still as its visual. +pub fn takes_still(kind: u32) -> bool { + return kind == K_REPORT; +} + +/// How the still reaches the user: a drawn file on a host with no inline visual tool. +pub fn still_channel(host: u32) -> u32 { + return answer_channel(host, false, true); +} + +/// The file goes inside the session folder wherever the host opens it in a pane. +pub fn place_for(host: u32) -> u32 { + if (still_channel(host) == CH_FILE_AND_PANE) { + return PLACE_SESSION; + } + return PLACE_ANY; +} + +/// A still frame shows the statement, not the hook still writing it and not the CTA hold. +pub fn still_ms_ok(ms: i32) -> bool { + return ms > REEL_HOOK_MS and ms < REEL_MIN_MS - REEL_CTA_MIN_MS; +} + +/// An answer is honest about its still when the still was made this turn or is said missing. +pub fn still_report_ok(kind: u32, made: bool, said_missing: bool) -> bool { + if (takes_still(kind) == false) { + return true; + } + return made or said_missing; +} + +test only_a_report_takes_a_still { + assert takes_still(K_REPORT); + assert takes_still(K_SPEC) == false; + assert takes_still(K_FLOW) == false; + assert takes_still(K_NUMBERS) == false; + assert takes_still(K_ONE_SENTENCE) == false; + assert takes_still(K_QUESTION) == false; +} + +test every_still_is_an_answer_the_t27mcp_law_gives_a_visual { + var k : u32 = 0; + var stills : u32 = 0; + while (k <= K_QUESTION) { + if (takes_still(k)) { + assert needs_visual(k); + stills = stills + 1; + } + k = k + 1; + } + assert stills == 1; +} + +test the_still_is_a_file_not_a_widget { + assert still_channel(HOST_CLAUDE_CODE_DESKTOP) == CH_FILE_AND_PANE; + assert still_channel(HOST_CLAUDE_CODE_CLI) == CH_FILE_PATH; + assert still_channel(HOST_MCP_APPS) == CH_UI_RESOURCE; + assert still_channel(HOST_TRI_CLAW) == CH_IMAGE_URL; +} + +test the_desktop_file_sits_in_the_session_folder { + assert place_for(HOST_CLAUDE_CODE_DESKTOP) == PLACE_SESSION; + assert place_for(HOST_CLAUDE_CODE_CLI) == PLACE_ANY; + assert place_for(HOST_MCP_APPS) == PLACE_ANY; + assert place_for(HOST_TRI_CLAW) == PLACE_ANY; +} + +test the_still_is_past_the_hook_and_before_the_cta { + assert still_ms_ok(STILL_MS); + assert still_ms_ok(REEL_HOOK_MS) == false; + assert still_ms_ok(1000) == false; + assert still_ms_ok(14000) == false; +} + +test a_missing_still_is_said_never_skipped { + assert still_report_ok(K_REPORT, true, false); + assert still_report_ok(K_REPORT, false, true); + assert still_report_ok(K_REPORT, false, false) == false; + assert still_report_ok(K_SPEC, false, false); + assert still_report_ok(K_ONE_SENTENCE, false, false); +} diff --git a/specs/t27f/scene_bg.t27 b/specs/t27f/scene_bg.t27 new file mode 100644 index 0000000000..c27b5624c4 --- /dev/null +++ b/specs/t27f/scene_bg.t27 @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_bg.t27 -- the cell texture behind every scene, and the 24-step circle table the +// round scenes use. Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: two stills are compared by SSIM, and a flat colour is the same structure in any picture: two +// dark frames with one small hero each score near 1000 permille whatever the heroes are. A scene +// therefore never leaves its backdrop flat. The backdrop is a field of cells whose brightness is a +// hash of the lesson's seed, so another lesson draws uncorrelated cells, and six cell shapes (grid, +// bricks, diamonds, bars, rings, bands) so another archetype draws another geometry. +// +// THE CALL: bg_px(st, acc, sd, x, y) is the colour of canvas point (x, y): st is the cell shape 0..5, +// acc the accent (a 14 % tint of it goes into every cell), sd the seed from seed_of(t, n, st). +// +// NOT CLAIMED: it is a texture, not a pattern with a meaning; the cell sizes are 12..24 canvas pixels. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_bg; + +use t27f::scene_raster::fmod; +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::rnd; +use t27f::scene_raster::luma; + +pub const COS24 : [24]i32 = [1000, 966, 866, 707, 500, 259, 0, -259, -500, -707, -866, -966, -1000, -966, -866, -707, -500, -259, 0, 259, 500, 707, 866, 966]; +pub const SIN24 : [24]i32 = [0, 259, 500, 707, 866, 966, 1000, 966, 866, 707, 500, 259, 0, -259, -500, -707, -866, -966, -1000, -966, -866, -707, -500, -259]; + +// cos and sin of 15 degrees times k, times 1000; k wraps. +pub fn cos24(k: i32) -> i32 { + return COS24[@as(usize, @intCast(fmod(k, 24)))]; +} +pub fn sin24(k: i32) -> i32 { + return SIN24[@as(usize, @intCast(fmod(k, 24)))]; +} + +// The seed of lesson t's scene st with content count n. +pub fn seed_of(t: usize, n: i32, st: i32) -> i32 { + return @as(i32, @intCast(t)) * 977 + n * 31 + st * 5 + 11; +} + +pub fn cell_size(sd: i32) -> i32 { + return 12 + rnd(sd, 4) * 4; +} + +// The cell that canvas point (x, y) is in, as a number; two points of one cell share it. +pub fn cell_id(st: i32, sd: i32, x: i32, y: i32) -> i32 { + const cs = cell_size(sd); + const px = x + 1200; + const py = y + 1200; + var id : i32 = 0; + if (st == 0) { id = @divTrunc(py, cs) * 256 + @divTrunc(px, cs); } + if (st == 1) { + const row = @divTrunc(py, cs); + id = row * 256 + @divTrunc(px + fmod(row, 2) * cs, cs * 2); + } + if (st == 2) { id = @divTrunc(px + py, cs) * 256 + @divTrunc(px - py + 2400, cs); } + if (st == 3) { id = @divTrunc(px, cs); } + if (st == 4) { + const dx = x - (120 + rnd(sd + 1, 120)); + const dy = y - (200 + rnd(sd + 2, 240)); + id = @divTrunc(dx * dx + dy * dy, cs * cs * 5); + } + if (st == 5) { id = @divTrunc(py, cs); } + return id; +} + +pub fn bg_px(st: i32, acc: i32, sd: i32, x: i32, y: i32) -> i32 { + const cs = cell_size(sd); + const id = cell_id(st, sd, x, y); + var l : i32 = 14 + rnd(sd * 131 + id, 6) * 8; + if (st == 0 && (fmod(x + 1200, cs) == 0 || fmod(y + 1200, cs) == 0)) { l = l - 8; } + return mix(rgb(l, l + 2, l + 8), acc, 36); +} + +// ---- tests ---- + +test "the circle table is a circle" { + var k : i32 = 0; + while (k < 24) { + const r2 = cos24(k) * cos24(k) + sin24(k) * sin24(k); + assert(r2 > 990000 && r2 < 1010000); + k = k + 1; + } + assert(cos24(0) == 1000); + assert(sin24(6) == 1000); + assert(cos24(12) == 0 - 1000); + assert(cos24(24) == cos24(0)); + assert(sin24(0 - 18) == sin24(6)); +} + +test "cells share a colour and neighbours mostly do not" { + const sd = seed_of(3, 5, 0); + const cs = cell_size(sd); + assert(cs >= 12 && cs <= 24); + // two points of one grid cell are one colour (clear of the cell's edge line) + const x0 = (@divTrunc(100 + 1200, cs) + 1) * cs - 1200 + 2; + const y0 = (@divTrunc(100 + 1200, cs) + 1) * cs - 1200 + 2; + assert(bg_px(0, 0xF5B700, sd, x0, y0) == bg_px(0, 0xF5B700, sd, x0 + 1, y0 + 1)); + // across a sampled row, many distinct colours + var distinct : i32 = 0; + var last : i32 = 0 - 1; + var x : i32 = 0; + while (x < 360) { + const c = bg_px(0, 0xF5B700, sd, x, 200); + if (c != last) { distinct = distinct + 1; } + last = c; + x = x + 1; + } + assert(distinct > 15); +} + +test "another lesson draws other cells: the textures are uncorrelated" { + var st : i32 = 0; + while (st < 6) { + var same : i32 = 0; + var seen : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = bg_px(st, 0x8B5CF6, seed_of(2, 4, st), x, y); + const b = bg_px(st, 0x8B5CF6, seed_of(9, 4, st), x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a == b) { same = same + 1; } + seen = seen + 1; + x = x + 7; + } + y = y + 9; + } + // 6 brightness levels: two unrelated fields agree on about one pixel in six + assert(same * 100 < seen * 40); + st = st + 1; + } +} + +test "negative control: the same seed gives the same texture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (bg_px(4, 0x22C55E, seed_of(5, 2, 4), x, y) != bg_px(4, 0x22C55E, seed_of(5, 2, 4), x, y)) { diff = diff + 1; } + x = x + 13; + } + y = y + 11; + } + assert(diff == 0); +} + +test "the backdrop is dark: no pixel is as bright as a hero" { + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + assert(luma(bg_px(1, 0xF5F5F5, seed_of(7, 1, 1), x, y)) < 110); + x = x + 9; + } + y = y + 13; + } +} diff --git a/specs/t27f/scene_branch.t27 b/specs/t27f/scene_branch.t27 new file mode 100644 index 0000000000..7c75ef9aec --- /dev/null +++ b/specs/t27f/scene_branch.t27 @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_branch.t27 -- scene A_BRANCH of reel_factory.t27: a decision tree. The stream +// starts at the foot and splits at every level, once per `if` or `switch` the lesson counts; the +// path the lesson's own bytes pick is drawn thick and lit in the accent, the others thin and dim. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: a branch is a place where the program chooses. The depth of the tree is the lesson's content +// count (3 to 5 levels), and the path taken at each level is one bit of the lesson's code. +// +// THE FUNCTION: branch_px(t, acc, n, x, y) is the colour of canvas point (x, y). Level l has 2^l +// nodes spread across 360 pixels; level 0 is at y = 570 and the top level at y = 90. +// +// NOT CLAIMED: the path is a choice read from the bytes, not the lesson's control flow. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_branch; + +use t27f::scene_raster::fmod; +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_disc; +use t27f::scene_raster::near_seg; +use t27f::scene_raster::clamp; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_lex::lex_byte; + +pub const TOP_Y : i32 = 90; +pub const FOOT_Y : i32 = 570; +pub const DIM_EDGE : i32 = 0x39426B; + +pub fn levels(n: i32) -> i32 { + return clamp(n + 2, 3, 5); +} +pub fn pow2(l: i32) -> i32 { + var p : i32 = 1; + var k : i32 = 0; + while (k < l) { p = p * 2; k = k + 1; } + return p; +} +// The y of level l of a tree of lv levels. +pub fn level_y(l: i32, lv: i32) -> i32 { + return FOOT_Y - @divTrunc(l * (FOOT_Y - TOP_Y), lv); +} +// The x of node i of level l: the 2^l nodes share the 360 pixels. +pub fn node_x(l: i32, i: i32) -> i32 { + return @divTrunc((i * 2 + 1) * 360, pow2(l) * 2); +} +// The index, at level l, of the node on the path the lesson takes. +pub fn path_at(t: usize, l: i32) -> i32 { + var i : i32 = 0; + var k : i32 = 1; + while (k <= l) { + i = i * 2 + fmod(lex_byte(t, k * 5 + 1), 2); + k = k + 1; + } + return i; +} + +pub fn branch_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(5, acc, seed_of(t, n, 5), x, y); + if (y < TOP_Y - 14 || y > FOOT_Y + 14) { return c0; } + const lv = levels(n); + var l : i32 = 1; + var hit : i32 = 0; + while (l <= lv) { + const ya = level_y(l, lv); + const yb = level_y(l - 1, lv); + if (y >= ya - 12 && y <= yb + 12) { + const cnt = pow2(l); + const tk = path_at(t, l); + var i : i32 = 0; + while (i < cnt) { + const px = node_x(l, i); + const pp = node_x(l - 1, @divTrunc(i, 2)); + const on = (i == tk); + if (on && in_disc(x, y, px, ya, 11)) { return acc; } + if (!on && in_disc(x, y, px, ya, 6)) { return DIM_EDGE; } + if (on && near_seg(x, y, pp, yb, px, ya, 3)) { return acc; } + if (!on && near_seg(x, y, pp, yb, px, ya, 1)) { hit = 1; } + i = i + 1; + } + } + l = l + 1; + } + if (in_disc(x, y, node_x(0, 0), level_y(0, lv), 14)) { return rgb(245, 245, 245); } + if (hit == 1) { return DIM_EDGE; } + return c0; +} + +// ---- tests ---- + +test "the tree has 3 to 5 levels and its nodes fit the canvas" { + assert(levels(0) == 3); + assert(levels(2) == 4); + assert(levels(9) == 5); + var l : i32 = 0; + while (l <= 5) { + var i : i32 = 0; + while (i < (pow2(l))) { + assert(node_x(l, i) > 0 && node_x(l, i) < 360); + i = i + 1; + } + assert(level_y(l, 5) >= TOP_Y && level_y(l, 5) <= FOOT_Y); + l = l + 1; + } + assert(level_y(0, 4) == FOOT_Y); + assert(level_y(4, 4) == TOP_Y); +} + +test "the path is one node a level and follows the lesson's bytes" { + var l : i32 = 0; + while (l <= 5) { + assert(path_at(13, l) >= 0 && path_at(13, l) < (pow2(l))); + l = l + 1; + } + var differ : i32 = 0; + var t : usize = 1; + while (t < 27) { + if (path_at(t, 5) != path_at(t - 1, 5)) { differ = differ + 1; } + t = t + 1; + } + assert(differ > 8); +} + +test "another lesson draws another tree" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = branch_px(13, 0xE0409A, 3, x, y); + const b = branch_px(20, 0xE0409A, 3, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (branch_px(8, 0x3B82F6, 2, x, y) != branch_px(8, 0x3B82F6, 2, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_code.t27 b/specs/t27f/scene_code.t27 new file mode 100644 index 0000000000..d2ba512000 --- /dev/null +++ b/specs/t27f/scene_code.t27 @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_code.t27 -- scene A_CODE of reel_factory.t27: the lesson's own code, set as a card +// of source with a gutter of line numbers. Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: the scene every lesson can have. The card's height is the lesson's line count and each of +// its lines is the lesson's line, so a four-line lesson and a twenty-line lesson do not make one +// picture. The first word of every line is set in the accent (the keyword rhythm: `pub`, `fn`, +// `const`, `test`), digits in blue, a quoted string in green, the rest pale. +// +// THE FUNCTION: code_px(t, acc, n, x, y) is the colour of canvas point (x, y) in lesson t's code scene. +// acc is the accent colour of the plan, n is unused here (the code scene counts lines itself). The +// canvas is scene_raster's 360 x 640; scene_still.t27 moves and scales it for the camera and the +// layout before it calls this. +// +// NOT CLAIMED: lines longer than COLS characters are cut at the card's edge, not wrapped; no +// keyword table is read here (that is reel_factory's), only the first word of a line is coloured. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_code; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::in_disc; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_raster::char_on; +use t27f::scene_raster::num_on; +use t27f::reel_topics::code_line0; +use t27f::reel_topics::code_lines; +use t27f::reel_topics::line_len; +use t27f::reel_topics::code_byte; + +pub const PANEL_X : i32 = 14; +pub const PANEL_Y : i32 = 118; +pub const PANEL_W : i32 = 332; +pub const BAR : i32 = 24; +pub const LINE_H : i32 = 12; +pub const ADV : i32 = 8; +pub const PAD : i32 = 10; +pub const GUTTER : i32 = 3; // columns taken by the line number and a space +pub const COLS : i32 = 38; // columns the card shows, gutter included + +pub const BG_CARD : i32 = 0x0E1222; +pub const INK : i32 = 0xDCE1F0; +pub const INK_DIM : i32 = 0x59607A; +pub const INK_NUM : i32 = 0x78BEFF; +pub const INK_STR : i32 = 0x4ADE80; + +// The cell texture behind the card (scene_bg, grid cells), seeded by the lesson. +pub fn backdrop(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + return bg_px(0, acc, seed_of(t, n, 0), x, y); +} + +// The height of the card for lesson t, bottom edge. +pub fn card_bottom(t: usize) -> i32 { + return PANEL_Y + BAR + @as(i32, @intCast(code_lines(t))) * LINE_H + PAD; +} + +fn is_letter(c: u8) -> bool { + return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c == 95; +} +fn is_digit(c: u8) -> bool { + return c >= 48 && c <= 57; +} +// The column of the first letter of line l and the column just past its first word. +fn first_word_start(l: usize) -> usize { + var k : usize = 0; + while (k < line_len(l) && !is_letter(code_byte(l, k))) { k = k + 1; } + return k; +} +fn first_word_end(l: usize) -> usize { + var k : usize = first_word_start(l); + while (k < line_len(l) && (is_letter(code_byte(l, k)) || is_digit(code_byte(l, k)))) { k = k + 1; } + return k; +} +// Whether column k of line l sits inside a quoted string (an odd number of quotes before it). +fn in_string(l: usize, k: usize) -> bool { + var q : usize = 0; + var j : usize = 0; + while (j < k) { + if (code_byte(l, j) == 34) { q = q + 1; } + j = j + 1; + } + return q % 2 == 1 || code_byte(l, k) == 34; +} + +// The colour of the character at column k of line l. +pub fn ink(l: usize, k: usize, acc: i32) -> i32 { + const c = code_byte(l, k); + if (in_string(l, k)) { return INK_STR; } + if (k >= first_word_start(l) && k < first_word_end(l)) { return acc; } + if (is_digit(c)) { return INK_NUM; } + if (is_letter(c)) { return INK; } + return INK_DIM; +} + +pub fn code_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + var c = backdrop(t, acc, n, x, y); + const bot = card_bottom(t); + if (!in_rect(x, y, PANEL_X - 2, PANEL_Y - 2, PANEL_X + PANEL_W + 2, bot + 2)) { return c; } + // the border, two canvas pixels in the accent + if (!in_rect(x, y, PANEL_X, PANEL_Y, PANEL_X + PANEL_W, bot)) { return acc; } + c = BG_CARD; + // the title bar: three dots and the lesson's number + if (y < PANEL_Y + BAR) { + c = mix(BG_CARD, acc, 70); + if (in_disc(x, y, PANEL_X + 12, PANEL_Y + 12, 4)) { c = INK_STR; } + if (in_disc(x, y, PANEL_X + 26, PANEL_Y + 12, 4)) { c = acc; } + if (in_disc(x, y, PANEL_X + 40, PANEL_Y + 12, 4)) { c = INK_NUM; } + if (num_on(@as(i32, @intCast(t)) + 1, x, y, PANEL_X + PANEL_W - 28, PANEL_Y + 7, 2)) { c = INK; } + return c; + } + // the code: line row, column col; glyph pixels are two canvas pixels square + const px0 = x - PANEL_X - 6; + const py0 = y - PANEL_Y - BAR - 4; + if (px0 < 0 || py0 < 0) { return c; } + const row = @divTrunc(py0, LINE_H); + const col = @divTrunc(px0, ADV); + if (row >= @as(i32, @intCast(code_lines(t))) || col >= COLS) { return c; } + const l = code_line0(t) + @as(usize, @intCast(row)); + const ox = PANEL_X + 6 + col * ADV; + const oy = PANEL_Y + BAR + 4 + row * LINE_H; + if (col < GUTTER - 1) { + if (num_on(row + 1, x, y, PANEL_X + 6, oy, 2)) { return INK_DIM; } + return c; + } + if (col == GUTTER - 1) { return c; } + const k = @as(usize, @intCast(col - GUTTER)); + if (k >= line_len(l)) { return c; } + if (char_on(@as(i32, code_byte(l, k)), x, y, ox, oy, 2)) { return ink(l, k, acc); } + return c; +} + +// ---- tests ---- + +test "the card is as tall as the lesson's code" { + var t : usize = 0; + while (t < 27) { + assert(card_bottom(t) == PANEL_Y + BAR + @as(i32, @intCast(code_lines(t))) * LINE_H + PAD); + assert(card_bottom(t) < 640); + t = t + 1; + } + assert(card_bottom(0) != card_bottom(14)); +} + +test "the scene reads the lesson's own code: lesson 0 and lesson 14 do not draw one picture" { + var diff : i32 = 0; + var inside : i32 = 0; + var y : i32 = PANEL_Y; + while (y < 560) { + var x : i32 = PANEL_X; + while (x < PANEL_X + PANEL_W) { + const a = code_px(0, 0xF5B700, 0, x, y); + const b = code_px(14, 0xF5B700, 0, x, y); + if (a != b) { diff = diff + 1; } + if (a == 0xF5B700) { inside = inside + 1; } + x = x + 1; + } + y = y + 1; + } + assert(diff > 4000); + assert(inside > 100); +} + +test "negative control: a card of the right height but another lesson's text is a different picture" { + // lessons that have the same line count still draw their own bytes + var found : bool = false; + var a : usize = 0; + while (a < 27 && !found) { + var b : usize = a + 1; + while (b < 27 && !found) { + if (code_lines(a) == code_lines(b)) { + var diff : i32 = 0; + var y : i32 = PANEL_Y + BAR; + while (y < card_bottom(a)) { + var x : i32 = PANEL_X; + while (x < PANEL_X + PANEL_W) { + if (code_px(a, 0xF5B700, 0, x, y) != code_px(b, 0xF5B700, 0, x, y)) { diff = diff + 1; } + x = x + 1; + } + y = y + 1; + } + assert(diff > 500); + found = true; + } + b = b + 1; + } + a = a + 1; + } + assert(found); +} + +test "every pixel is a colour and the scene draws inside the canvas" { + var x : i32 = 0; + while (x < 360) { + var y : i32 = 0; + while (y < 640) { + const c = code_px(7, 0x8B5CF6, 0, x, y); + assert(c >= 0 && c <= 0xFFFFFF); + y = y + 17; + } + x = x + 11; + } +} diff --git a/specs/t27f/scene_data.t27 b/specs/t27f/scene_data.t27 new file mode 100644 index 0000000000..8d1a315cd8 --- /dev/null +++ b/specs/t27f/scene_data.t27 @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_data.t27 -- scene A_DATA of reel_factory.t27: the lesson's code as a stack of glass +// blocks, one per line, as wide as the line is long, each stepping a little to the right of the one +// above it and casting a shadow, with the line's first word printed on its face. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: a struct or an enum is fields stacked in a record, and a field is a name and a width. The +// stack is the lesson's own: the number of blocks is its line count and the width of each its +// line's length, so a short record and a long one do not look alike. +// +// THE FUNCTION: data_px(t, acc, n, x, y) is the colour of canvas point (x, y). Blocks are 18 pixels +// tall every 22, from y = 88, at most 24; block r starts at x = 20 + 9 * (r mod 5). +// +// NOT CLAIMED: it does not read field types or layout; "glass" is a gradient and a highlight line. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_data; + +use t27f::scene_raster::fmod; +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::char_on; +use t27f::scene_raster::imin; +use t27f::scene_raster::clamp; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_lex::lex_n; +use t27f::scene_lex::lex_line; +use t27f::scene_lex::lex_llen; +use t27f::scene_lex::lex_ch; +use t27f::scene_lex::lex_first; + +pub const BY0 : i32 = 88; +pub const BH : i32 = 18; +pub const BPITCH : i32 = 22; +pub const MAXB : i32 = 24; + +pub fn block_n(t: usize) -> i32 { + return imin(lex_n(t), MAXB); +} +pub fn block_x(r: i32) -> i32 { + return 20 + 9 * fmod(r, 5); +} +pub fn blocks_end(t: usize) -> i32 { + return block_n(t) * BPITCH; +} +pub fn block_w(t: usize, r: i32) -> i32 { + return clamp(50 + lex_llen(lex_line(t, r)) * 5, 70, 300); +} + +pub fn data_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(0, acc, seed_of(t, n, 6), x, y); + if (y < BY0 || y - BY0 >= blocks_end(t)) { return c0; } + const r = @divTrunc(y - BY0, BPITCH); + if (r >= block_n(t)) { return c0; } + const oy = BY0 + r * BPITCH; + const x0 = block_x(r); + const w = block_w(t, r); + const l = lex_line(t, r); + if (in_rect(x, y, x0, oy, x0 + w, oy + BH)) { + // the first word on the face of the glass + const ox = x0 + 6; + const k = @divTrunc(x - ox, 8); + if (x >= ox && k < 12 && y >= oy + 4 && y < oy + 14) { + const ch = lex_ch(l, lex_first(l) + k); + if (ch > 0 && char_on(ch, x, y, ox + k * 8, oy + 4, 2)) { return rgb(245, 247, 255); } + } + if (y - oy < 2) { return mix(acc, rgb(255, 255, 255), 120); } + return mix(acc, rgb(18, 22, 60), @divTrunc((x - x0) * 200, w) + (y - oy) * 4); + } + if (in_rect(x, y, x0 + 4, oy + 4, x0 + w + 4, oy + BH + 4)) { return rgb(5, 6, 14); } + return c0; +} + +// ---- tests ---- + +test "a block per line, wider for a longer line" { + var t : usize = 0; + while (t < 27) { + assert(block_n(t) >= 1 && block_n(t) <= MAXB); + var r : i32 = 0; + while (r < block_n(t)) { + assert(block_w(t, r) >= 70 && block_w(t, r) <= 300); + assert(block_x(r) + block_w(t, r) < 360); + r = r + 1; + } + t = t + 1; + } + assert(block_x(0) == 20); + assert(block_x(7) == 38); +} + +test "the stack is the lesson's: another lesson draws another record" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = data_px(16, 0x22C55E, 4, x, y); + const b = data_px(17, 0x22C55E, 4, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "the glass is lit at its top edge and shaded to the right" { + const t : usize = 16; + const x0 = block_x(0); + const w = block_w(t, 0); + const top = data_px(t, 0x22C55E, 4, x0 + w - 3, BY0); + const body_l = data_px(t, 0x22C55E, 4, x0 + w - 3, BY0 + 16); + assert(top != body_l); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (data_px(18, 0xF5B700, 3, x, y) != data_px(18, 0xF5B700, 3, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_graph.t27 b/specs/t27f/scene_graph.t27 new file mode 100644 index 0000000000..40ca23837c --- /dev/null +++ b/specs/t27f/scene_graph.t27 @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_graph.t27 -- scene A_GRAPH of reel_factory.t27: the lesson's code as a diagram, one +// node per line of source, in the order the lines stand, joined by the flow from each to the next. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: the T27F idea in one picture -- a spec as nodes and edges. A declaration (`pub`, `fn`, `const`, +// `test`, `struct`, `enum`, `use`) is a node filled in the accent, any other line a dark node, and +// each node carries the first characters of its own line. A longer lesson draws more nodes. +// +// THE FUNCTION: graph_px(t, acc, n, x, y) is the colour of canvas point (x, y). Three nodes to a row, +// at most 33 nodes; a lesson with more lines draws its first 33. +// +// NOT CLAIMED: the edges are the order of the lines, not a call graph; reading calls out of the text +// is the T27F graph IR, which this does not do. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_graph; + +use t27f::scene_raster::fmod; +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::near_seg; +use t27f::scene_raster::char_on; +use t27f::scene_raster::imin; +use t27f::scene_raster::imax; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_lex::lex_n; +use t27f::scene_lex::lex_line; +use t27f::scene_lex::lex_llen; +use t27f::scene_lex::lex_ch; +use t27f::scene_lex::lex_first; +use t27f::scene_lex::lex_has; +use t27f::scene_lex::KW_FN; +use t27f::scene_lex::KW_PUB; +use t27f::scene_lex::KW_CONST; +use t27f::scene_lex::KW_TEST; +use t27f::scene_lex::KW_STRUCT; +use t27f::scene_lex::KW_ENUM; +use t27f::scene_lex::KW_USE; + +pub const NODE_W : i32 = 100; +pub const NODE_H : i32 = 22; +pub const GRAPH_COLS : i32 = 3; +pub const X0 : i32 = 14; +pub const DX : i32 = 116; +pub const Y0 : i32 = 92; +pub const DY : i32 = 42; +pub const MAXN : i32 = 33; +pub const GRAPH_INK : i32 = 0xE6EAF5; +pub const NODE_BG : i32 = 0x161B33; + +pub fn node_n(t: usize) -> i32 { + return imin(lex_n(t), MAXN); +} +pub fn node_cx(r: i32) -> i32 { + return X0 + fmod(r, GRAPH_COLS) * DX + @divTrunc(NODE_W, 2); +} +pub fn node_cy(r: i32) -> i32 { + return Y0 + @divTrunc(r, GRAPH_COLS) * DY + @divTrunc(NODE_H, 2); +} +// Whether line l declares something. +pub fn is_decl(l: usize) -> bool { + return lex_has(l, KW_FN) || lex_has(l, KW_PUB) || lex_has(l, KW_CONST) || lex_has(l, KW_TEST) || lex_has(l, KW_STRUCT) || lex_has(l, KW_ENUM) || lex_has(l, KW_USE); +} + +pub fn graph_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(1, acc, seed_of(t, n, 1), x, y); + const cnt = node_n(t); + const row = @divTrunc(y - Y0 + DY, DY) - 1; + // the node under the pixel, if any + if (y >= Y0 && x >= X0) { + const col = @divTrunc(x - X0, DX); + const r = row * GRAPH_COLS + col; + if (col < GRAPH_COLS && r >= 0 && r < cnt && in_rect(x, y, X0 + col * DX, Y0 + row * DY, X0 + col * DX + NODE_W, Y0 + row * DY + NODE_H)) { + const l = lex_line(t, r); + const ox = X0 + col * DX + 4; + const oy = Y0 + row * DY + 6; + var fill = NODE_BG; + if (is_decl(l)) { fill = mix(NODE_BG, acc, 200); } + if (!in_rect(x, y, X0 + col * DX + 1, Y0 + row * DY + 1, X0 + col * DX + NODE_W - 1, Y0 + row * DY + NODE_H - 1)) { return acc; } + const k = @divTrunc(x - ox, 8); + if (x >= ox && y >= oy && k < 11) { + const ch = lex_ch(l, lex_first(l) + k); + if (ch > 0 && char_on(ch, x, y, ox + k * 8, oy, 2)) { + if (is_decl(l)) { return rgb(10, 12, 24); } + return GRAPH_INK; + } + } + return fill; + } + } + // the edges: line r to line r + 1, only those that can pass this row + var r0 = imax(0, (row - 1) * GRAPH_COLS); + const r1 = imin(cnt - 2, row * GRAPH_COLS + GRAPH_COLS); + while (r0 <= r1) { + if (near_seg(x, y, node_cx(r0), node_cy(r0), node_cx(r0 + 1), node_cy(r0 + 1), 1)) { + return acc; + } + r0 = r0 + 1; + } + return c0; +} + +// ---- tests ---- + +test "one node per line, three to a row" { + var t : usize = 0; + while (t < 27) { + assert(node_n(t) >= 1 && node_n(t) <= MAXN); + t = t + 1; + } + assert(node_cx(0) == X0 + 50); + assert(node_cx(4) == X0 + DX + 50); + assert(node_cy(3) == Y0 + DY + 11); +} + +test "the nodes are the lesson's lines: another lesson draws another diagram" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = graph_px(2, 0xF5B700, 3, x, y); + const b = graph_px(11, 0xF5B700, 3, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "a declaration node is filled in the accent and a plain one is not" { + // count accent-tinted node pixels over a lesson that has declarations + var decl : i32 = 0; + var r : i32 = 0; + const t : usize = 12; + while (r < node_n(t)) { + if (is_decl(lex_line(t, r))) { decl = decl + 1; } + r = r + 1; + } + assert(decl >= 1); + assert(decl <= node_n(t)); + // the node's border pixel is the accent whatever the line is + assert(graph_px(t, 0x22C55E, 1, X0, Y0) == 0x22C55E); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (graph_px(5, 0x3B82F6, 2, x, y) != graph_px(5, 0x3B82F6, 2, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_guard.t27 b/specs/t27f/scene_guard.t27 new file mode 100644 index 0000000000..02b8ffda6f --- /dev/null +++ b/specs/t27f/scene_guard.t27 @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_guard.t27 -- scene A_GUARD of reel_factory.t27: a dial of 24 ticks round a big +// number. The number is the lesson's count of asserts and invariants; the ticks lit in the accent +// are three for each of them, up to 23, the rest stay dim. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: an assert is a guard, and a guard is a thing that is either holding or not. A dial with more +// lit ticks is a lesson with more guards, so a lesson that teaches three checks and one that +// teaches none do not draw the same dial. +// +// THE FUNCTION: guard_px(t, acc, n, x, y) is the colour of canvas point (x, y). Centre (180, 320), +// ticks on a circle of radius 138, the number in the middle at scale 10. +// +// NOT CLAIMED: the dial does not run the asserts; it counts them. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_guard; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_disc; +use t27f::scene_raster::in_ring; +use t27f::scene_raster::num_on; +use t27f::scene_raster::clamp; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_bg::cos24; +use t27f::scene_bg::sin24; + +pub const GX : i32 = 180; +pub const GY : i32 = 320; +pub const DIAL : i32 = 138; +pub const TICKS : i32 = 24; +pub const DIM_TICK : i32 = 0x39426B; + +// How many of the 24 ticks are lit for a lesson that counts n guards. +pub fn lit_ticks(n: i32) -> i32 { + return clamp(n * 3, 3, 23); +} +pub fn digits_of(v: i32) -> i32 { + var d : i32 = 1; + var w = v; + while (w >= 10) { + w = @divTrunc(w, 10); + d = d + 1; + } + return d; +} +pub fn tick_x(k: i32) -> i32 { + return GX + @divTrunc(DIAL * cos24(k), 1000); +} +pub fn tick_y(k: i32) -> i32 { + return GY + @divTrunc(DIAL * sin24(k), 1000); +} + +pub fn guard_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(2, acc, seed_of(t, n, 8), x, y); + // the number in the middle: white, on a dark plate + const dg = digits_of(n); + const ox = GX + 5 - dg * 20; + if (num_on(n, x, y, ox, GY - 25, 10)) { return rgb(245, 245, 245); } + if (in_disc(x, y, GX, GY, 96)) { + if (in_ring(x, y, GX, GY, 94, 96)) { return acc; } + return mix(rgb(8, 10, 22), acc, 18); + } + if (!in_ring(x, y, GX, GY, 126, 152)) { return c0; } + const lit = lit_ticks(n); + var k : i32 = 0; + while (k < TICKS) { + if (in_disc(x, y, tick_x(k), tick_y(k), 9)) { + if (k < lit) { return acc; } + return DIM_TICK; + } + k = k + 1; + } + return c0; +} + +// ---- tests ---- + +test "three ticks a guard, between 3 and 23" { + assert(lit_ticks(0) == 3); + assert(lit_ticks(4) == 12); + assert(lit_ticks(30) == 23); + assert(digits_of(0) == 1); + assert(digits_of(9) == 1); + assert(digits_of(10) == 2); + assert(digits_of(123) == 3); +} + +test "the ticks stand on the canvas" { + var k : i32 = 0; + while (k < TICKS) { + assert(tick_x(k) > 10 && tick_x(k) < 350); + assert(tick_y(k) > 150 && tick_y(k) < 490); + k = k + 1; + } +} + +test "more guards light more of the dial" { + var few : i32 = 0; + var many : i32 = 0; + var y : i32 = 160; + while (y < 480) { + var x : i32 = 30; + while (x < 330) { + if (guard_px(13, 0x3B82F6, 2, x, y) == 0x3B82F6) { few = few + 1; } + if (guard_px(13, 0x3B82F6, 7, x, y) == 0x3B82F6) { many = many + 1; } + x = x + 1; + } + y = y + 1; + } + assert(many > few); + assert(few > 0); +} + +test "the number is on the plate and another lesson draws another dial" { + var white : i32 = 0; + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = guard_px(8, 0xF5B700, 5, x, y); + const b = guard_px(21, 0xF5B700, 5, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a == 0xF5F5F5) { white = white + 1; } + if (a != b) { diff = diff + 1; } + x = x + 2; + } + y = y + 2; + } + assert(white > 50); + assert(diff > 5000); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (guard_px(3, 0x22C55E, 3, x, y) != guard_px(3, 0x22C55E, 3, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_lex.t27 b/specs/t27f/scene_lex.t27 new file mode 100644 index 0000000000..ddc25526c5 --- /dev/null +++ b/specs/t27f/scene_lex.t27 @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_lex.t27 -- what the scene renderers read out of a lesson's code and text: its +// lines, its bytes, the keywords on each line. Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: reel_topics.t27 gives the code as usize-indexed bytes and reel_factory.t27 finds a keyword +// at a word boundary; the renderers want i32 numbers (scene_raster's coordinates are i32) and the +// same few questions -- how many lines, which line has an `assert`, what is byte i of the lesson -- +// asked from twelve places. They are asked here once, in i32, so a renderer never casts a line +// index and never counts a keyword a second way. +// +// KEYWORDS: k is reel_factory's keyword index: 0 test, 1 fn, 2 struct, 3 enum, 4 trit, 5 while, +// 6 for, 7 if, 8 switch, 9 use, 10 invariant, 11 bench, 12 assert, 13 const, 14 var, 15 let, +// 16 pub. A line "has" k when reel_factory.word_at finds k at a word boundary anywhere on it. +// +// NOT CLAIMED: a keyword inside a string or a comment counts (the lesson code has its comments +// dropped; a string with `if` in it would count). The hash is for scattering, not for identity. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_lex; + +use t27f::scene_raster::fmod; +use t27f::reel_topics::code_line0; +use t27f::reel_topics::code_lines; +use t27f::reel_topics::line_len; +use t27f::reel_topics::code_byte; +use t27f::reel_factory::word_at; + +pub const KW_TEST : usize = 0; +pub const KW_FN : usize = 1; +pub const KW_STRUCT : usize = 2; +pub const KW_ENUM : usize = 3; +pub const KW_TRIT : usize = 4; +pub const KW_WHILE : usize = 5; +pub const KW_FOR : usize = 6; +pub const KW_IF : usize = 7; +pub const KW_SWITCH : usize = 8; +pub const KW_USE : usize = 9; +pub const KW_INVARIANT : usize = 10; +pub const KW_BENCH : usize = 11; +pub const KW_ASSERT : usize = 12; +pub const KW_CONST : usize = 13; +pub const KW_VAR : usize = 14; +pub const KW_LET : usize = 15; +pub const KW_PUB : usize = 16; + +// How many lines lesson t has, and the absolute index of its row r. +pub fn lex_n(t: usize) -> i32 { + return @as(i32, @intCast(code_lines(t))); +} +pub fn lex_line(t: usize, r: i32) -> usize { + return code_line0(t) + @as(usize, @intCast(r)); +} +// The length of line l, and its byte at column k (0 past the end). +pub fn lex_llen(l: usize) -> i32 { + return @as(i32, @intCast(line_len(l))); +} +pub fn lex_ch(l: usize, k: i32) -> i32 { + if (k < 0 || k >= lex_llen(l)) { return 0; } + return @as(i32, code_byte(l, @as(usize, @intCast(k)))); +} +pub fn lex_letter(c: i32) -> bool { + return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c == 95; +} +pub fn lex_digit(c: i32) -> bool { + return c >= 48 && c <= 57; +} + +// Column of the first character on line l that is not a space, or its length. +pub fn lex_first(l: usize) -> i32 { + var k : i32 = 0; + while (k < lex_llen(l) && lex_ch(l, k) == 32) { k = k + 1; } + return k; +} +// Column just past the word (letters, digits, underscore) that starts at column k. +pub fn lex_word_end(l: usize, k: i32) -> i32 { + var e : i32 = k; + while (e < lex_llen(l) && (lex_letter(lex_ch(l, e)) || lex_digit(lex_ch(l, e)))) { e = e + 1; } + return e; +} + +// How many times keyword k stands on line l. +pub fn lex_kw_count(l: usize, k: usize) -> i32 { + var c : i32 = 0; + var p : usize = 0; + while (p < line_len(l)) { + if (word_at(l, p, k)) { c = c + 1; } + p = p + 1; + } + return c; +} +pub fn lex_has(l: usize, k: usize) -> bool { + return lex_kw_count(l, k) > 0; +} +// How many of lesson t's lines have keyword k. +pub fn lex_lines_with(t: usize, k: usize) -> i32 { + var c : i32 = 0; + var r : i32 = 0; + while (r < lex_n(t)) { + if (lex_has(lex_line(t, r), k)) { c = c + 1; } + r = r + 1; + } + return c; +} +// How many times keyword k stands in all of lesson t. +pub fn lex_kw_total(t: usize, k: usize) -> i32 { + var c : i32 = 0; + var r : i32 = 0; + while (r < lex_n(t)) { + c = c + lex_kw_count(lex_line(t, r), k); + r = r + 1; + } + return c; +} + +// The bytes of lesson t's code, all lines end to end. +pub fn lex_total(t: usize) -> i32 { + var s : i32 = 0; + var r : i32 = 0; + while (r < lex_n(t)) { + s = s + lex_llen(lex_line(t, r)); + r = r + 1; + } + return s; +} +// Byte i of lesson t's code, i counted from 0 and wrapped past the end; 0 for a lesson without code. +pub fn lex_byte(t: usize, i: i32) -> i32 { + const total = lex_total(t); + if (total == 0) { return 0; } + var j = fmod(i, total); + var r : i32 = 0; + while (r < lex_n(t)) { + const n = lex_llen(lex_line(t, r)); + if (j < n) { return lex_ch(lex_line(t, r), j); } + j = j - n; + r = r + 1; + } + return 0; +} +// A number from lesson t's code, for scattering: it changes when any byte changes. +pub fn lex_hash(t: usize) -> i32 { + var h : i32 = 5381; + var r : i32 = 0; + while (r < lex_n(t)) { + const l = lex_line(t, r); + var k : i32 = 0; + while (k < lex_llen(l)) { + h = fmod(h * 33 + lex_ch(l, k), 1000003); + k = k + 1; + } + r = r + 1; + } + return h; +} + +// ---- tests ---- + +test "lines, bytes and totals agree with reel_topics" { + var t : usize = 0; + while (t < 27) { + assert(lex_n(t) == @as(i32, @intCast(code_lines(t)))); + var sum : i32 = 0; + var r : i32 = 0; + while (r < lex_n(t)) { + sum = sum + lex_llen(lex_line(t, r)); + r = r + 1; + } + assert(lex_total(t) == sum); + assert(sum > 0); + t = t + 1; + } + assert(lex_ch(lex_line(0, 0), 0 - 1) == 0); + assert(lex_ch(lex_line(0, 0), 4000) == 0); +} + +test "the keyword counts find the construct each lesson teaches" { + // lesson 9 is "A test block", 13 is "Choices: if, else and switch", 14 is "Loops: while and for", + // 16 is "Structs", 17 is "Enums", 18 is "Using other modules", 19 is "Invariants and benches" + assert(lex_lines_with(9, KW_TEST) >= 1); + assert(lex_kw_total(9, KW_ASSERT) >= 1); + assert(lex_kw_total(13, KW_IF) + lex_kw_total(13, KW_SWITCH) >= 1); + assert(lex_kw_total(14, KW_WHILE) + lex_kw_total(14, KW_FOR) >= 1); + assert(lex_kw_total(16, KW_STRUCT) >= 1); + assert(lex_kw_total(17, KW_ENUM) >= 1); + assert(lex_kw_total(18, KW_USE) >= 1); + assert(lex_kw_total(19, KW_INVARIANT) + lex_kw_total(19, KW_BENCH) >= 1); + // and a lesson that does not teach it has none: lesson 3 is "Constants" + assert(lex_kw_total(3, KW_WHILE) == 0); + assert(lex_kw_total(3, KW_STRUCT) == 0); +} + +test "byte i wraps and the hash tells lessons apart" { + var t : usize = 0; + while (t < 27) { + const n = lex_total(t); + assert(lex_byte(t, 0) == lex_byte(t, n)); + assert(lex_byte(t, 3) == lex_byte(t, 3 + 2 * n)); + assert(lex_byte(t, 0) > 0); + t = t + 1; + } + var distinct : i32 = 0; + t = 1; + while (t < 27) { + if (lex_hash(t) != lex_hash(t - 1)) { distinct = distinct + 1; } + t = t + 1; + } + assert(distinct == 26); +} diff --git a/specs/t27f/scene_links.t27 b/specs/t27f/scene_links.t27 new file mode 100644 index 0000000000..79890e02d3 --- /dev/null +++ b/specs/t27f/scene_links.t27 @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_links.t27 -- scene A_LINKS of reel_factory.t27: a hub and its neighbours. The +// lesson's module is the box in the middle; round it stand as many boxes as the lesson has `use` +// edges (3 to 9), each joined to the hub by an edge and to the next box by a faint chord, each +// labelled with three letters of the lesson's own code. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: `use` is how one module reaches another. The picture is the edge count of the lesson, laid +// out on a circle so a lesson with more edges has a fuller wheel. +// +// THE FUNCTION: links_px(t, acc, n, x, y) is the colour of canvas point (x, y). Hub at (180, 320), +// neighbours on a circle of radius 138, the first at the angle the lesson's first byte picks. +// +// NOT CLAIMED: the neighbours' names are letters of the lesson's code, not the modules it imports +// (the lesson code is a snippet, not a module header). +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_links; + +use t27f::scene_raster::fmod; +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::near_seg; +use t27f::scene_raster::char_on; +use t27f::scene_raster::clamp; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_bg::cos24; +use t27f::scene_bg::sin24; +use t27f::scene_lex::lex_byte; + +pub const HX : i32 = 180; +pub const HY : i32 = 320; +pub const WHEEL : i32 = 138; +pub const HALF_W : i32 = 27; +pub const HALF_H : i32 = 12; +pub const CHORD : i32 = 0x2C3354; + +pub fn sats(n: i32) -> i32 { + return clamp(n, 3, 9); +} +// The slot of the circle, of 24, that neighbour i of s stands at. +pub fn slot(t: usize, i: i32, s: i32) -> i32 { + return fmod(lex_byte(t, 0), 24) + @divTrunc(i * 24, s); +} +pub fn sat_x(t: usize, i: i32, s: i32) -> i32 { + return HX + @divTrunc(WHEEL * cos24(slot(t, i, s)), 1000); +} +pub fn sat_y(t: usize, i: i32, s: i32) -> i32 { + return HY + @divTrunc(WHEEL * 14 * sin24(slot(t, i, s)), 10000); +} + +pub fn links_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(1, acc, seed_of(t, n, 7), x, y); + const s = sats(n); + // the hub + if (in_rect(x, y, HX - 40, HY - 16, HX + 40, HY + 16)) { + if (!in_rect(x, y, HX - 38, HY - 14, HX + 38, HY + 14)) { return rgb(245, 245, 245); } + const k = @divTrunc(x - (HX - 30), 10); + if (k >= 0 && k < 6 && y >= HY - 6 && y < HY + 6) { + if (char_on(lex_byte(t, k + 2), x, y, HX - 30 + k * 10, HY - 6, 2)) { return acc; } + } + return rgb(14, 18, 40); + } + var i : i32 = 0; + var edge : i32 = 0; + while (i < s) { + const sx = sat_x(t, i, s); + const sy = sat_y(t, i, s); + if (in_rect(x, y, sx - HALF_W, sy - HALF_H, sx + HALF_W, sy + HALF_H)) { + if (!in_rect(x, y, sx - HALF_W + 2, sy - HALF_H + 2, sx + HALF_W - 2, sy + HALF_H - 2)) { return acc; } + const k = @divTrunc(x - (sx - 21), 14); + if (k >= 0 && k < 3 && y >= sy - 5 && y < sy + 5) { + if (char_on(lex_byte(t, i * 3 + k + 7), x, y, sx - 21 + k * 14, sy - 5, 2)) { return rgb(245, 247, 255); } + } + return mix(rgb(14, 18, 40), acc, 60); + } + if (near_seg(x, y, HX, HY, sx, sy, 1)) { edge = 1; } + if (near_seg(x, y, sx, sy, sat_x(t, i + 1, s), sat_y(t, i + 1, s), 0)) { + if (edge == 0) { edge = 2; } + } + i = i + 1; + } + if (edge == 1) { return acc; } + if (edge == 2) { return CHORD; } + return c0; +} + +// ---- tests ---- + +test "three to nine neighbours, all on the canvas" { + assert(sats(0) == 3); + assert(sats(5) == 5); + assert(sats(30) == 9); + var t : usize = 0; + while (t < 27) { + var i : i32 = 0; + while (i < 9) { + assert(sat_x(t, i, 9) > 20 && sat_x(t, i, 9) < 340); + assert(sat_y(t, i, 9) > 100 && sat_y(t, i, 9) < 560); + i = i + 1; + } + t = t + 1; + } +} + +test "the wheel is the lesson's: another lesson draws another wheel" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = links_px(18, 0x3B82F6, 4, x, y); + const b = links_px(24, 0x3B82F6, 4, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "more edges draw more boxes" { + // count box-border pixels (the accent) along the wheel for 3 and 9 neighbours + var few : i32 = 0; + var many : i32 = 0; + var y : i32 = 100; + while (y < 560) { + var x : i32 = 20; + while (x < 340) { + if (links_px(18, 0x3B82F6, 3, x, y) == 0x3B82F6) { few = few + 1; } + if (links_px(18, 0x3B82F6, 9, x, y) == 0x3B82F6) { many = many + 1; } + x = x + 2; + } + y = y + 2; + } + assert(many > few); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (links_px(10, 0xF5B700, 5, x, y) != links_px(10, 0xF5B700, 5, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_loop.t27 b/specs/t27f/scene_loop.t27 new file mode 100644 index 0000000000..6b4ff2607d --- /dev/null +++ b/specs/t27f/scene_loop.t27 @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_loop.t27 -- scene A_LOOP of reel_factory.t27: concentric orbits, one ring per +// loop the lesson counts, with 12 slots on every ring; a slot holds a dot when the lesson's next +// byte is not a multiple of three, in the accent when it is one more than a multiple. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: a loop is something that goes round. Three plus the lesson's content count rings (at most 8), +// and the count itself in the centre, so a lesson with more loops draws a fuller orbit. +// +// THE FUNCTION: loop_px(t, acc, n, x, y) is the colour of canvas point (x, y). The centre is +// (180, 320); the rings stand 16 to 19 pixels apart out to a radius of 160. +// +// NOT CLAIMED: the dots are placed from the lesson's bytes, they do not run the loop. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_loop; + +use t27f::scene_raster::fmod; +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_disc; +use t27f::scene_raster::in_ring; +use t27f::scene_raster::num_on; +use t27f::scene_raster::imin; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_bg::cos24; +use t27f::scene_bg::sin24; +use t27f::scene_lex::lex_byte; + +pub const LCX : i32 = 180; +pub const LCY : i32 = 320; +pub const SLOTS : i32 = 12; +pub const RING_DIM : i32 = 0x2C3354; + +pub fn ring_count(n: i32) -> i32 { + return 3 + imin(n, 5); +} +pub fn ring_radius(j: i32, rings: i32) -> i32 { + return 40 + @divTrunc(j * 120, rings); +} +// 0 no dot, 1 a dot, 2 a dot in the accent: the slot m of ring j of lesson t. +pub fn slot_of(t: usize, j: i32, m: i32) -> i32 { + return fmod(lex_byte(t, j * SLOTS + m), 3); +} + +pub fn loop_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(4, acc, seed_of(t, n, 4), x, y); + if (in_disc(x, y, LCX, LCY, 26)) { + if (in_disc(x, y, LCX, LCY, 24)) { + if (num_on(n, x, y, LCX - 6, LCY - 10, 4)) { return rgb(245, 245, 245); } + return mix(rgb(10, 12, 24), acc, 90); + } + return acc; + } + if (!in_disc(x, y, LCX, LCY, 172)) { return c0; } + const rings = ring_count(n); + var j : i32 = 0; + while (j < rings) { + const rj = ring_radius(j, rings); + if (in_ring(x, y, LCX, LCY, rj - 8, rj + 8)) { + var m : i32 = 0; + while (m < SLOTS) { + const k = m * 2 + fmod(j, 2); + const dx = LCX + @divTrunc(rj * cos24(k), 1000); + const dy = LCY + @divTrunc(rj * sin24(k), 1000); + const s = slot_of(t, j, m); + if (s > 0 && in_disc(x, y, dx, dy, 6)) { + if (s == 2) { return acc; } + return rgb(235, 238, 250); + } + m = m + 1; + } + if (in_ring(x, y, LCX, LCY, rj, rj + 1)) { return RING_DIM; } + } + j = j + 1; + } + return c0; +} + +// ---- tests ---- + +test "the rings count the lesson's loops" { + assert(ring_count(1) == 4); + assert(ring_count(5) == 8); + assert(ring_count(40) == 8); + var j : i32 = 0; + while (j < 8) { + assert(ring_radius(j, 8) >= 40 && ring_radius(j, 8) < 160); + j = j + 1; + } + assert(ring_radius(0, 4) == 40); +} + +test "the dots are the lesson's bytes: another lesson draws another orbit" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = loop_px(14, 0x8B5CF6, 3, x, y); + const b = loop_px(15, 0x8B5CF6, 3, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "a lesson with more loops draws more rings" { + var few : i32 = 0; + var many : i32 = 0; + var x : i32 = LCX + 30; + while (x < LCX + 175) { + if (loop_px(14, 0x8B5CF6, 1, x, LCY) != bg_px(4, 0x8B5CF6, seed_of(14, 1, 4), x, LCY)) { few = few + 1; } + if (loop_px(14, 0x8B5CF6, 5, x, LCY) != bg_px(4, 0x8B5CF6, seed_of(14, 5, 4), x, LCY)) { many = many + 1; } + x = x + 1; + } + assert(many > 0); + assert(few > 0); + assert(ring_count(5) > ring_count(1)); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (loop_px(6, 0x3B82F6, 4, x, y) != loop_px(6, 0x3B82F6, 4, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_map.t27 b/specs/t27f/scene_map.t27 new file mode 100644 index 0000000000..5ebb313984 --- /dev/null +++ b/specs/t27f/scene_map.t27 @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_map.t27 -- scene A_MAP of reel_factory.t27: the course map. Nine modules of +// three lessons make 27 cells; the lesson's own cell is lit solid in the accent with its number, +// the cells before it are done (a quarter of the accent), the cells after it are dark and each a +// shade of its own. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: a map says where you are. Every lesson sits at a different cell and has a different amount +// of the course behind it, so the 27 maps differ in where the light is and how much of the grid is +// tinted. +// +// THE FUNCTION: map_px(t, acc, n, x, y) is the colour of canvas point (x, y). Cell (row, col) +// is 96 x 48 at x = 26 + 104 * col, y = 60 + 62 * row; its number is row * 3 + col + 1. +// +// NOT CLAIMED: the map is the course's shape, 9 x 3; it knows no module titles. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_map; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::num_on; +use t27f::scene_raster::rnd; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; + +pub const MX0 : i32 = 26; +pub const MY0 : i32 = 60; +pub const MPX : i32 = 104; +pub const MPY : i32 = 62; +pub const MW : i32 = 96; +pub const MH : i32 = 48; +pub const MROWS : i32 = 9; +pub const MCOLS : i32 = 3; + +// The cell, 0..26, that lesson t sits in. +pub fn cell_of(t: usize) -> i32 { + return @as(i32, @intCast(t)); +} +// The shade of an unlit cell: a dark grey that depends on the lesson and on the cell. +pub fn shade(t: usize, c: i32) -> i32 { + return 18 + rnd(@as(i32, @intCast(t)) * 211 + c * 17 + 3, 10) * 5; +} + +pub fn map_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(5, acc, seed_of(t, n, 11), x, y); + if (x < MX0 || y < MY0) { return c0; } + const col = @divTrunc(x - MX0, MPX); + const row = @divTrunc(y - MY0, MPY); + if (col >= MCOLS || row >= MROWS) { return c0; } + const cx0 = MX0 + col * MPX; + const cy0 = MY0 + row * MPY; + if (!in_rect(x, y, cx0, cy0, cx0 + MW, cy0 + MH)) { return c0; } + const c = row * MCOLS + col; + const mine = cell_of(t); + if (num_on(c + 1, x, y, cx0 + 8, cy0 + 8, 4)) { + if (c == mine) { return rgb(10, 12, 24); } + return rgb(200, 206, 226); + } + if (c == mine) { return acc; } + if (c < mine) { return mix(rgb(10, 12, 24), acc, 70); } + const g = shade(t, c); + return rgb(g, g + 2, g + 10); +} + +// ---- tests ---- + +test "the lesson's cell is its place on the 9 x 3 course map" { + assert(cell_of(0) == 0); + assert(cell_of(1) == 1); + assert(cell_of(3) == 3); + assert(cell_of(26) == 26); + var t : usize = 0; + while (t < 27) { + assert(cell_of(t) == @as(i32, @intCast(t))); + t = t + 1; + } +} + +test "the cell is lit, the cells before it are tinted, the cells after are dark" { + const acc = 0x3B82F6; + // lesson 13 is cell 13: row 4, column 1. Probe a plain corner of three cells. + const lit = map_px(13, acc, 3, MX0 + 1 * MPX + 90, MY0 + 4 * MPY + 40); + const done = map_px(13, acc, 3, MX0 + 0 * MPX + 90, MY0 + 4 * MPY + 40); + const todo = map_px(13, acc, 3, MX0 + 2 * MPX + 90, MY0 + 4 * MPY + 40); + assert(lit == acc); + assert(done != acc); + assert(todo != acc); + assert(done != todo); +} + +test "another lesson lights another cell" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = map_px(5, 0xF5B700, 2, x, y); + const b = map_px(22, 0xF5B700, 2, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 2; + } + y = y + 2; + } + assert(diff > 20000); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (map_px(20, 0x8B5CF6, 4, x, y) != map_px(20, 0x8B5CF6, 4, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_raster.t27 b/specs/t27f/scene_raster.t27 new file mode 100644 index 0000000000..21cb2c2e63 --- /dev/null +++ b/specs/t27f/scene_raster.t27 @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_raster.t27 -- the shared raster of the twelve scene renderers: colours, shapes, a +// 3x5 font and a seeded hash, all in integers. Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: reel_factory.t27 plans one of twelve scenes per lesson but draws none. A scene is a pure +// function from (lesson, pixel) to a colour, so that the same lesson always gives the same picture +// and the stills can be compared. Every scene needs the same few things -- a colour, a disc, a +// segment, a letter -- and the recorded defect of this codebase family is the hand-copied helper, so +// they live here once and the scenes `use` them. +// +// THE CANVAS: scenes draw on a virtual canvas of VW x VH = 360 x 640, the tall cut at one third of +// 1080 x 1920. scene_still.t27 maps a still of any size onto it, so a 108 x 192 gate image and a +// 1080 x 1920 cover are the same picture. Colours are 0xRRGGBB held in an i32 (it fits: < 2^24); +// every coordinate is an i32 too, so no scene needs a cast between its own numbers. +// +// THE FONT: 3 x 5 pixels a letter, one row a 3-bit number (bit 2 is the left pixel), ASCII 32..95 +// plus { | } ~; a lowercase letter draws as its capital. It is small on purpose: a scene sets it at +// scale 2..8 and the blocks are the look. +// +// NOT CLAIMED: edges are hard (a point either is or is not inside a shape), so a still at 1080 x 1920 +// shows 3 x 3 pixel steps on a diagonal. The hash is a linear congruential generator, enough to +// scatter positions and not a source of randomness. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_raster; + +pub const VW : i32 = 360; +pub const VH : i32 = 640; + +// ---- integer helpers ---- + +// a modulo m for m > 0, always in 0..m-1 (the floor modulo; spelled out so t27b can compile it). +pub fn fmod(a: i32, m: i32) -> i32 { + const r = a - @divTrunc(a, m) * m; + if (r < 0) { return r + m; } + return r; +} + +// ---- colour ---- + +pub fn rgb(r: i32, g: i32, b: i32) -> i32 { + return r * 65536 + g * 256 + b; +} +pub fn red(c: i32) -> i32 { + return @divTrunc(c, 65536); +} +pub fn green(c: i32) -> i32 { + return fmod(@divTrunc(c, 256), 256); +} +pub fn blue(c: i32) -> i32 { + return fmod(c, 256); +} +pub fn iabs(v: i32) -> i32 { + if (v < 0) { return 0 - v; } + return v; +} +pub fn imin(a: i32, b: i32) -> i32 { + if (a < b) { return a; } + return b; +} +pub fn imax(a: i32, b: i32) -> i32 { + if (a > b) { return a; } + return b; +} +pub fn clamp(v: i32, lo: i32, hi: i32) -> i32 { + if (v < lo) { return lo; } + if (v > hi) { return hi; } + return v; +} +// Rec. 601 luma, 0..255, in integers. +pub fn luma(c: i32) -> i32 { + return @divTrunc(red(c) * 77 + green(c) * 150 + blue(c) * 29, 256); +} +// c0 at a = 0, c1 at a = 255, linear between. +pub fn mix(c0: i32, c1: i32, a: i32) -> i32 { + const k = clamp(a, 0, 255); + const r = @divTrunc(red(c0) * (255 - k) + red(c1) * k, 255); + const g = @divTrunc(green(c0) * (255 - k) + green(c1) * k, 255); + const b = @divTrunc(blue(c0) * (255 - k) + blue(c1) * k, 255); + return rgb(r, g, b); +} + +// ---- hash ---- + +// The next state of a linear congruential generator, 31 bits. +pub fn lcg(s: i32) -> i32 { + const v : i64 = @as(i64, s) * 1103515245 + 12345; + var m : i64 = v - @divTrunc(v, 2147483648) * 2147483648; + if (m < 0) { m = m + 2147483648; } + return @as(i32, @intCast(m)); +} +// A number in 0..n-1 from seed s: two steps, then the high bits (the low bits of an LCG cycle). +pub fn rnd(s: i32, n: i32) -> i32 { + const h = lcg(lcg(s + 7919)); + return fmod(@divTrunc(h, 4096), n); +} + +// ---- shapes ---- + +pub fn in_rect(x: i32, y: i32, x0: i32, y0: i32, x1: i32, y1: i32) -> bool { + return x >= x0 && x < x1 && y >= y0 && y < y1; +} +pub fn in_disc(x: i32, y: i32, cx: i32, cy: i32, r: i32) -> bool { + const dx : i64 = @as(i64, x - cx); + const dy : i64 = @as(i64, y - cy); + return dx * dx + dy * dy <= @as(i64, r) * @as(i64, r); +} +pub fn in_ring(x: i32, y: i32, cx: i32, cy: i32, r0: i32, r1: i32) -> bool { + const dx : i64 = @as(i64, x - cx); + const dy : i64 = @as(i64, y - cy); + const d2 = dx * dx + dy * dy; + return d2 >= @as(i64, r0) * @as(i64, r0) && d2 <= @as(i64, r1) * @as(i64, r1); +} +// Whether (x, y) is within hw of the segment from (x0, y0) to (x1, y1). +pub fn near_seg(x: i32, y: i32, x0: i32, y0: i32, x1: i32, y1: i32, hw: i32) -> bool { + const ex : i64 = @as(i64, x1 - x0); + const ey : i64 = @as(i64, y1 - y0); + const px : i64 = @as(i64, x - x0); + const py : i64 = @as(i64, y - y0); + const len2 = ex * ex + ey * ey; + const lim : i64 = @as(i64, hw) * @as(i64, hw); + if (len2 == 0) { return px * px + py * py <= lim; } + const dot = px * ex + py * ey; + if (dot <= 0) { return px * px + py * py <= lim; } + if (dot >= len2) { + const qx : i64 = @as(i64, x - x1); + const qy : i64 = @as(i64, y - y1); + return qx * qx + qy * qy <= lim; + } + const cross = px * ey - py * ex; + return cross * cross <= lim * len2; +} +// A triangle wave: 0 at phase 0, rising to amp - 1 at half a period, back to 0. +pub fn tri_wave(v: i32, period: i32, amp: i32) -> i32 { + const m = fmod(v, period); + const half = @divTrunc(period, 2); + if (m < half) { return @divTrunc(m * amp, half); } + return @divTrunc((period - m) * amp, half); +} + +// ---- the 3 x 5 font ---- + +// 68 glyphs of five rows: ASCII 32..95, then { | } ~. Row bits: 4 = left, 2 = middle, 1 = right. +pub const FONT : [340]u8 = [ + 0, 0, 0, 0, 0, // space + 2, 2, 2, 0, 2, // ! + 5, 5, 0, 0, 0, // " + 5, 7, 5, 7, 5, // # + 3, 6, 2, 3, 6, // $ + 5, 1, 2, 4, 5, // % + 2, 5, 2, 5, 3, // & + 2, 2, 0, 0, 0, // ' + 1, 2, 2, 2, 1, // ( + 4, 2, 2, 2, 4, // ) + 5, 2, 7, 2, 5, // * + 0, 2, 7, 2, 0, // + + 0, 0, 0, 2, 4, // , + 0, 0, 7, 0, 0, // - + 0, 0, 0, 0, 2, // . + 1, 1, 2, 4, 4, // / + 7, 5, 5, 5, 7, // 0 + 2, 6, 2, 2, 7, // 1 + 7, 1, 7, 4, 7, // 2 + 7, 1, 7, 1, 7, // 3 + 5, 5, 7, 1, 1, // 4 + 7, 4, 7, 1, 7, // 5 + 7, 4, 7, 5, 7, // 6 + 7, 1, 1, 2, 2, // 7 + 7, 5, 7, 5, 7, // 8 + 7, 5, 7, 1, 7, // 9 + 0, 2, 0, 2, 0, // : + 0, 2, 0, 2, 4, // ; + 1, 2, 4, 2, 1, // < + 0, 7, 0, 7, 0, // = + 4, 2, 1, 2, 4, // > + 7, 1, 3, 0, 2, // ? + 7, 5, 7, 4, 7, // @ + 2, 5, 7, 5, 5, // A + 6, 5, 6, 5, 6, // B + 3, 4, 4, 4, 3, // C + 6, 5, 5, 5, 6, // D + 7, 4, 6, 4, 7, // E + 7, 4, 6, 4, 4, // F + 3, 4, 5, 5, 3, // G + 5, 5, 7, 5, 5, // H + 7, 2, 2, 2, 7, // I + 1, 1, 1, 5, 2, // J + 5, 5, 6, 5, 5, // K + 4, 4, 4, 4, 7, // L + 5, 7, 7, 5, 5, // M + 6, 5, 5, 5, 5, // N + 2, 5, 5, 5, 2, // O + 6, 5, 6, 4, 4, // P + 2, 5, 5, 7, 3, // Q + 6, 5, 6, 5, 5, // R + 3, 4, 2, 1, 6, // S + 7, 2, 2, 2, 2, // T + 5, 5, 5, 5, 7, // U + 5, 5, 5, 5, 2, // V + 5, 5, 7, 7, 5, // W + 5, 5, 2, 5, 5, // X + 5, 5, 2, 2, 2, // Y + 7, 1, 2, 4, 7, // Z + 3, 2, 2, 2, 3, // [ + 4, 4, 2, 1, 1, // backslash + 6, 2, 2, 2, 6, // ] + 2, 5, 0, 0, 0, // ^ + 0, 0, 0, 0, 7, // _ + 3, 2, 6, 2, 3, // { + 2, 2, 2, 2, 2, // | + 6, 2, 3, 2, 6, // } + 0, 3, 6, 0, 0, // ~ +]; +pub const GLYPHS : i32 = 68; + +// The glyph index of character c, or -1 for one the font does not draw. +pub fn glyph_of(c: i32) -> i32 { + if (c >= 32 && c <= 95) { return c - 32; } + if (c >= 97 && c <= 122) { return c - 64; } + if (c >= 123 && c <= 126) { return c - 59; } + return 0 - 1; +} +// Whether pixel (gx, gy), gx 0..2 and gy 0..4, of character c is set. +pub fn glyph_on(c: i32, gx: i32, gy: i32) -> bool { + const g = glyph_of(c); + if (g < 0) { return false; } + if (gx < 0 || gx > 2 || gy < 0 || gy > 4) { return false; } + const row = @as(i32, FONT[@as(usize, @intCast(g * 5 + gy))]); + var pw : i32 = 4; + if (gx == 1) { pw = 2; } + if (gx == 2) { pw = 1; } + return fmod(@divTrunc(row, pw), 2) == 1; +} +// Whether canvas point (x, y) is on character c drawn with its top left at (ox, oy), each font +// pixel s canvas pixels square. +pub fn char_on(c: i32, x: i32, y: i32, ox: i32, oy: i32, s: i32) -> bool { + if (x < ox || y < oy) { return false; } + return glyph_on(c, @divTrunc(x - ox, s), @divTrunc(y - oy, s)); +} +// The advance of one character at scale s: three pixels of glyph and one of gap. +pub fn advance(s: i32) -> i32 { + return 4 * s; +} +// Whether (x, y) is on the decimal number v (at most 5 digits) with its top left at (ox, oy). +pub fn num_on(v: i32, x: i32, y: i32, ox: i32, oy: i32, s: i32) -> bool { + if (y < oy || y >= oy + 5 * s || x < ox) { return false; } + var digits : i32 = 1; + var w = v; + while (w >= 10) { + w = @divTrunc(w, 10); + digits = digits + 1; + } + const k = @divTrunc(x - ox, advance(s)); + if (k >= digits) { return false; } + var p : i32 = digits - 1 - k; + var d = v; + while (p > 0) { + d = @divTrunc(d, 10); + p = p - 1; + } + return char_on(48 + fmod(d, 10), x, y, ox + k * advance(s), oy, s); +} + +// ---- tests ---- + +test "a colour packs and unpacks, and mixing keeps both ends" { + const c = rgb(18, 200, 255); + assert(red(c) == 18); + assert(green(c) == 200); + assert(blue(c) == 255); + assert(mix(c, 0, 0) == c); + assert(mix(c, 0, 255) == 0); + assert(luma(rgb(255, 255, 255)) >= 254); + assert(luma(0) == 0); + assert(luma(rgb(0, 255, 0)) > luma(rgb(255, 0, 0))); + assert(luma(rgb(255, 0, 0)) > luma(rgb(0, 0, 255))); +} + +test "the hash is deterministic, in range, and not one number" { + assert(rnd(5, 100) == rnd(5, 100)); + var seen : i32 = 0; + var last : i32 = 0 - 1; + var s : i32 = 0; + while (s < 64) { + const r = rnd(s, 50); + assert(r >= 0 && r < 50); + if (r != last) { seen = seen + 1; } + last = r; + s = s + 1; + } + assert(seen >= 40); +} + +test "shapes contain their centre and not a far point" { + assert(in_rect(5, 5, 0, 0, 10, 10)); + assert(!in_rect(10, 5, 0, 0, 10, 10)); + assert(in_disc(50, 50, 50, 50, 3)); + assert(!in_disc(60, 50, 50, 50, 3)); + assert(in_ring(53, 50, 50, 50, 2, 4)); + assert(!in_ring(50, 50, 50, 50, 2, 4)); + assert(near_seg(5, 6, 0, 0, 10, 10, 2)); + assert(!near_seg(5, 12, 0, 0, 10, 10, 2)); + assert(near_seg(0 - 1, 0, 0, 0, 10, 0, 2)); + assert(!near_seg(0 - 5, 0, 0, 0, 10, 0, 2)); + assert(tri_wave(0, 10, 100) == 0); + assert(tri_wave(5, 10, 100) == 100); + assert(tri_wave(10, 10, 100) == 0); +} + +test "the font draws a capital A and folds a lowercase letter onto it" { + var n : i32 = 0; + var gy : i32 = 0; + while (gy < 5) { + var gx : i32 = 0; + while (gx < 3) { + if (glyph_on(65, gx, gy)) { n = n + 1; } + assert(glyph_on(65, gx, gy) == glyph_on(97, gx, gy)); + gx = gx + 1; + } + gy = gy + 1; + } + assert(n == 10); + assert(!glyph_on(32, 1, 2)); + assert(!glyph_on(200, 1, 2)); + assert(glyph_on(124, 1, 0)); + assert(char_on(65, 22, 10, 20, 10, 2)); + assert(!char_on(65, 20, 10, 20, 10, 2)); + assert(!char_on(65, 19, 10, 20, 10, 2)); +} + +test "a number draws its digits left to right and nothing past them" { + // 407 at scale 2 from (10, 10): '4' covers x 10..15, '0' 18..23, '7' 26..31 + assert(num_on(407, 10, 10, 10, 10, 2)); + assert(num_on(407, 26, 10, 10, 10, 2)); + assert(!num_on(407, 34, 10, 10, 10, 2)); + assert(!num_on(407, 10, 22, 10, 10, 2)); + // a different number is a different picture + var diff : i32 = 0; + var px : i32 = 10; + while (px < 34) { + var py : i32 = 10; + while (py < 20) { + if (num_on(407, px, py, 10, 10, 2) != num_on(408, px, py, 10, 10, 2)) { diff = diff + 1; } + py = py + 1; + } + px = px + 1; + } + assert(diff > 0); +} + +test "the font table holds sixty-eight glyphs of three-bit rows" { + assert(GLYPHS * 5 == 340); + var i : usize = 0; + while (i < 340) { + assert(@as(i32, FONT[i]) <= 7); + i = i + 1; + } +} diff --git a/specs/t27f/scene_still.t27 b/specs/t27f/scene_still.t27 new file mode 100644 index 0000000000..932d74f2d9 --- /dev/null +++ b/specs/t27f/scene_still.t27 @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_still.t27 -- the still of a lesson: reel_factory.t27's plan in, one picture out. +// The plan names the scene, the accent, the camera motion and the layout; this module dresses the +// scene with the last three and calls the renderer the first one names. Issue +// gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: reel_still.t27 (PR #10548) takes the frame the reel shows at STILL_MS = 9000 ms, frame 270, +// and every lesson reel drew token_floor's physics world there. Here the frame is the lesson's own +// scene. The camera is fixed at that frame, so each motion is a constant move, not an animation: +// push the canvas is seen 8 % closer, about its centre +// orbit the canvas is turned about 5.7 degrees, about its centre +// rise the scene sits 36 canvas pixels higher +// sweep the scene sits 28 canvas pixels to the left +// and each layout moves the hero off the centre: +// centre nowhere top 70 pixels up side 50 pixels to the right +// +// THE CALL: pixel(t, a, ac, mo, ly, n, x, y, w, h) is the colour, 0xRRGGBB, of pixel (x, y) of a +// w x h still in the 9:16 tall cut. a, ac, mo and ly are reel_factory's arche, accent, motion and +// layout of lesson t, and n is the content count of the scene: reel_factory's content(t, a). They +// are arguments because arche plans the whole course and a still calls this once a pixel. +// +// NOT CLAIMED: the tall cut only (the wide cut and the other FMT_* sizes are not drawn here); the +// still is the scene, not the captions over it, which are the reel's drawtext overlays. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_still; + +use t27f::scene_raster::VW; +use t27f::scene_raster::VH; +use t27f::scene_raster::rgb; +use t27f::scene_code::code_px; +use t27f::scene_graph::graph_px; +use t27f::scene_tests::tests_px; +use t27f::scene_trit::trit_px; +use t27f::scene_loop::loop_px; +use t27f::scene_branch::branch_px; +use t27f::scene_data::data_px; +use t27f::scene_links::links_px; +use t27f::scene_guard::guard_px; +use t27f::scene_title::title_px; +use t27f::scene_task::task_px; +use t27f::scene_map::map_px; + +pub const M_PUSH : usize = 0; +pub const M_ORBIT : usize = 1; +pub const M_RISE : usize = 2; +pub const M_SWEEP : usize = 3; +pub const L_CENTRE : usize = 0; +pub const L_TOP : usize = 1; +pub const L_SIDE : usize = 2; + +pub const CX : i32 = 180; +pub const CY : i32 = 320; + +// The accent colours of reel_factory.ACCENT_RGB: gold, violet, magenta, green, blue, white. +pub const ACCENT : [6]i32 = [0xF5B700, 0x8B5CF6, 0xE0409A, 0x22C55E, 0x3B82F6, 0xF5F5F5]; + +// Where canvas point (x, y) lands in the scene's own frame, once the camera and the layout have +// moved it. +pub fn scene_x(mo: usize, ly: usize, x: i32, y: i32) -> i32 { + var sx : i32 = x; + if (mo == M_PUSH) { sx = CX + @divTrunc((x - CX) * 100, 108); } + if (mo == M_ORBIT) { sx = CX + @divTrunc((x - CX) * 995 + (y - CY) * 99, 1000); } + if (mo == M_SWEEP) { sx = x + 28; } + if (ly == L_SIDE) { sx = sx - 50; } + return sx; +} +pub fn scene_y(mo: usize, ly: usize, x: i32, y: i32) -> i32 { + var sy : i32 = y; + if (mo == M_PUSH) { sy = CY + @divTrunc((y - CY) * 100, 108); } + if (mo == M_ORBIT) { sy = CY + @divTrunc((y - CY) * 995 - (x - CX) * 99, 1000); } + if (mo == M_RISE) { sy = y + 36; } + if (ly == L_TOP) { sy = sy + 70; } + return sy; +} + +// The colour at canvas point (x, y) of the tall cut, scene a. +pub fn canvas_colour(t: usize, a: usize, ac: usize, mo: usize, ly: usize, n: i32, x: i32, y: i32) -> i32 { + const sx = scene_x(mo, ly, x, y); + const sy = scene_y(mo, ly, x, y); + const acc = ACCENT[ac]; + if (a == 0) { return code_px(t, acc, n, sx, sy); } + if (a == 1) { return graph_px(t, acc, n, sx, sy); } + if (a == 2) { return tests_px(t, acc, n, sx, sy); } + if (a == 3) { return trit_px(t, acc, n, sx, sy); } + if (a == 4) { return loop_px(t, acc, n, sx, sy); } + if (a == 5) { return branch_px(t, acc, n, sx, sy); } + if (a == 6) { return data_px(t, acc, n, sx, sy); } + if (a == 7) { return links_px(t, acc, n, sx, sy); } + if (a == 8) { return guard_px(t, acc, n, sx, sy); } + if (a == 9) { return title_px(t, acc, n, sx, sy); } + if (a == 10) { return task_px(t, acc, n, sx, sy); } + if (a == 11) { return map_px(t, acc, n, sx, sy); } + return rgb(0, 0, 0); +} + +// The colour of pixel (x, y) of a w x h still. +pub fn pixel(t: usize, a: usize, ac: usize, mo: usize, ly: usize, n: i32, x: i32, y: i32, w: i32, h: i32) -> i32 { + const cx = @divTrunc(x * VW + @divTrunc(VW, 2), w); + const cy = @divTrunc(y * VH + @divTrunc(VH, 2), h); + return canvas_colour(t, a, ac, mo, ly, n, cx, cy); +} + +// ---- tests ---- + +test "the camera and the layout move the canvas as the header says" { + assert(scene_x(M_PUSH, L_CENTRE, CX, CY) == CX); + assert(scene_y(M_PUSH, L_CENTRE, CX, CY) == CY); + assert(scene_x(M_PUSH, L_CENTRE, 0, CY) > 0); + assert(scene_x(M_SWEEP, L_CENTRE, 100, 100) == 128); + assert(scene_y(M_RISE, L_CENTRE, 100, 100) == 136); + assert(scene_y(M_RISE, L_TOP, 100, 100) == 206); + assert(scene_x(M_RISE, L_SIDE, 100, 100) == 50); + assert(scene_x(M_ORBIT, L_CENTRE, CX + 100, CY) == CX + 99); + assert(scene_y(M_ORBIT, L_CENTRE, CX + 100, CY) == CY - 9); +} + +test "a still of the code scene is the code scene at that size, and is not a flat colour" { + var seen : i32 = 0; + var last : i32 = 0 - 1; + var y : i32 = 0; + while (y < 192) { + var x : i32 = 0; + while (x < 108) { + const c = pixel(3, 0, 1, 0, 0, 0, x, y, 108, 192); + assert(c >= 0 && c <= 0xFFFFFF); + if (c != last) { seen = seen + 1; } + last = c; + x = x + 1; + } + y = y + 1; + } + assert(seen > 3000); +} + +test "the dressing changes the picture: another motion or layout of one scene differs" { + var d_motion : i32 = 0; + var d_layout : i32 = 0; + var y : i32 = 0; + while (y < 192) { + var x : i32 = 0; + while (x < 108) { + if (pixel(3, 0, 1, 0, 0, 0, x, y, 108, 192) != pixel(3, 0, 1, 3, 0, 0, x, y, 108, 192)) { d_motion = d_motion + 1; } + if (pixel(3, 0, 1, 0, 0, 0, x, y, 108, 192) != pixel(3, 0, 1, 0, 1, 0, x, y, 108, 192)) { d_layout = d_layout + 1; } + x = x + 1; + } + y = y + 1; + } + assert(d_motion > 2000); + assert(d_layout > 2000); +} + +test "every one of the 12 archetypes draws a still that is not a flat colour and differs from its neighbour" { + var a : usize = 0; + while (a < 12) { + var next_a : usize = a + 1; + if (next_a == 12) { next_a = 0; } + var seen : i32 = 0; + var diff : i32 = 0; + var last : i32 = 0 - 1; + var y : i32 = 0; + while (y < 96) { + var x : i32 = 0; + while (x < 54) { + const c = pixel(13, a, 2, 0, 0, 3, x, y, 54, 96); + const d = pixel(13, next_a, 2, 0, 0, 3, x, y, 54, 96); + assert(c >= 0 && c <= 0xFFFFFF); + if (c != last) { seen = seen + 1; } + if (c != d) { diff = diff + 1; } + last = c; + x = x + 1; + } + y = y + 1; + } + assert(seen > 400); + assert(diff > 1000); + a = a + 1; + } +} diff --git a/specs/t27f/scene_task.t27 b/specs/t27f/scene_task.t27 new file mode 100644 index 0000000000..6d613556e7 --- /dev/null +++ b/specs/t27f/scene_task.t27 @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_task.t27 -- scene A_TASK of reel_factory.t27: the lesson's task as a checklist. +// The English task text is wrapped at 34 letters a row at scale 2; every row gets a box, and the +// first two thirds of the rows are ticked in the accent. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: the task is what the learner is asked to do. A checklist of its own words, partly done, is +// a picture nothing else in the factory draws, and its length is the lesson's. +// +// THE FUNCTION: task_px(t, acc, n, x, y) is the colour of canvas point (x, y). Row r starts at +// y = 100 + 22 * r (at most 24 rows); the box is at x = 20, the text at x = 46. +// +// NOT CLAIMED: it wraps on letters, not words, and the tick is a layout rule, not progress. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_task; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::near_seg; +use t27f::scene_raster::char_on; +use t27f::scene_raster::imin; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::reel_topics::item; +use t27f::reel_topics::item_len; +use t27f::reel_topics::cp; + +pub const WRAP : i32 = 34; +pub const TY0 : i32 = 100; +pub const TPITCH : i32 = 22; +pub const TMAX : i32 = 24; +pub const TX_BOX : i32 = 20; +pub const TX_TEXT : i32 = 46; + +pub fn task_len(t: usize) -> i32 { + return @as(i32, @intCast(item_len(item(t, 0, 2)))); +} +pub fn task_rows(t: usize) -> i32 { + return imin(@divTrunc(task_len(t) + WRAP - 1, WRAP), TMAX); +} +pub fn task_ch(t: usize, i: i32) -> i32 { + if (i < 0 || i >= task_len(t)) { return 0; } + return @as(i32, @intCast(cp(item(t, 0, 2), @as(usize, @intCast(i))))); +} +// Rows ticked: two thirds, rounded up, of the rows. +pub fn ticked(rows: i32) -> i32 { + return @divTrunc(rows * 2 + 2, 3); +} + +pub fn task_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(4, acc, seed_of(t, n, 10), x, y); + if (y < TY0) { return c0; } + const rows = task_rows(t); + const r = @divTrunc(y - TY0, TPITCH); + if (r >= rows) { return c0; } + const oy = TY0 + r * TPITCH; + // the box, with a tick for the done rows + if (in_rect(x, y, TX_BOX, oy, TX_BOX + 18, oy + 18)) { + if (r < ticked(rows)) { + if (near_seg(x, y, TX_BOX + 4, oy + 9, TX_BOX + 8, oy + 14, 1)) { return rgb(10, 12, 24); } + if (near_seg(x, y, TX_BOX + 8, oy + 14, TX_BOX + 15, oy + 4, 1)) { return rgb(10, 12, 24); } + return acc; + } + if (!in_rect(x, y, TX_BOX + 2, oy + 2, TX_BOX + 16, oy + 16)) { return acc; } + return rgb(10, 12, 24); + } + if (x >= TX_TEXT && y >= oy + 4 && y < oy + 14) { + const k = @divTrunc(x - TX_TEXT, 8); + if (k < WRAP) { + if (char_on(task_ch(t, r * WRAP + k), x, y, TX_TEXT + k * 8, oy + 4, 2)) { + if (r < ticked(rows)) { return rgb(160, 168, 196); } + return rgb(245, 247, 255); + } + } + } + return c0; +} + +// ---- tests ---- + +test "every lesson has a task, a few rows long" { + var t : usize = 0; + while (t < 27) { + assert(task_len(t) > 10); + assert(task_rows(t) >= 1 && task_rows(t) <= TMAX); + assert(ticked(task_rows(t)) >= 1 && ticked(task_rows(t)) <= task_rows(t) + 1); + t = t + 1; + } + assert(ticked(3) == 2); + assert(ticked(6) == 4); +} + +test "the task text is drawn: white pixels on the unticked rows" { + var white : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (task_px(6, 0x22C55E, 3, x, y) == 0xF5F7FF) { white = white + 1; } + x = x + 1; + } + y = y + 1; + } + assert(white > 30); +} + +test "another lesson draws another checklist" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = task_px(2, 0x22C55E, 3, x, y); + const b = task_px(19, 0x22C55E, 3, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 2; + } + y = y + 2; + } + assert(diff > 5000); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (task_px(12, 0xE0409A, 5, x, y) != task_px(12, 0xE0409A, 5, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_tests.t27 b/specs/t27f/scene_tests.t27 new file mode 100644 index 0000000000..e37cb691ae --- /dev/null +++ b/specs/t27f/scene_tests.t27 @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_tests.t27 -- scene A_TESTS of reel_factory.t27: the lesson's code as a test run. +// Each line of the lesson is a row; a row whose line holds an `assert` shows a ticked box, a bar as +// long as the line and PASS, a `test` header row shows a bar in the accent, any other line a dim bar. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: the lessons that teach by asserting (expressions, constants, trits) are mostly asserts, and +// the picture says so: a column of green PASS rows, as many as the lesson has asserts. +// +// THE FUNCTION: tests_px(t, acc, n, x, y) is the colour of canvas point (x, y). Rows are 22 pixels +// apart from y = 90, at most 24; a lesson with more lines draws its first 24. +// +// NOT CLAIMED: it runs nothing; the ticks are the lines that contain the word assert. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_tests; + +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::near_seg; +use t27f::scene_raster::char_on; +use t27f::scene_raster::imin; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_lex::lex_n; +use t27f::scene_lex::lex_line; +use t27f::scene_lex::lex_llen; +use t27f::scene_lex::lex_has; +use t27f::scene_lex::lex_lines_with; +use t27f::scene_lex::KW_ASSERT; +use t27f::scene_lex::KW_TEST; + +pub const ROW_Y0 : i32 = 90; +pub const ROW_H : i32 = 22; +pub const MAXR : i32 = 24; +pub const PASS_GREEN : i32 = 0x4ADE80; +pub const DIM : i32 = 0x2A3050; + +pub fn row_n(t: usize) -> i32 { + return imin(lex_n(t), MAXR); +} + +pub fn tests_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(2, acc, seed_of(t, n, 2), x, y); + if (y < ROW_Y0) { return c0; } + const r = @divTrunc(y - ROW_Y0, ROW_H); + if (r >= row_n(t)) { return c0; } + const oy = ROW_Y0 + r * ROW_H; + if (y - oy < 3 || y - oy > 18) { return c0; } + const l = lex_line(t, r); + const ok = lex_has(l, KW_ASSERT); + const head = lex_has(l, KW_TEST); + // the box, with a tick in it for a passing row + if (in_rect(x, y, 16, oy + 3, 36, oy + 19)) { + if (!in_rect(x, y, 18, oy + 5, 34, oy + 17)) { + if (ok) { return PASS_GREEN; } + if (head) { return acc; } + return DIM; + } + if (ok && (near_seg(x, y, 20, oy + 11, 25, oy + 15, 1) || near_seg(x, y, 25, oy + 15, 32, oy + 7, 1))) { + return PASS_GREEN; + } + return rgb(8, 10, 20); + } + // the bar: as long as the line + const blen = 8 + lex_llen(l) * 5; + if (in_rect(x, y, 46, oy + 5, 46 + blen, oy + 17)) { + if (ok) { return mix(rgb(10, 40, 24), PASS_GREEN, 150); } + if (head) { return mix(rgb(20, 20, 40), acc, 220); } + return DIM; + } + // PASS at the right + if (ok && x >= 300) { + const k = @divTrunc(x - 300, 14); + if (k < 4 && y >= oy + 6) { + var ch : i32 = 80; + if (k == 1) { ch = 65; } + if (k >= 2) { ch = 83; } + if (char_on(ch, x, y, 300 + k * 14, oy + 6, 3)) { return PASS_GREEN; } + } + } + return c0; +} + +// ---- tests ---- + +test "a row per line and a green row per assert" { + var t : usize = 0; + while (t < 27) { + assert(row_n(t) >= 1 && row_n(t) <= MAXR); + t = t + 1; + } + // lesson 8, Expressions, is a column of asserts; lesson 3 has none + assert(lex_lines_with(8, KW_ASSERT) >= 3); + var green : i32 = 0; + var y : i32 = ROW_Y0; + while (y < ROW_Y0 + MAXR * ROW_H) { + if (tests_px(8, 0xF5B700, 8, 26, y) == PASS_GREEN) { green = green + 1; } + y = y + 1; + } + assert(green > 30); +} + +test "the rows are the lesson's lines: another lesson draws another run" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = tests_px(8, 0x3B82F6, 8, x, y); + const b = tests_px(21, 0x3B82F6, 8, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 4000); +} + +test "negative control: a row without an assert shows no PASS" { + var checked : i32 = 0; + var green : i32 = 0; + var r : i32 = 0; + while (r < row_n(8)) { + if (!lex_has(lex_line(8, r), KW_ASSERT)) { + checked = checked + 1; + var y : i32 = ROW_Y0 + r * ROW_H; + while (y < ROW_Y0 + r * ROW_H + ROW_H) { + var x : i32 = 300; + while (x < 360) { + if (tests_px(8, 0xF5B700, 1, x, y) == PASS_GREEN) { green = green + 1; } + x = x + 1; + } + y = y + 1; + } + } + r = r + 1; + } + assert(checked > 0); + assert(green == 0); +} diff --git a/specs/t27f/scene_title.t27 b/specs/t27f/scene_title.t27 new file mode 100644 index 0000000000..9fd30ad9d0 --- /dev/null +++ b/specs/t27f/scene_title.t27 @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_title.t27 -- scene A_TITLE of reel_factory.t27: the lesson's own title as big +// kinetic letters. Eleven letters a line at scale 7, every letter on its own wave so the line +// ripples, with a hard shadow under it and every third letter in the accent. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: for a lesson that has no picture of its own the honest picture is its name. Different +// titles are different lengths and break into different lines, so no two lessons draw one wall of +// text. +// +// THE FUNCTION: title_px(t, acc, n, x, y) is the colour of canvas point (x, y). Letter k of line r +// stands at x = 26 + 28 * k, y = y0 + 50 * r + wave; y0 centres the block on the canvas. +// +// NOT CLAIMED: it breaks on every 11th letter, not on word boundaries. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_title; + +use t27f::scene_raster::fmod; +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::char_on; +use t27f::scene_raster::tri_wave; +use t27f::scene_raster::imin; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::reel_topics::item; +use t27f::reel_topics::item_len; +use t27f::reel_topics::cp; + +pub const PER_LINE : i32 = 11; +pub const LX0 : i32 = 26; +pub const TITLE_ADV : i32 = 28; +pub const PITCH : i32 = 50; +pub const SHADOW : i32 = 5; + +pub fn title_len(t: usize) -> i32 { + return @as(i32, @intCast(item_len(item(t, 0, 0)))); +} +pub fn title_lines(t: usize) -> i32 { + return imin(@divTrunc(title_len(t) + PER_LINE - 1, PER_LINE), 8); +} +pub fn title_ch(t: usize, i: i32) -> i32 { + if (i < 0 || i >= title_len(t)) { return 0; } + return @as(i32, @intCast(cp(item(t, 0, 0), @as(usize, @intCast(i))))); +} +// The top of the block of title lines, so that it sits in the middle of the canvas. +pub fn title_y0(t: usize) -> i32 { + return 320 - @divTrunc(title_lines(t) * PITCH, 2); +} +// How far letter i is lifted off its line. +pub fn wave(t: usize, i: i32) -> i32 { + return tri_wave(i * 9 + @as(i32, @intCast(t)) * 5, 40, 14); +} + +// Whether letter i of the title is drawn at canvas point (x, y), shifted by (sx, sy). +pub fn letter_on(t: usize, i: i32, x: i32, y: i32, sx: i32, sy: i32) -> bool { + const c = title_ch(t, i); + if (c <= 32) { return false; } + const k = fmod(i, PER_LINE); + const r = @divTrunc(i, PER_LINE); + return char_on(c, x - sx, y - sy, LX0 + k * TITLE_ADV, title_y0(t) + r * PITCH - wave(t, i) + 14, 7); +} + +pub fn title_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(3, acc, seed_of(t, n, 9), x, y); + const k = @divTrunc(x - LX0, TITLE_ADV); + if (x < LX0 || k >= PER_LINE) { return c0; } + const lines = title_lines(t); + const dy = y - title_y0(t); + if (dy < 0 - SHADOW || dy > lines * PITCH + 30) { return c0; } + var r : i32 = 0; + while (r < lines) { + const i = r * PER_LINE + k; + if (letter_on(t, i, x, y, 0, 0)) { + if (fmod(i, 3) == 0) { return acc; } + return rgb(245, 247, 255); + } + if (letter_on(t, i, x, y, SHADOW, SHADOW)) { return rgb(4, 5, 12); } + r = r + 1; + } + return c0; +} + +// ---- tests ---- + +test "the title is the lesson's own, in letters the font draws" { + var t : usize = 0; + while (t < 27) { + assert(title_len(t) > 2); + assert(title_lines(t) >= 1 && title_lines(t) <= 8); + assert(title_y0(t) >= 100 && title_y0(t) <= 300); + t = t + 1; + } + assert(title_ch(0, 0) > 32); + assert(title_ch(0, 900) == 0); +} + +test "the letters ripple, each on its own wave" { + var seen : i32 = 0; + var i : i32 = 0; + while (i < 11) { + assert(wave(5, i) >= 0 && wave(5, i) < 14); + if (wave(5, i) != wave(5, 0)) { seen = seen + 1; } + i = i + 1; + } + assert(seen > 6); +} + +test "another title draws another wall of letters" { + var white : i32 = 0; + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = title_px(2, 0xF5B700, 3, x, y); + const b = title_px(23, 0xF5B700, 3, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a == 0xF5F7FF) { white = white + 1; } + if (a != b) { diff = diff + 1; } + x = x + 2; + } + y = y + 2; + } + assert(white > 100); + assert(diff > 5000); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (title_px(11, 0x3B82F6, 6, x, y) != title_px(11, 0x3B82F6, 6, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/scene_trit.t27 b/specs/t27f/scene_trit.t27 new file mode 100644 index 0000000000..7e53dcacda --- /dev/null +++ b/specs/t27f/scene_trit.t27 @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/scene_trit.t27 -- scene A_TRIT of reel_factory.t27: the lesson's own bytes as a field of +// trits. Each cell of a 9 x 14 grid is one trit, -1, 0 or +1, read from the next byte of the +// lesson's code: a minus bar on red, a ring on grey, a plus on the accent. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: t27 is a ternary language, and the lessons that teach trits (and every other lesson, for +// that matter) are text that a trit field can show: the bytes of the source taken mod 3. The grid +// is the lesson, one byte a cell, so no two lessons draw one field. +// +// THE FUNCTION: trit_px(t, acc, n, x, y) is the colour of canvas point (x, y). The cell of index i +// holds trit (byte i of the lesson + i) mod 3 - 1. +// +// NOT CLAIMED: the mod-3 reading is a picture of the bytes, not the lesson's trit values. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module scene_trit; + +use t27f::scene_raster::fmod; +use t27f::scene_raster::rgb; +use t27f::scene_raster::mix; +use t27f::scene_raster::in_rect; +use t27f::scene_raster::in_disc; +use t27f::scene_raster::in_ring; +use t27f::scene_bg::bg_px; +use t27f::scene_bg::seed_of; +use t27f::scene_lex::lex_byte; + +pub const GCOLS : i32 = 9; +pub const GROWS : i32 = 14; +pub const CELL : i32 = 40; +pub const GY0 : i32 = 40; +pub const NEG : i32 = 0xE5484D; +pub const ZERO : i32 = 0x7C8499; + +// The trit, -1, 0 or +1, of cell i of lesson t. +pub fn trit_of(t: usize, i: i32) -> i32 { + return fmod(lex_byte(t, i) + i, 3) - 1; +} + +pub fn trit_px(t: usize, acc: i32, n: i32, x: i32, y: i32) -> i32 { + const c0 = bg_px(3, acc, seed_of(t, n, 3), x, y); + if (y < GY0 || x < 0) { return c0; } + const col = @divTrunc(x, CELL); + const row = @divTrunc(y - GY0, CELL); + if (col >= GCOLS || row >= GROWS) { return c0; } + const v = trit_of(t, row * GCOLS + col); + const cx = col * CELL + 20; + const cy = GY0 + row * CELL + 20; + var hue = ZERO; + if (v < 0) { hue = NEG; } + if (v > 0) { hue = acc; } + if (!in_disc(x, y, cx, cy, 17)) { return c0; } + if (in_ring(x, y, cx, cy, 15, 17)) { return hue; } + const body = mix(rgb(10, 12, 24), hue, 70); + if (v < 0 && in_rect(x, y, cx - 8, cy - 2, cx + 8, cy + 2)) { return hue; } + if (v > 0 && (in_rect(x, y, cx - 8, cy - 2, cx + 8, cy + 2) || in_rect(x, y, cx - 2, cy - 8, cx + 2, cy + 8))) { return hue; } + if (v == 0 && in_ring(x, y, cx, cy, 4, 7)) { return hue; } + return body; +} + +// ---- tests ---- + +test "the trit of a cell is -1, 0 or +1 and all three occur" { + var neg : i32 = 0; + var zero : i32 = 0; + var pos : i32 = 0; + var i : i32 = 0; + while (i < GCOLS * GROWS) { + const v = trit_of(7, i); + assert(v >= 0 - 1 && v <= 1); + if (v < 0) { neg = neg + 1; } + if (v == 0) { zero = zero + 1; } + if (v > 0) { pos = pos + 1; } + i = i + 1; + } + assert(neg > 10 && zero > 10 && pos > 10); +} + +test "the field is the lesson's bytes: another lesson draws another field" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + const a = trit_px(4, 0x22C55E, 6, x, y); + const b = trit_px(19, 0x22C55E, 6, x, y); + assert(a >= 0 && a <= 0xFFFFFF); + if (a != b) { diff = diff + 1; } + x = x + 3; + } + y = y + 3; + } + assert(diff > 5000); +} + +test "each of the three trits has its own look" { + // find one cell of each sign in lesson 4 and compare the middle pixel of each + var cn : i32 = 0 - 1; + var cz : i32 = 0 - 1; + var cp : i32 = 0 - 1; + var i : i32 = 0; + while (i < GCOLS * GROWS) { + const v = trit_of(4, i); + if (v < 0 && cn < 0) { cn = i; } + if (v == 0 && cz < 0) { cz = i; } + if (v > 0 && cp < 0) { cp = i; } + i = i + 1; + } + assert(cn >= 0 && cz >= 0 && cp >= 0); + const rn = trit_px(4, 0x22C55E, 6, fmod(cn, GCOLS) * CELL + 20 + 5, GY0 + @divTrunc(cn, GCOLS) * CELL + 20); + const rp = trit_px(4, 0x22C55E, 6, fmod(cp, GCOLS) * CELL + 20 + 5, GY0 + @divTrunc(cp, GCOLS) * CELL + 20); + assert(rn == NEG); + assert(rp == 0x22C55E); + assert(rn != rp); +} + +test "negative control: the same lesson twice is one picture" { + var diff : i32 = 0; + var y : i32 = 0; + while (y < 640) { + var x : i32 = 0; + while (x < 360) { + if (trit_px(9, 0xE0409A, 2, x, y) != trit_px(9, 0xE0409A, 2, x, y)) { diff = diff + 1; } + x = x + 5; + } + y = y + 5; + } + assert(diff == 0); +} diff --git a/specs/t27f/still_gate.t27 b/specs/t27f/still_gate.t27 new file mode 100644 index 0000000000..a7f854d11c --- /dev/null +++ b/specs/t27f/still_gate.t27 @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/still_gate.t27 -- the anomaly gate of the T27F content factory. It renders the 27 +// stills of course 0 from the factory's own plan (reel_factory.t27 picks the archetype, accent, +// motion and layout of each lesson; scene_still.t27 draws it), compares every pair with a block +// SSIM, and refuses the course when the most similar pair is above SIMILAR_MAX_PERMILLE. +// Issue gHashTag/t27#10571 (R3), epic #10560. +// +// WHY: the owner's complaint was that every reel looks the same. reel_factory.t27 plans 27 +// different scenes, but a plan is not a picture: two renderers can still draw near-identical +// frames. This is the gate that looks at the pictures, so "they are all different" is measured +// instead of believed. +// +// THE METRIC: structural similarity on the luma of a 108 x 192 still, in 12 x 12 blocks (9 x 16 of +// them), the mean over the blocks, in permille. For one block with n = 144 pixels, sums Sa, Sb, +// Saa, Sbb, Sab of the two stills' luma, and C1 = 7, C2 = 59 (the usual (0.01 * 255)^2 and +// (0.03 * 255)^2, rounded up to integers): +// SSIM = (2 Sa Sb + C1 n^2) (2 (n Sab - Sa Sb) + C2 n^2) +// / ((Sa^2 + Sb^2 + C1 n^2) (n Saa - Sa^2 + n Sbb - Sb^2 + C2 n^2)) +// which is the textbook formula multiplied through by n^2 so that it needs no division until the +// end. Integers only, in i64, so the gate gives the same number on every machine. +// +// THE GATE: gate() renders the 27 stills into a caller-owned buffer, scores all 351 pairs and +// reports the worst pair in out[0..3] = (lesson a, lesson b, permille). It returns 1 when that +// score is above SIMILAR_MAX_PERMILLE (the course is refused) and 0 when it is not. +// +// NOT CLAIMED: SSIM on luma sees layout and contrast, not meaning; two stills with different +// words in the same boxes would pass. The 850 bound is reel_factory's, now measured (see the +// test that pins the worst pair of course 0). No file is written and nothing is printed: a +// scratch driver can print the table, the module itself holds no main. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module still_gate; + +use t27f::reel_factory::arche; +use t27f::reel_factory::accent; +use t27f::reel_factory::motion; +use t27f::reel_factory::layout; +use t27f::reel_factory::content; +use t27f::reel_factory::too_similar; +use t27f::reel_factory::SIMILAR_MAX_PERMILLE; +use t27f::scene_still::pixel; +use t27f::scene_raster::luma; + +pub const GATE_W : i32 = 108; +pub const GATE_H : i32 = 192; +pub const GATE_PIX : usize = 20736; +pub const GATE_BLOCK : i32 = 12; +pub const GATE_BX : i32 = 9; +pub const GATE_BY : i32 = 16; +pub const GATE_BLOCKS : usize = 144; +pub const GATE_STILLS : usize = 27; +pub const GATE_PAIRS : usize = 351; + +// The scale of one block: n = 144 pixels, n^2 = 20736. +pub const SSIM_N : i64 = 144; +pub const SSIM_NN : i64 = 20736; +pub const SSIM_C1 : i64 = 7; +pub const SSIM_C2 : i64 = 59; + +// Draw lesson t's still from the plan into img[base .. base + GATE_PIX], as luma. +pub fn render_luma(t: usize, img: []u8, base: usize) -> void { + const a = arche(t); + const ac = accent(t); + const mo = motion(t); + const ly = layout(t); + const n = @as(i32, @intCast(content(t, a))); + var y : i32 = 0; + while (y < GATE_H) { + var x : i32 = 0; + while (x < GATE_W) { + const c = pixel(t, a, ac, mo, ly, n, x, y, GATE_W, GATE_H); + img[base + @as(usize, @intCast(y * GATE_W + x))] = @as(u8, @intCast(luma(c))); + x = x + 1; + } + y = y + 1; + } +} + +// The index in img of pixel (i, j) of block (bx, by) of the still that starts at base. +pub fn at(base: usize, bx: i32, by: i32, i: i32, j: i32) -> usize { + return base + @as(usize, @intCast((by * GATE_BLOCK + j) * GATE_W + bx * GATE_BLOCK + i)); +} + +// Fill sum[s * 144 + b] and sq[s * 144 + b] for still s, whose pixels start at base. +pub fn block_stats(img: []u8, s: usize, base: usize, sum: []i64, sq: []i64) -> void { + var by : i32 = 0; + while (by < GATE_BY) { + var bx : i32 = 0; + while (bx < GATE_BX) { + var a : i64 = 0; + var q : i64 = 0; + var j : i32 = 0; + while (j < GATE_BLOCK) { + var i : i32 = 0; + while (i < GATE_BLOCK) { + const v = @as(i64, img[at(base, bx, by, i, j)]); + a = a + v; + q = q + v * v; + i = i + 1; + } + j = j + 1; + } + const b = @as(usize, @intCast(by * GATE_BX + bx)); + sum[s * GATE_BLOCKS + b] = a; + sq[s * GATE_BLOCKS + b] = q; + bx = bx + 1; + } + by = by + 1; + } +} + +// The SSIM, in permille, of two blocks from their sums, sums of squares and sum of products. +pub fn block_ssim(sa: i64, sb: i64, qa: i64, qb: i64, sab: i64) -> i64 { + const l1 = 2 * sa * sb + SSIM_C1 * SSIM_NN; + const l2 = 2 * (SSIM_N * sab - sa * sb) + SSIM_C2 * SSIM_NN; + const d1 = sa * sa + sb * sb + SSIM_C1 * SSIM_NN; + const d2 = (SSIM_N * qa - sa * sa) + (SSIM_N * qb - sb * sb) + SSIM_C2 * SSIM_NN; + const q1 = @divTrunc(l1 * 1000, d1); + return @divTrunc(q1 * l2, d2); +} + +// The mean block SSIM, in permille, of stills a and b (their images are at a * GATE_PIX and b * +// GATE_PIX in img, their block statistics already in sum and sq). +pub fn pair_ssim(img: []u8, a: usize, b: usize, sum: []i64, sq: []i64) -> i32 { + const ba = a * GATE_PIX; + const bb = b * GATE_PIX; + var total : i64 = 0; + var by : i32 = 0; + while (by < GATE_BY) { + var bx : i32 = 0; + while (bx < GATE_BX) { + var sab : i64 = 0; + var j : i32 = 0; + while (j < GATE_BLOCK) { + var i : i32 = 0; + while (i < GATE_BLOCK) { + sab = sab + @as(i64, img[at(ba, bx, by, i, j)]) * @as(i64, img[at(bb, bx, by, i, j)]); + i = i + 1; + } + j = j + 1; + } + const k = @as(usize, @intCast(by * GATE_BX + bx)); + total = total + block_ssim(sum[a * GATE_BLOCKS + k], sum[b * GATE_BLOCKS + k], sq[a * GATE_BLOCKS + k], sq[b * GATE_BLOCKS + k], sab); + bx = bx + 1; + } + by = by + 1; + } + return @as(i32, @intCast(@divTrunc(total, @as(i64, @intCast(GATE_BLOCKS))))); +} + +// Render the 27 stills of the course into img (27 * 20736 bytes) and their block statistics into +// sum and sq (27 * 144 each). +pub fn render_course(img: []u8, sum: []i64, sq: []i64) -> void { + var s : usize = 0; + while (s < GATE_STILLS) { + render_luma(s, img, s * GATE_PIX); + block_stats(img, s, s * GATE_PIX, sum, sq); + s = s + 1; + } +} + +// Score every pair; out[0] = lesson a, out[1] = lesson b, out[2] = permille of the most similar. +pub fn worst_pair(img: []u8, sum: []i64, sq: []i64, out: []i32) -> void { + var best : i32 = 0 - 2000; + var ba : usize = 0; + var bb : usize = 1; + var a : usize = 0; + while (a < GATE_STILLS) { + var b : usize = a + 1; + while (b < GATE_STILLS) { + const v = pair_ssim(img, a, b, sum, sq); + if (v > best) { + best = v; + ba = a; + bb = b; + } + b = b + 1; + } + a = a + 1; + } + out[0] = @as(i32, @intCast(ba)); + out[1] = @as(i32, @intCast(bb)); + out[2] = best; +} + +// The whole gate. 1 when the course is refused, 0 when every pair is told apart. +pub fn gate(img: []u8, sum: []i64, sq: []i64, out: []i32) -> i32 { + render_course(img, sum, sq); + worst_pair(img, sum, sq, out); + if (too_similar(@as(u32, @intCast(out[2] + 0)))) { return 1; } + return 0; +} + +// ---- tests ---- + +test "one block against itself is 1000 and against its negative is far below" { + // a block of mixed luma: half 40, half 200 + const sa = 72 * 40 + 72 * 200; + const qa = 72 * 40 * 40 + 72 * 200 * 200; + assert(block_ssim(sa, sa, qa, qa, qa) == 1000); + // the same block against its inverse (40 <-> 200): same mean, covariance negative + const sab = 144 * 40 * 200; + assert(block_ssim(sa, sa, qa, qa, sab) < 0); + // a flat block against another flat block of the same luma is 1000 (nothing to tell apart) + assert(block_ssim(144 * 30, 144 * 30, 144 * 900, 144 * 900, 144 * 900) == 1000); + // flat dark against flat bright: the luminance term separates them + assert(block_ssim(144 * 30, 144 * 200, 144 * 900, 144 * 40000, 144 * 6000) < 500); +} + +test "the gate scores the 27 stills of course 0 and reports the most similar pair" { + var img : [559872]u8 = undefined; + var sum : [3888]i64 = undefined; + var sq : [3888]i64 = undefined; + var out : [3]i32 = undefined; + const refused = gate(&img, &sum, &sq, &out); + assert(out[0] >= 0 && out[0] < 26); + assert(out[1] > out[0] && out[1] < 27); + assert(out[2] > 0 - 1000 && out[2] <= 1000); + // the pair really is the maximum: it is not beaten by the pair (0, 1) or the pair (25, 26) + assert(out[2] >= pair_ssim(&img, 0, 1, &sum, &sq)); + assert(out[2] >= pair_ssim(&img, 25, 26, &sum, &sq)); + // the verdict follows the bound + assert((refused == 1) == too_similar(@as(u32, @intCast(out[2] + 0)))); + assert(refused == 0); + assert(out[2] <= @as(i32, @intCast(SIMILAR_MAX_PERMILLE))); + // measured on the Railway lab (R3, #10571): the most similar pair of course 0 is lessons 8 and + // 20 at 607 permille, 243 under the bound. A renderer change that moves it must move this line. + assert(out[0] == 8); + assert(out[1] == 20); + assert(out[2] == 607); +} + +test "negative control: a still against itself is 1000 and the gate would refuse a repeat" { + var img : [41472]u8 = undefined; + var sum : [288]i64 = undefined; + var sq : [288]i64 = undefined; + render_luma(5, &img, 0); + render_luma(5, &img, GATE_PIX); + block_stats(&img, 0, 0, &sum, &sq); + block_stats(&img, 1, GATE_PIX, &sum, &sq); + const same = pair_ssim(&img, 0, 1, &sum, &sq); + assert(same == 1000); + assert(too_similar(@as(u32, @intCast(same)))); +} + +test "negative control: two different lessons are scored below a repeat" { + var img : [41472]u8 = undefined; + var sum : [288]i64 = undefined; + var sq : [288]i64 = undefined; + render_luma(5, &img, 0); + render_luma(6, &img, GATE_PIX); + block_stats(&img, 0, 0, &sum, &sq); + block_stats(&img, 1, GATE_PIX, &sum, &sq); + assert(pair_ssim(&img, 0, 1, &sum, &sq) < 1000); +}