Skip to content

Commit 961a6fb

Browse files
wormeymanclaude
andauthored
test(cliffs): four of the ten ore remainders are the onDestroy CASCADE (#84) (#125)
`vulcanusOreRejection.ts` records box overlap as explaining 21 of the 31 cells the ore suppresses, the other ten as "run remainders", and the cascade half of that question as REFUTED. That refutation is about a DIFFERENT cascade. It tested #108's crossing-stage mechanism - a rejection zeroes the cell's edge registers, a neighbour's code changes, re-test to a fixpoint - and its conclusion ("rejected cells do not turn neighbours rejectable") still stands. The `applyCliffs` cascade is `Cliff::onDestroy` taking the facing end of every connected neighbour and destroying a neighbour left with no end at all. It was read out of the binary in #113, AFTER #110 ran, and nothing had re-run the remainder question against it. Replayed entirely on the game's own data - its ore-off cliff set (892 with the game's orientations), its resource entity positions, the prototype boxes the fixture itself carries - so none of the port's field, ore model or geyser roll appears in the measurement: | arm | matched | wrong | surplus | | --- | --- | --- | --- | | control - destroy the lever's own 31 | 861 | 0 | 0 | | direct overlap only, no cascade | 856 | 5 | 10 | | direct overlap + onDestroy CASCADE | 859 | 2 | 6 | - Direct overlap fires on 21 and every one is in the lever's 31 - precision 1.000 re-derived with no data of ours. - The cascade closes 4 of the 10 remainders and 3 of the 5 orientation errors. Recall 21/31 = 0.677 -> 25/31 = 0.806, no new parameter, no wider box. - `missing` is 0 in both arms, so no cell the game kept is ever removed. That is what separates a mechanism from a bigger rectangle - and #124 established the box is not the engine's collision test to begin with. The control validates the harness, and getting it exact needs `noUpdateConnections: true`: the game's dumped set is POST-pipeline, so running `updateConnections` over it again double-applies the pass and scores wrong=13, which reads like a defect in the cascade model and is an artifact of the harness. Any experiment starting from a dumped set has the same hazard. Six remain: 1546,1550.5 1546,1554.5 1606,1590.5 1606,1594.5 1622,1614.5 1626,1614.5. Two are the geyser pair, and the first is the one cell in this whole residual whose destruction the game's own orientations directly witness (#124's n=1) - so the best-evidenced destruction is still unexplained. Measurement only - the renderer rejects at the crossing stage and does not run `applyCliffConnections` at all. Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 2bc07da commit 961a6fb

3 files changed

Lines changed: 360 additions & 0 deletions

File tree

docs/noise/vulcanus-cliffs-NOTES.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3085,3 +3085,87 @@ resource control actually changes, not by growing a rectangle.
30853085
- A spurious dependence through `CompiledMapGenSettings` (the lever perturbing
30863086
the cliff field rather than the ore suppressing it) is not excluded either,
30873087
though #99's spatial locality argues against it.
3088+
3089+
## Four of the ten ore remainders are the `onDestroy` CASCADE (2026-08-03, #84)
3090+
3091+
The section above closed entity collision as the ore mechanism and said not to
3092+
widen the box. This is what to do instead, and it needed no new capture and no
3093+
new parameter.
3094+
3095+
`test/cliffOreRemainderCascade.spec.ts`.
3096+
3097+
### The refutation on file was about a DIFFERENT cascade
3098+
3099+
`vulcanusOreRejection.ts` records box overlap as explaining 21 of the 31 cells
3100+
the ore suppresses, with the other ten as "run remainders" and two candidates -
3101+
"a cascade along cliff connections **or** a wider box" - and records the cascade
3102+
half as REFUTED.
3103+
3104+
**That refutation tested #108's CROSSING-stage mechanism**: a rejection zeroes
3105+
the cell's edge registers, a neighbour's code changes, re-test to a fixpoint.
3106+
Its conclusion - "rejected cells do not turn neighbours rejectable" - is still
3107+
true and is not what is being overturned.
3108+
3109+
The `applyCliffs` cascade is a different thing: `Cliff::onDestroy` takes the
3110+
facing end of every connected neighbour, and a neighbour left with **no end at
3111+
all** is destroyed outright. It was read out of the binary in **#113, after
3112+
#110 ran**, and nothing had re-run the remainder question against it.
3113+
3114+
### Replayed entirely on the game's own data
3115+
3116+
What makes this worth trusting is that no part of the port's field, ore model or
3117+
geyser roll appears in it:
3118+
3119+
- **start** from the game's `ALL resources OFF` cliff set - 892 cells, the
3120+
game's own orientations;
3121+
- **destroy** the cells whose base collision box overlaps one of the **game's
3122+
own** resource entity positions, at the prototype half-extents the fixture
3123+
itself carries;
3124+
- **compare** against the game's `resources ON` set - 861 cells.
3125+
3126+
| arm | matched | wrong | surplus |
3127+
| --- | --- | --- | --- |
3128+
| control - destroy the lever's own 31 | **861** | **0** | **0** |
3129+
| direct overlap only, no cascade | 856 | 5 | 10 |
3130+
| direct overlap + `onDestroy` cascade | **859** | **2** | **6** |
3131+
3132+
Direct overlap fires on **21** cells and every one is in the lever's 31 - zero
3133+
false positives, precision 1.000, re-derived without any of our own data.
3134+
3135+
**Recall on the lever's 31: 21/31 = 0.677 -> 25/31 = 0.806**, with no new
3136+
parameter and no wider box. `missing` is 0 in both arms, so no cell the game
3137+
kept is ever removed - which is exactly what distinguishes a mechanism from a
3138+
bigger rectangle. A box wide enough to reach those four would also reach cells
3139+
the game kept, and #124 established that the box is not the engine's collision
3140+
test to begin with.
3141+
3142+
### The control is load-bearing, and so is one flag
3143+
3144+
The control says the ore-off world minus those 31 cells IS the ore-on world,
3145+
orientations included. Without it none of the other rows mean anything.
3146+
3147+
Getting it exact needs `noUpdateConnections: true`: **the game's dumped set is
3148+
POST-pipeline**, so running `updateConnections` over it again double-applies the
3149+
pass and trims ends that legitimately survived. Forget it and the control scores
3150+
`wrong = 13` - which reads like a defect in the cascade model and is an artifact
3151+
of the harness. Any future experiment that starts from a dumped set rather than
3152+
from our own queue has the same hazard.
3153+
3154+
### The six that remain
3155+
3156+
```
3157+
1546,1550.5 1546,1554.5 1606,1590.5 1606,1594.5 1622,1614.5 1626,1614.5
3158+
```
3159+
3160+
Two of them - `1546,1550.5` and `1546,1554.5` - are the pair attributed to the
3161+
geyser, and the first is the one cell in this entire residual whose destruction
3162+
is directly witnessed by the game's own orientations (#124's n=1). So the
3163+
best-evidenced destruction in the whole investigation is still unexplained by
3164+
any rule.
3165+
3166+
### What this does NOT change
3167+
3168+
Nothing shipping. The renderer rejects at the crossing stage and does not run
3169+
`applyCliffConnections` at all - see `## The queue has a CONSUMER` for why that
3170+
is deliberate. This is a statement about the RULE, and about which of its
3171+
remainders now have a mechanism.

src/noise/cliffs/vulcanusOreRejection.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,21 @@
9494
* "a cascade along cliff connections", and `rejectionCascades` measures it:
9595
* a bit-for-bit no-op at the shipping settings, and net harmful on the
9696
* collapsed rule. Rejected cells do not turn neighbours rejectable.
97+
*
98+
* **That refutation is about the CROSSING-stage cascade, and it does not
99+
* cover the one in `applyCliffs`** - `Cliff::onDestroy` taking the facing
100+
* end of every connected neighbour and destroying a neighbour left with no
101+
* end at all. That mechanism was read out of the binary later, in #113, so
102+
* nothing had re-run the remainder question against it.
103+
* `test/cliffOreRemainderCascade.spec.ts` does, entirely on the game's own
104+
* data - its ore-off cliff set, its resource positions, these prototype
105+
* boxes - and it closes **four of the ten** remainders and three of the
106+
* five orientation errors, at zero cost in precision. Recall on the
107+
* lever's 31 goes 21/31 to **25/31**. Six remain.
108+
*
109+
* So read the line above as "rejected cells do not turn neighbours
110+
* REJECTABLE", which is still true, and not as "the remainders are not a
111+
* cascade" - four of them are.
97112
* - **The crossing STAGE explains 2 of the remainders with no tuning at all.**
98113
* The predicate fires on 20 placed cells; the placement loses 22, because
99114
* zeroing a rejected cell's edges leaves two neighbours with codes that no
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { describe, expect, it } from "vite-plus/test";
2+
3+
import ore from "./fixtures/oracle-vulcanus-cliff-ore-direction.seed123456.json";
4+
import { CLIFF_ORIENTATION_NAMES } from "../src/noise/cliffs/cliffCatalog";
5+
import {
6+
applyCliffConnections,
7+
cliffCodeForOrientation,
8+
} from "../src/noise/cliffs/cliffConnections";
9+
import {
10+
VULCANUS_CLIFF_BASE_COLLISION_BOX,
11+
VULCANUS_GEYSER_COLLISION_HALF,
12+
VULCANUS_ORE_COLLISION_HALF,
13+
} from "../src/noise/cliffs/vulcanusOreRejection";
14+
15+
/**
16+
* **Four of the ten ore "run remainders" are the `Cliff::onDestroy` cascade, and
17+
* nobody had tested that because the cascade did not exist yet when they were
18+
* measured** (#84).
19+
*
20+
* `vulcanusOreRejection.ts` records that box overlap accounts for 21 of the 31
21+
* cells the ore suppresses and that the other ten are "run remainders", with two
22+
* candidate explanations - "a cascade along cliff connections **or** a wider
23+
* box" - and it records the cascade half as REFUTED by `cliffOreCascade.spec.ts`.
24+
*
25+
* **That refutation is about a different cascade.** It tested #108's
26+
* CROSSING-stage mechanism: a rejection zeroes the cell's edge registers, a
27+
* neighbour's code changes, re-test to a fixpoint. The `applyCliffs` cascade -
28+
* `Cliff::onDestroy` taking the facing end of every connected neighbour, and
29+
* destroying a neighbour left with no end at all - was only read out of the
30+
* binary later, in #113. Nothing re-ran the remainder question against it.
31+
*
32+
* Running it closes four of the ten, at **zero** cost in precision.
33+
*
34+
* ## The experiment uses only the GAME's data on both sides
35+
*
36+
* This is what makes it worth trusting: nothing of the port's own field, ore
37+
* model or geyser roll appears anywhere in it.
38+
*
39+
* - **Start** from the game's `ALL resources OFF` cliff set - 892 cells with the
40+
* game's own orientations.
41+
* - **Destroy** the cells whose base collision box overlaps one of the game's
42+
* own resource entity positions, with the prototype half-extents the fixture
43+
* itself carries.
44+
* - **Compare** against the game's `resources ON` set - 861 cells.
45+
*
46+
* The only modelled things are the overlap rule and the ported cascade.
47+
*
48+
* | arm | matched | wrong | surplus |
49+
* | --- | --- | --- | --- |
50+
* | control - destroy the lever's own 31 | **861** | **0** | **0** |
51+
* | direct overlap only, no cascade | 856 | 5 | 10 |
52+
* | direct overlap + `onDestroy` cascade | **859** | **2** | **6** |
53+
*
54+
* The control is what validates the whole setup: the ore-off world minus those
55+
* 31 cells IS the ore-on world, orientations included. Getting that exactly is
56+
* also why the second `updateConnections` pass has to be suppressed - the game's
57+
* dumped set is POST-pipeline, so running the pass again double-applies it and
58+
* trims ends that legitimately survive (it scores `wrong = 13` if you forget).
59+
*
60+
* Recall on the lever's 31 goes **21/31 = 0.677 to 25/31 = 0.806** with no new
61+
* parameter and no wider box - which matters because #124 established that
62+
* widening the box would be fitting a shape to an effect the engine's collision
63+
* system provably does not produce.
64+
*/
65+
66+
const nameToId = new Map(CLIFF_ORIENTATION_NAMES.map((n, i) => [n, i]));
67+
const K = (x: number, y: number): string => `${String(x)},${String(y)}`;
68+
69+
interface Ent {
70+
x: number;
71+
y: number;
72+
name: string;
73+
orientation?: string;
74+
}
75+
interface Region {
76+
x0: number;
77+
y0: number;
78+
x1: number;
79+
y1: number;
80+
}
81+
82+
const oreCases = ore.cases as unknown as {
83+
label: string;
84+
region: Region;
85+
cliffs: Ent[];
86+
resources: Ent[];
87+
}[];
88+
const arm = (label: string): { label: string; region: Region; cliffs: Ent[]; resources: Ent[] } => {
89+
const c = oreCases.find((q) => q.label === label);
90+
if (c === undefined) throw new Error(`no arm ${label}`);
91+
return c;
92+
};
93+
const cliffMap = (label: string): Map<string, number> => {
94+
const c = arm(label);
95+
const m = new Map<string, number>();
96+
for (const e of c.cliffs)
97+
if (
98+
e.name === "cliff-vulcanus" &&
99+
e.x >= c.region.x0 &&
100+
e.x < c.region.x1 &&
101+
e.y >= c.region.y0 &&
102+
e.y < c.region.y1
103+
) {
104+
const id = nameToId.get(e.orientation ?? "");
105+
if (id !== undefined) m.set(K(e.x, e.y), id);
106+
}
107+
return m;
108+
};
109+
110+
const ON = cliffMap("entity region, resources ON");
111+
const ALL_OFF = cliffMap("entity region, ALL resources OFF");
112+
const REG = arm("entity region, resources ON").region;
113+
const inR = (p: { x: number; y: number }): boolean =>
114+
p.x >= REG.x0 && p.x < REG.x1 && p.y >= REG.y0 && p.y < REG.y1;
115+
const RES = arm("entity region, resources ON").resources;
116+
const SUPPRESSED = new Set([...ALL_OFF.keys()].filter((k) => !ON.has(k)));
117+
118+
/** The game's own ore-off cliffs, as an `applyCliffConnections` input. */
119+
const START = [...ALL_OFF.entries()].map(([k, o]) => {
120+
const [xs, ys] = k.split(",");
121+
return { x: Number(xs), y: Number(ys), code: cliffCodeForOrientation(o) };
122+
});
123+
124+
const [bl, bt, br, bb] = VULCANUS_CLIFF_BASE_COLLISION_BOX;
125+
const halfOf = (name: string): number =>
126+
name === "sulfuric-acid-geyser" ? VULCANUS_GEYSER_COLLISION_HALF : VULCANUS_ORE_COLLISION_HALF;
127+
128+
/** Bucketed so the overlap test is not 892 x 3933. */
129+
const BUCKET = 8;
130+
const BUCKETS = new Map<string, Ent[]>();
131+
for (const r of RES) {
132+
const k = K(Math.floor(r.x / BUCKET), Math.floor(r.y / BUCKET));
133+
const a = BUCKETS.get(k);
134+
if (a === undefined) BUCKETS.set(k, [r]);
135+
else a.push(r);
136+
}
137+
138+
/** Does the cell's base box overlap any of the GAME's resource entity boxes? */
139+
const overlapsGameResource = (cx: number, cy: number): boolean => {
140+
const left = cx + bl;
141+
const top = cy + bt;
142+
const right = cx + br;
143+
const bottom = cy + bb;
144+
for (let gx = Math.floor((left - 2) / BUCKET); gx <= Math.floor((right + 2) / BUCKET); gx++)
145+
for (let gy = Math.floor((top - 2) / BUCKET); gy <= Math.floor((bottom + 2) / BUCKET); gy++) {
146+
const a = BUCKETS.get(K(gx, gy));
147+
if (a === undefined) continue;
148+
for (const r of a) {
149+
const h = halfOf(r.name);
150+
if (r.x - h < right && left < r.x + h && r.y - h < bottom && top < r.y + h) return true;
151+
}
152+
}
153+
return false;
154+
};
155+
156+
const DIRECT = new Set(START.filter((c) => overlapsGameResource(c.x, c.y)).map((c) => K(c.x, c.y)));
157+
158+
interface Score {
159+
matched: number;
160+
wrong: number;
161+
surplus: number;
162+
missing: number;
163+
}
164+
const run = (kill: Set<string>, noCascade: boolean): { score: Score; left: string[] } => {
165+
const out = applyCliffConnections(START, {
166+
collides: (_o, x, y) => kill.has(K(x, y)),
167+
noCascade,
168+
// The dumped set is POST-pipeline; running `updateConnections` again would
169+
// double-apply it. See the module comment.
170+
noUpdateConnections: true,
171+
});
172+
const survivors = new Map(out.filter(inR).map((p) => [K(p.x, p.y), p.orientation] as const));
173+
const score: Score = { matched: 0, wrong: 0, surplus: 0, missing: 0 };
174+
for (const [k, id] of survivors) {
175+
const t = ON.get(k);
176+
if (t === undefined) score.surplus++;
177+
else if (t === id) score.matched++;
178+
else score.wrong++;
179+
}
180+
for (const k of ON.keys()) if (!survivors.has(k)) score.missing++;
181+
return { score, left: [...SUPPRESSED].filter((k) => survivors.has(k)) };
182+
};
183+
184+
describe("the ore lever, replayed entirely on the game's own data", () => {
185+
/**
186+
* **The control that validates the harness.** The ore-off world minus the 31
187+
* cells the lever attributes to the ore is exactly the ore-on world -
188+
* positions and orientations. If this ever stops being 861/0/0/0 the arms
189+
* below mean nothing.
190+
*/
191+
it("reproduces the resources-ON world exactly from the OFF world", () => {
192+
expect(ALL_OFF.size).toBe(892);
193+
expect(ON.size).toBe(861);
194+
expect(SUPPRESSED.size).toBe(31);
195+
expect(run(SUPPRESSED, false).score).toEqual({
196+
matched: 861,
197+
wrong: 0,
198+
surplus: 0,
199+
missing: 0,
200+
});
201+
}, 300000);
202+
203+
/**
204+
* Box overlap against the game's own resource positions fires on 21 cells and
205+
* **every one of them is in the lever's set** - precision 1.000 with nothing
206+
* of ours in the measurement. That is the half of `vulcanusOreRejection.ts`'s
207+
* rule that was never in doubt, re-derived without the port's ore field or
208+
* geyser roll.
209+
*/
210+
it("finds 21 directly overlapped cells and no false positives", () => {
211+
expect(DIRECT.size).toBe(21);
212+
expect([...DIRECT].filter((k) => !SUPPRESSED.has(k))).toEqual([]);
213+
}, 300000);
214+
});
215+
216+
describe("the onDestroy cascade explains four of the ten remainders", () => {
217+
/**
218+
* The comparison that carries the finding. Destroying the same 21 cells
219+
* differs only in whether `Cliff::onDestroy` runs, and the cascade removes
220+
* four more - each one a cell left with no end at all once its neighbours'
221+
* facing ends went - plus three of the five orientation errors.
222+
*
223+
* **No cell the lever keeps is ever removed**, in either arm, so this costs no
224+
* precision. That is the property that distinguishes a mechanism from a wider
225+
* box: a box big enough to reach these four would also reach cells the game
226+
* kept.
227+
*/
228+
it("takes the remainder from 10 to 6 with no precision cost", () => {
229+
const withoutCascade = run(DIRECT, true);
230+
const withCascade = run(DIRECT, false);
231+
232+
expect(withoutCascade.score).toEqual({ matched: 856, wrong: 5, surplus: 10, missing: 0 });
233+
expect(withCascade.score).toEqual({ matched: 859, wrong: 2, surplus: 6, missing: 0 });
234+
235+
// Recall on the lever's own 31, before and after.
236+
expect((31 - withoutCascade.left.length) / 31).toBeCloseTo(0.677, 3);
237+
expect((31 - withCascade.left.length) / 31).toBeCloseTo(0.806, 3);
238+
239+
// ...and neither arm removes a cell the game kept - `missing` is 0 in both.
240+
expect(withoutCascade.score.missing).toBe(0);
241+
expect(withCascade.score.missing).toBe(0);
242+
}, 300000);
243+
244+
/**
245+
* The six that remain, pinned as the input to whatever comes next. Two of them
246+
* (`1546,1550.5`, `1546,1554.5`) are the pair that #123 attributed to the
247+
* geyser and #124 used for its n=1 destroy-stage proof - so the one cell in
248+
* this whole residual whose destruction is directly witnessed by the game's
249+
* own orientations is still unexplained by any rule.
250+
*/
251+
it("pins the six the cascade does not reach", () => {
252+
expect(run(DIRECT, false).left.sort((a, b) => a.localeCompare(b))).toEqual([
253+
"1546,1550.5",
254+
"1546,1554.5",
255+
"1606,1590.5",
256+
"1606,1594.5",
257+
"1622,1614.5",
258+
"1626,1614.5",
259+
]);
260+
}, 300000);
261+
});

0 commit comments

Comments
 (0)