Skip to content

Commit f484fdc

Browse files
wormeymanclaude
andcommitted
feat(cliffs): the rejections act on the CROSSING, not on the entity (#84)
#107 handed over one untested hypothesis with its control named: "disputed edges sit adjacent to rejected cells above the base rate of all crossing edges". Run, it is not an enrichment over a base rate - it is a dichotomy, and it refutes the STAGE at which the port applies both Vulcanus cliff rejections. Of the 1235 edges the port has and the game does not, **1233** sit against a cell the game did not emit. Of the 36,103 in-region edges both sides agree on, **0** do. A cell's edge register is the same array slot as its neighbour's (#103), so the game's absences take their crossings with them. **The refutation is structural, not a score.** A pure post-filter makes a falsifiable prediction: when cell N is rejected, its surviving neighbour C still holds the shared crossing and is emitted carrying it. Counted with the port's own rejection predicate over the fine sweep's 41 levels, that should happen **1,662 times**. The game does it **0 times**. The vacuity arm is the identical counter reading the port's own post-filter output, where it fires on all 1,662 - so the zero is a property of the game's cliffs, not a dead branch. `tryToAddCliff` really does ignore `wouldCollide`'s return value; the #71/#73 disassembly reading was not misread. What is refuted is the conclusion drawn from it - "and therefore no effect on the neighbouring cells". Both doc comments that asserted it are corrected rather than left standing. `rejectAtCrossingStage` zeroes a rejected cell's four edge registers after the repair sweep, before any code is read. Collapsed rule: wrong orientations **1235 -> 693**, surplus 1366 -> 1200, matched 18130 -> 18654. At the SHIPPING settings across the three entity regions: wrong orientations **33 -> 21**, precision 0.9839 -> 0.9858, and the matched set IDENTICAL at 1525 - it removes wrong edges and costs no recall. Not free: 18 more of the game's cells go missing under the collapsed rule, because an edge taken off a survivor can leave its code non-placing. Reported rather than buried. Nauvis is byte-identical either way (it already matched 1.0000 both directions), so this is not a regression risk there and not corroboration from there either. Tiling stays byte-identical - the zeroing runs over the whole chunk, including cells outside the query box - and `tiledEquality.spec.ts`'s Vulcanus cliff seam and chunk-boundary cases verify it rather than the comment asserting it. What is left is the PREDICATE, not the stage: 693 wrong and 1200 surplus survive, and the sweep cannot measure the field where the game emits nothing at any level. Item of #84; the issue stays open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExK3ASs5i2KMm9NwWqnZuj
1 parent b0aa30c commit f484fdc

4 files changed

Lines changed: 487 additions & 8 deletions

File tree

docs/noise/vulcanus-cliffs-NOTES.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,3 +1985,81 @@ neighbourhood of a rejected cell. Note this is a HYPOTHESIS from one chunk plus
19851985
the coverage statistics; it has not been tested, and the obvious control is
19861986
whether disputed edges are adjacent to rejected cells at a rate above the base
19871987
rate of all crossing edges.
1988+
1989+
## The rejections act on the CROSSING, not on the entity (2026-08-03, #84)
1990+
1991+
#107 handed over a hypothesis - "disputed edges sit adjacent to rejected cells
1992+
above the base rate of all crossing edges" - and named the control. Run, it is
1993+
not an enrichment over a base rate. It is a dichotomy, and it refutes the stage
1994+
at which the port applies both Vulcanus cliff rejections.
1995+
1996+
`test/vulcanusCliffRejectionStage.spec.ts`, over the fine sweep's 41 levels at
1997+
`[1500,1500]`:
1998+
1999+
| | |
2000+
| --- | --- |
2001+
| edges the port has and the game does not | **1235** |
2002+
| ... sitting against a cell the game did not emit | **1233** |
2003+
| edges both sides agree on (in region) | **36103** |
2004+
| ... sitting against a cell the game did not emit | **0** |
2005+
2006+
Zero, not "fewer". A cell's edge register is the same array slot as its
2007+
neighbour's (#103), so this says the game's absences take their crossings with
2008+
them, every time.
2009+
2010+
### The refutation is structural, not a score
2011+
2012+
The post-filter reading makes a falsifiable prediction, and it does not need any
2013+
model to score well. When cell `N` is rejected, a pure post-filter leaves the
2014+
shared crossing in `C`'s code, so `C` is emitted still carrying it. Counted with
2015+
the port's own rejection predicate, that should happen **1,662 times** across the
2016+
41 levels. The game does it **0 times**.
2017+
2018+
The vacuity arm is the same counter reading the port's own post-filter output
2019+
instead of the game's: it fires on all 1,662. So the zero is a property of the
2020+
game's cliffs, not a dead branch. (A first attempt at a vacuity arm compared
2021+
against the *next* level's game output and also returned 0 - not a broken
2022+
control but a stronger result, since the invariant holds independently at all 41
2023+
levels.)
2024+
2025+
Note what this does and does not settle. `tryToAddCliff` really does ignore
2026+
`wouldCollide`'s return value - the disassembly reading behind #71/#73 was not
2027+
misread. What is refuted is that the OBSERVABLE output behaves like a
2028+
post-filter. Whether `wouldCollide` removes the crossings or the game never
2029+
computed them there is not decidable from entity dumps, and the port now models
2030+
the first because it is the one that can be written.
2031+
2032+
### The fix, and what it is worth
2033+
2034+
`rejectAtCrossingStage` in `cliffPlacement.ts` zeroes a rejected cell's four edge
2035+
registers after the repair sweep, before any code is read. Scored two ways:
2036+
2037+
| | matched | wrong | surplus | missing |
2038+
| --- | --- | --- | --- | --- |
2039+
| collapsed rule, post-filter | 18130 | 1235 | 1366 | 85 |
2040+
| collapsed rule, crossing stage | **18654** | **693** | **1200** | 103 |
2041+
2042+
and at the SHIPPING settings (smoothing 1, the real 120-tile interval,
2043+
`cliffiness_basic` rather than a constant) across the three entity regions:
2044+
wrong orientations **33 -> 21**, precision 0.9839 -> 0.9858, and the matched set
2045+
**identical** at 1525 - this removes wrong edges and costs no recall at all.
2046+
2047+
It is not free: 18 more of the game's cells go missing under the collapsed rule,
2048+
because an edge taken off a survivor can leave its code non-placing. Reported
2049+
rather than buried.
2050+
2051+
Nauvis is unaffected in both directions - it already matched 1.0000 recall and
2052+
precision, and enabling the flag there reproduces the same 282 and 52 cells with
2053+
0 wrong orientations. So this is not a regression risk on the other planet, but
2054+
neither is it corroboration from it: Nauvis has no cells that could move.
2055+
2056+
### What is left
2057+
2058+
The predicate, not the stage. 693 wrong orientations and 1200 surplus cells
2059+
survive under the collapsed rule, and the remaining absences still follow the
2060+
same "the game emitted nothing there" shape - so the lava-box + ore predicate is
2061+
catching most of what the game suppresses but not all of it. Chasing that is a
2062+
question about WHICH cells the game refuses, and the sweep cannot answer it
2063+
where the game emits nothing at any level: those corners get no bracket, so the
2064+
field there is still unmeasured. A wider sweep (well outside `[700,900]`) is the
2065+
measurement that would close that gap.

src/noise/cliffs/cliffPlacement.ts

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,13 @@ export interface CliffBands {
100100
* orientation, takes that orientation's `collision_bounding_box`, and calls
101101
* `wouldCollide` (`0x101625468`) against the tile mask grid; on a hit the
102102
* cliff is simply **not added**. `generateCliffs` ignores the return value
103-
* entirely - there is no retry, no alternative orientation, and no effect on
104-
* the neighbouring cells - so this is a pure post-filter on the emit loop,
105-
* which is why it can run per-cell in any order and leaves tiling
106-
* byte-identical.
103+
* entirely - there is no retry and no alternative orientation.
104+
*
105+
* That reading of the code is unchanged, but the conclusion drawn from it -
106+
* "and therefore no effect on the neighbouring cells" - **is refuted by the
107+
* game's output**, so see `rejectAtCrossingStage` below before assuming this
108+
* is a post-filter. The observable behaviour is that a rejected cell's
109+
* crossings go with it and its neighbour's orientation changes.
107110
*
108111
* Which tiles collide is planet-specific but the rule is not: a tile collides
109112
* when its `CollisionMask` shares a layer with the cliff's. The cliff mask
@@ -132,10 +135,26 @@ export interface CliffBands {
132135
* directly, so a filter applied further out would score a different thing than
133136
* it renders.
134137
*
135-
* Like `tileCollides` this is a pure post-filter on the emit loop - it cannot
136-
* affect a neighbouring cell, so it leaves worker tiling byte-identical.
138+
* Like `tileCollides`, whether this acts as a post-filter or on the crossing
139+
* itself is `rejectAtCrossingStage`'s decision, and the measurement says the
140+
* crossing. Either way it stays chunk-local - the zeroing runs over the whole
141+
* chunk, including cells outside the query box - so worker tiling remains
142+
* byte-identical.
137143
*/
138144
readonly cellRejects?: (code: number, x: number, y: number) => boolean;
145+
/**
146+
* EXPERIMENTAL (#84): apply `tileCollides` / `cellRejects` by zeroing the
147+
* rejected cell's four edge registers after the repair sweep, instead of
148+
* filtering the emitted cell. A neighbour sharing one of those edges therefore
149+
* loses it too, and its orientation changes.
150+
*
151+
* The post-filter reading came from `tryToAddCliff` ignoring `wouldCollide`'s
152+
* return value, and it is REFUTED as a description of the observable output:
153+
* see `test/vulcanusCliffRejectionStage.spec.ts`. Under a post-filter, a
154+
* surviving cell keeps an edge whose neighbour was rejected; the game shows
155+
* that 0 times where the model predicts 1,662.
156+
*/
157+
readonly rejectAtCrossingStage?: boolean;
139158
}
140159

141160
/** Cells per chunk axis: a 32-tile chunk over the 4-tile placement grid. */
@@ -487,6 +506,35 @@ export function makeCliffPlacementFromFields(
487506

488507
fixImpossibleCellsSweep(v, hEdges, n, n);
489508

509+
if (bands.rejectAtCrossingStage === true) {
510+
// Collect first, then clear: a cell's rejection is decided from
511+
// the code the repair left, not from a code a previous cell's
512+
// clearing has already eaten into.
513+
const kill: number[] = [];
514+
for (let cy = 0; cy < n; cy++) {
515+
for (let cx = 0; cx < n; cx++) {
516+
const code = cellCode(
517+
v[cy * (n + 1) + cx],
518+
v[cy * (n + 1) + cx + 1],
519+
hEdges[cy * n + cx],
520+
hEdges[(cy + 1) * n + cx],
521+
);
522+
if (!isCliffPlaced(code)) continue;
523+
const x = (baseX + cx) * CLIFF_GRID_SIZE + CLIFF_CELL_CENTER_X;
524+
const y = (baseY + cy) * CLIFF_GRID_SIZE + CLIFF_CELL_CENTER_Y;
525+
if (rejected(code, x, y) || cellRejects?.(code, x, y) === true) kill.push(cx, cy);
526+
}
527+
}
528+
for (let i = 0; i < kill.length; i += 2) {
529+
const cx = kill[i];
530+
const cy = kill[i + 1];
531+
v[cy * (n + 1) + cx] = 0;
532+
v[cy * (n + 1) + cx + 1] = 0;
533+
hEdges[cy * n + cx] = 0;
534+
hEdges[(cy + 1) * n + cx] = 0;
535+
}
536+
}
537+
490538
for (let cy = 0; cy < n; cy++) {
491539
for (let cx = 0; cx < n; cx++) {
492540
const code = cellCode(
@@ -502,8 +550,10 @@ export function makeCliffPlacementFromFields(
502550
// expensive half (it resolves tiles), and a chunk always
503551
// overhangs the query box.
504552
if (x < x0 || x >= x1 || y < y0 || y >= y1) continue;
505-
if (rejected(code, x, y)) continue;
506-
if (cellRejects?.(code, x, y) === true) continue;
553+
if (bands.rejectAtCrossingStage !== true) {
554+
if (rejected(code, x, y)) continue;
555+
if (cellRejects?.(code, x, y) === true) continue;
556+
}
507557
result.push({ x, y, code });
508558
}
509559
}

src/noise/preview/renderVulcanusCliffs.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ export function renderVulcanusCliffs(base: ImageData, opts: RenderVulcanusCliffs
109109
smoothing: VULCANUS_CLIFF_SMOOTHING,
110110
tileCollides: (x, y) => VULCANUS_CLIFF_BLOCKING_TILES.has(tileAt(x, y).name),
111111
cellRejects: makeVulcanusOreRejection(resources, ctx.vulcanusResourceControls),
112+
// Both rejections act on the CROSSING, not on the emitted entity (#84).
113+
// A rejected cell's four edges go with it, so a surviving neighbour loses
114+
// the shared one and changes orientation. See
115+
// `test/vulcanusCliffRejectionStage.spec.ts`: the post-filter reading
116+
// predicts 1,662 cases of a survivor keeping such an edge and the game
117+
// shows 0. Worth 33 -> 21 wrong orientations at no cost in recall.
118+
rejectAtCrossingStage: true,
112119
});
113120

114121
const box = opts.cellQueryBox ?? {

0 commit comments

Comments
 (0)