From 490f3dc227195c49f4dc4d4aa63c2ee8dbd122d6 Mon Sep 17 00:00:00 2001 From: Jeany Date: Mon, 19 Oct 2026 22:16:24 -0600 Subject: [PATCH 1/2] A map of our own refuses what it built wrong without a word: a text for other players, trigger() records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit print(…, { to: P2 }) and { to: AllPlayers } were shown to the program's owner alone: the emitter never read `to`, and Display Text shows to the current player, the owner. The Python side sets each player of the group current in turn. Refused now, until a print does the same. build-own clears the map's triggers, so a script's trigger() records were dropped. Refused now, until step 13 keeps them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg --- compiler/backend/emit.ts | 4 ++++ scripts/build-own.mts | 2 ++ tests/emitter.test.ts | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/compiler/backend/emit.ts b/compiler/backend/emit.ts index adf88ee..f17e87c 100644 --- a/compiler/backend/emit.ts +++ b/compiler/backend/emit.ts @@ -20,6 +20,7 @@ */ import { STACK_DEPTH, type ArrayDecl, type BoolExpr, type Call, type FuncDecl, type NumExpr, type Program, type Stmt, type TextPart, type VarDecl, type At } from "../ir"; import { PACK_FROM } from "../eud"; +import { PlayerGroup } from "../../vendor/triggers"; import { END, MOST_ACTS, MOST_CONDS, STACK, checkGraph, nextOf, type Act, type Compare, type Cond, type Dword, type FixedFn, type Graph, type Num, type Region, type Trigger } from "./graph"; /** The game's tick (frames since the game started), and the fields of the trigger a map of our own wakes by. */ @@ -523,6 +524,9 @@ class Scope { case "unrolled": return s.iterations.every((it) => this.stmts(it, true)) || this.island(s); case "sleep": return this.native || this.island(s); case "print": + // (Display Text shows to the current player, the owner in a map of our own: a text for anyone else is refused + // until a print goes to each player of its group in turn, as the Python side's does.) + if (this.native && s.to !== PlayerGroup.CurrentPlayer) return this.no("a text shown to other players than the program's owner") || this.island(s); if (this.native && s.position === "chat" && s.parts.every((p) => this.printed(p))) return true; return this.island(s); case "call": return (this.native && this.call(s.call)) || this.island(s); diff --git a/scripts/build-own.mts b/scripts/build-own.mts index 36c7742..e593320 100644 --- a/scripts/build-own.mts +++ b/scripts/build-own.mts @@ -33,6 +33,8 @@ if (r.diagnostics.length) { for (const d of r.diagnostics) console.error(`${d.fi if (r.ir.length !== 1) { console.error(`a map of our own has one program; this script has ${r.ir.length}`); process.exit(1); } const program = r.ir[0]; if (program.perPlayer) { console.error("a map of our own has no per-player program yet"); process.exit(1); } +// (The map's triggers are cleared below, so `trigger()` records would be lost without a word.) +if (r.triggers.length) { console.error(`a map of our own keeps no trigger() records yet; this script has ${r.triggers.length}`); process.exit(1); } const compiled = performance.now(); const decls = new Map(); diff --git a/tests/emitter.test.ts b/tests/emitter.test.ts index 00a0dd0..79e48ce 100644 --- a/tests/emitter.test.ts +++ b/tests/emitter.test.ts @@ -903,6 +903,16 @@ describe("the emitter, a loop's test", () => { }); }); +describe("the emitter, a map of our own's texts", () => { + it("refuses a text for other players than the owner, whom Display Text alone would show it to", () => { + // print("hi", { to: AllPlayers }) and print("hi", { to: P2 }): the Python side shows each to every player of + // its group in turn; a map of our own showed both to its owner alone, saying nothing. + const said = (to: number) => emitBody([{ kind: "print", to, position: "chat", parts: [{ kind: "text", text: "hi" }], at, label: "" }], new Map(), "p"); + expect(typeof said(13)).not.toBe("string"); + for (const to of [17, 1, 18]) expect(said(to)).toMatch(/a text shown to other players than the program's owner/); + }); +}); + // ── a map of our own, as the game walks its triggers ── /** From 7407c884406847a64e8a11f19cc3d3dff0a29dc9 Mon Sep 17 00:00:00 2001 From: Jeany Date: Mon, 19 Oct 2026 22:16:24 -0600 Subject: [PATCH 2/2] =?UTF-8?q?Plan:=20the=20way=20off=20eudplib,=20audite?= =?UTF-8?q?d=20=E2=80=94=20what=20the=20nine=20steps=20left=20out,=20seven?= =?UTF-8?q?teen=20steps=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every probe's programs, about 230 one-feature scripts, the IR against both backends, eudplib's own machinery and every tool that reads a build, checked against the list after step 4 (numbers) was found by accident. New steps: the game's conditions and actions, its memory, programs/owners/players, the build in the editor, the tools on maps of our own, the gate before the Python goes. Fixed point, random and u8/u16 joined step 4; errors joined tasks. With the two silent misbuilds found, the reason the arithmetic stayed hidden (refusals without reasons), the smaller things, which showpiece waits on which step, three decisions, and a ledger so the list stays whole. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T1CM9pLuTuAn3mgTEkjgVg --- docs/v4-plan.md | 152 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 139 insertions(+), 13 deletions(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index c466975..1a4e424 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -3511,19 +3511,68 @@ None of what follows runs code of our own or reaches past what the emulator list patterns as a state machine) that builds back. Classic triggers only. **The way off eudplib.** Each step held the same way — the script built both ways says the same at the same frames in -the lifted game, and the probe plays the same in Remastered — and each better than eudplib's way, not a copy of it: - -1. Calls (below). -2. Texts with values (4's digits), and texts made in the game. -3. Arrays and the heap in the emitter, a store at any index included. -4. Numbers the emitter leaves to the Python side today: a product of two values, a quotient and a remainder (by a - constant too), `&` `|` `^` of two values or with a constant, a shift by a value — eudplib's `f_mul`, `f_div` and - bit functions. A map of our own refuses all of them now (found 2026-10-19, writing the arrays probe). -5. Units: loops, fields, events, the queries scheduled (6, 7). -6. Tasks, `async`, per-player programs, on the walk as the scheduler (2). -7. Input (13). -8. The map's own triggers kept beside the program (eudplib's stage 3), or built into it. -9. `python/trigscript.py` (7,100 lines) and Pyodide deleted: the own-loader probe builds in 0.7 s against 5.3. +the lifted game, and the probe plays the same in Remastered — and each better than eudplib's way, not a copy of it. +The list had nine steps until the audit of 2026-10-19 ("The way off eudplib, audited", after the arrays section) +found what they left out; where a step moved, its old number is given. + +1. Calls ✓ (#35), and recursion ✓ (#37). +2. Texts with numbers in them ✓ (#38). Texts as values moved to 8. +3. Arrays of a fixed length ✓ (#39). Arrays that grow moved to 9. +4. **Numbers.** A product, a quotient and a remainder of two values and by a constant, signed and `u32`; `&` `|` `^` + as values; a shift by a value; `trunc`. Fixed point: `fmul`, `fdiv`, `c / x` (the emitter's reciprocal exists), + `sqrt`, `sin`, `cos`, `atan2` (`tan` and `hypot` come down to these). `u8` and `u16` variables (saturating + `bits`). Random numbers: `random()`, `randomInt`, seeded as `f_randomize` seeds them (the Randomize switch), each + draw a product (`f_dwrand`). A fixed number printed. eudplib's `f_mul`, `f_div`, the bit functions and `f_dwrand`, + each beaten. +5. **The game's conditions and actions** (new). A trigger record of any condition and any action in the graph and the + layout — they encode three today: the Memory condition, Set Deaths and Display Text. Fields filled from values + (`action.variables`), action texts (transmissions, objectives, leaderboards). Every condition in a test and as a + value (ore, deaths, score, kills, Bring, Command, the countdown; the Python side's bit search), the players' facts + (race, slot, human, left) and supply; `rose` and `once`. A print to a group — each player in turn, as the Python + side does — and at the centre. Sounds: Play WAV with the clips in the archive, the music library. Locations: + `centerLocation`, the reserved locations, the scratch location an order goes through. +6. **The game's memory** (new). `stats()` tables read and written (the speed, colour and name cells included), + `memory.vision`, `memory.game`, `memory.map`, `memory()` and `setMemory()`. What the fog library, the raycaster and + Bad Apple stand on. +7. **Programs, owners and players** (new; per-player programs were in 6). Several `program()`s in a map — a top-level + `on.*` is a program of its own, and `trigscript/fog` adds one. Owners checked against the map's slots and forces, as + the Python side's `owner_slots`. Per-player programs. The walk linked into every player's list with a once-a-tick + guard, as eudplib's runner is, so an empty owner slot or an owner who leaves does not stop the program — both + played, the leaver in a game of two. +8. **Texts as values** (was half of 2). Text variables and arrays; compared, searched, measured, repeated, split and + joined; templates and JSX made while the game runs; `name()` and `color()` in a print; `ui.lines`. +9. **Arrays that grow, and what stands on the heap** (was half of 3). `push`, `pop`, a length set, `through`, `Map` + and `Set`, a task's arrays, an array in a function that calls itself (the front end makes every one of those + growing today, a fixed length too); closures over locals and reference classes (`held`, the pool of 1,024 cells); + signals, which stand on both. +10. **Units** (was 5). Every unit value, unit loops and events, fields, flags, writes and verbs, picks, `select()` and + the game command it sends, `underMouse`; the queries scheduled (ideas 6, 7). +11. **Tasks, `async` and errors** (was 6; errors new). `start`, `cancel`, `running`, `Promise.all`/`race`/`any`, + `every`, `after`, `on.*` handlers, on the walk as the scheduler (idea 2). `try`, `catch`, `finally`, `throw`, + `uncaught`, a program's `errors`, the error classes. +12. **Input** (was 7; idea 13). Keys, clicks, the mouse, typed lines, sync channels, the `local.*` reads and the user's + player id. The test harness types and presses through chatEvent's and MSQC's addresses (`GameSim`): it moves with + them, and the map stops needing what MSQC takes (a location, the Valkyrie's entry, Player 12's units). +13. **The map's own triggers** (was 8). Kept beside the program, in eudplib's order (programs after every player's + triggers) or another one said; `trigger()` and `hyperTriggers()` records. +14. **The build in the editor** (new). A build step of the plugin's own for Save, Test Map and export; the CHK and the + archive written in the plugin (mopaq bundled, or an addition to the plugin API); eudplib's fixes to the map + (MTXM's null tiles, a desync by its source's account; UNIT flags; UNIx/UPGx/TECx defaults); the script's settings + (stack depth, heap) passed; a refusal shown as a Problem while the script is typed, naming what it refuses; the + changeover (below); Magenta and TrigScript in one map; the payload's size (eudplib stacks its objects; ours + overlaps only cells, and recursion's payload is 1.87 MB). +15. **The tools on maps of our own** (new). The trigger map and the variable map from the layout, in the rows scm-sim + reads — A1 to A5, Find an EUD Error, cost by line, the desync detector and `sim.program()` name lines with them; + `build-fixture` moved onto the own build (the `trigger()` records, Home/Away/Pixel, PEOPLE, the settings); + the allowlist's evidence traced again; the lowering bench's baseline anew; a built map tested end to end in CI. +16. **The gate before the Python goes** (new). With it goes the second build every step is held against. Before: + random whole programs (idea 17) built our way and run in the lifted game against the simulator; every probe and + showpiece built our way and played again in Remastered; the simulator's model (`docs/ir.md`: the Python lowering's + today) moved to the own backend. +17. **Deleted** (was 9): `python/trigscript.py` (7,100 lines), its embedding (`embed-python.mts`, whose stdlib half + stays), the IR's hand-off to Python (`serializeIr`, `packTable`, `IR_VERSION`), the `requires` of plugin-eudplib, + and the copy in five repositories that says a map is built by eudplib. The own-loader probe builds in 0.7 s + against 5.3. ### Calls on our own backend (2026-10-19) @@ -3751,6 +3800,83 @@ print and own-loader probes still all ok, recursion's frame 11.53 ms (11.64 befo Not yet: arrays that grow (`push`, `pop`, a length set: the heap), arrays of texts, `u8` and `u16` arrays, a task's arrays; numbers (step 4) — the probe's first store was `board[i] = i * i + 1`, refused. +### The way off eudplib, audited (2026-10-19) + +Step 4 was found by accident: the arrays probe's `i * i + 1` was refused, and the probe was changed to fit. So the +list was checked against everything the Python side does, from four sides: + +- **The probes.** Every program of the 93 probes given to the own emitter, then each of its statements and expressions + alone, every refusal counted by what refused it. +- **One feature at a time.** About 230 scripts of a feature each, compiled and given to `emitBody`. +- **The code.** The IR, kind by kind, against `emit.ts` and `python/trigscript.py`; eudplib's own machinery (its + injector, euddraft's driver and plugins, `fixmapdata`); the euddraft plugins the input uses. +- **Everything that reads a build.** The editor's build, the tools of Part A, the scripts, the tests, CI, scm-sim, and + the other repositories that mention eudplib. + +**What a map of our own takes today.** Numbers added, subtracted, multiplied and shifted by a constant, compared, +`min`, `max`, `abs`; booleans; arrays of a fixed length; calls and recursion; a print of known text and whole numbers to +the current player; sleeps; loops; and what the front end turns into those — classes, records, generators, labels, +sorting, callbacks, function values, `await` of a function, `until`. Twelve of the 93 probes build: the five own-backend +probes and seven measurement probes. No showpiece does. + +**What the nine steps left out** — steps 5, 6, 7 and 14 to 16 now, and parts of 4, 8, 9 and 11: + +- *The game itself.* The graph and the layout can hold three kinds of record. Every other action is refused — Create + Unit (5,992 statements across the probes), Remove Unit, Set Resources, Play WAV (312), Center View, Talking Portrait, + Transmission, Minimap Ping, the countdown, leaderboards, objectives — and every read of the game: ore, deaths, Bring, + kills, a player's slot, race or leaving. Random numbers too. No step named any of it; units (old 5) covered only + unit values. +- *The game's memory.* `stats()` tables, `memory.*`, `local.*`: what the fog library, the raycaster and Bad Apple + write. +- *Locations.* `centerLocation`, the reserved ones, the scratch location an order goes through. +- *Fixed point past `+` and `-`.* `fmul` (23 statements), `fdiv` (81), `sqrt`, `sin`, `cos`: old step 4 named only + whole numbers. +- *Errors.* `try`, `throw`, `uncaught`. +- *Programs and players.* 27 of the 93 probes have more than one program, and `trigscript/fog` adds one to any script + that imports it; `build-own` takes one. The start-up links the owner's list only (`layout.ts`): an empty owner slot + never starts the program, and an owner who leaves may stop it. eudplib links a runner into every player's list. +- *The build and the tools.* No build step in the editor (`build-own` is a script that imports the editor's code from + a checkout, and the plugin API cannot write the archive); no sounds in the archive; none of eudplib's fixes to the + map; Magenta and TrigScript cannot share a map (two payloads at one address, two links of the lists); A1 to A5, + Find an EUD Error and the test harness read the variable and trigger maps only the Python build writes; no built map + is tested in CI; and once the Python is deleted, nothing is left to build the second way and compare against. + +**Wrong without a word — made refusals with this audit.** `print(…, { to })` was shown to the program's owner +whoever it was for (`Scope` never read `to`; the Python side sets each player of the group current in turn): a text +for anyone else is refused now (`tests/emitter.test.ts`). `trigger()` records were dropped by `build-own`, which +clears the map's triggers: refused now. + +**Why the arithmetic stayed hidden.** Most refusals read `a assign it does not lower (no reason given), line N`: +`Scope.test()` drops the reasons `bool()` gives, `lowers()` and `withCells()` give none, and `why ||=` keeps an +earlier reason that is not the one. `a * b`, `a / 4`, `random(6)` and `minerals(P1)` were all refused in the same +words. Step 14 asks for a refusal that names what it refuses; the ledger (below) asks for it to be right. + +**Smaller things**, to fix inside the step that meets them: +- A boolean `?:` is refused, with constants on both sides too (`Rewriter.cond` has no case for it). +- A call in a `do` loop's test is refused. +- `emitBody` does not try a crowded branch again with copies, as `emitLoop` does. +- "An array read at the loop's counter", a rule for eudplib's loops, refuses code of our own too. +- `build-own` passes no stack depth, so the refusal that says to lower it cannot be followed. +- Arrays and stacks past 65,536 cells. + +**Which showpiece waits on which step.** Tetris: 4 (random), 5 (Remove Unit), 11, 12. Snake: the same and 8 (JSX +made in the game). The raycaster: 4 (fixed point, `sin`, `cos`), 5 (`centerLocation`), 6, 7 (the fog's program), 12. +Bad Apple: 5, 6, 7, as the fog library. The bullets: 5 (a death count read), 9. The cutscene: 5, 8, 9, 10, 11, 12. + +**Decisions it asks for:** +- *The changeover.* Build our own way wherever the emitter takes the whole script, and through eudplib elsewhere, + saying which in Output — from step 14 on; or switch nothing until step 13. The first gives map makers the speed early + and needs a script's refusals shown while it is typed, so that a map does not change builds without anyone seeing. +- *Protection.* eudplib scrambles the locations, shuffles its payload and adds an ISOM section of negative length; + ours does none of it. Keep none of it, or some? +- *Magenta.* It keeps plugin-eudplib alive. Until it builds on the own layout, a map with both could have TrigScript's + program built into Magenta's eudplib build, or refuse the pair. + +**The ledger, so the list stays whole.** A script for each feature — the audit's 230 are the start — each with its +step and whether a map of our own takes it, run by a test in `tests/`. It fails when a script's answer changes, and +when a kind of the IR or a name the library exports has no row. A step is done when its rows say so. Proposed as the +first thing before step 4. + ## Part C — the game, pushed Each is a library over Part B, offered only after its probe. "Known" means a played probe