Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/noise/vulcanus-cliffs-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2127,3 +2127,69 @@ smoothing, the repair, and - since #108 - the STAGE. What remains is which cells
the game refuses, and the honest statement is that the list is open: the same
trap #106 fell into is available here, so the next candidate needs a positive
measurement rather than promotion by elimination.

## The ore rule, scored against the lever - and the cascade refuted (2026-08-03, #84)

`oracle-vulcanus-cliff-ore-direction` re-ran `[1500,1500]` with the resources
switched off through `autoplace_controls`, so the ore's effect is a known SET of
cells rather than an inference. Scoring the port's predicate against it gives a
precision and a recall instead of a total to match.
`test/cliffOreCascade.spec.ts`.

| | |
| --- | --- |
| game, resources ON / ALL OFF (in region) | 861 / 892 |
| cells the ore suppresses | **31** |
| cells that APPEAR when ore is added | **0** (the one-way property of #99, re-confirmed on the entity region) |
| cells merely re-coded | 5 |
| our model suppresses | **22**, of which genuinely suppressed **22** |
| | **precision 1.000, recall 0.710** |

**The rule is exactly right where it fires and simply too narrow.** That is a
much more useful statement than "it explains 21 of 31", because it says which
direction is safe to move in.

Attribution of the 31, from the per-control arms: **27 calcite, 4 geyser, 0
tungsten/coal**. Of the 9 our predicate misses, **4 are geyser** cells that
`includeGeyser: false` deliberately excludes (the geyser rolls, so including it
costs precision - measured, still harmful), 5 are calcite, and **all 9 are
adjacent to another suppressed cell**.

### The crossing stage explains 2 remainders for free

The predicate fires on **20** placed cells; the placement loses **22**. The extra
two are neighbours left with a non-placing code after a rejected cell's edges
were zeroed (#108). No tuning - it falls out of the mechanism, and it is the
first thing to reduce the remainder count since the rule was characterised.

### The cascade half of the open question is REFUTED

`vulcanusOreRejection.ts` left it as "a cascade along cliff connections **or** a
wider box". #108 makes the cascade concrete: zeroing a cell's edges changes a
neighbour's code, hence its orientation, hence its collision box, so re-testing
to a fixpoint is exactly that cascade. `rejectionCascades` is the arm.

| | matched | wrong | surplus | missing |
| --- | --- | --- | --- | --- |
| collapsed rule, one pass | 18654 | 693 | 1200 | 103 |
| collapsed rule, fixpoint | 18640 | 700 | 1196 | 110 |

At the SHIPPING settings it is **bit-for-bit identical** - and the collapsed-rule
row is what makes that a result rather than an untriggered branch. A rejected
cell never turns a neighbour into a rejectable orientation.

That leaves the wider-box half, which is the one #88 says must not be tuned into
fitting.

### Half of `[1500,1500]`'s residual is not ore at all

Running BOTH sides with the resources off isolates it:

| | matched | wrong | surplus | missing |
| --- | --- | --- | --- | --- |
| resources ON, both sides | 842 | 16 | 19 | 3 |
| resources OFF, both sides | 876 | **13** | **10** | 3 |

So 13 wrong orientations and 10 surplus cells survive with the ore entirely out
of the picture. Tuning the ore rule cannot reach them, and that non-ore half is
the larger target now.
64 changes: 43 additions & 21 deletions src/noise/cliffs/cliffPlacement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ export interface CliffBands {
* that 0 times where the model predicts 1,662.
*/
readonly rejectAtCrossingStage?: boolean;
/**
* With `rejectAtCrossingStage`, re-run the rejection pass until it finds
* nothing, so a cell whose ORIENTATION changed because a neighbour's edges
* were zeroed is re-tested with its new collision box.
*
* This is the "cascade along cliff connections" half of the open question in
* `vulcanusOreRejection.ts` - the other half being a wider box. Off by
* default; `test/cliffOreCascade.spec.ts` is what decides it.
*/
readonly rejectionCascades?: boolean;
}

/** Cells per chunk axis: a 32-tile chunk over the 4-tile placement grid. */
Expand Down Expand Up @@ -510,28 +520,40 @@ export function makeCliffPlacementFromFields(
// Collect first, then clear: a cell's rejection is decided from
// the code the repair left, not from a code a previous cell's
// clearing has already eaten into.
const kill: number[] = [];
for (let cy = 0; cy < n; cy++) {
for (let cx = 0; cx < n; cx++) {
const code = cellCode(
v[cy * (n + 1) + cx],
v[cy * (n + 1) + cx + 1],
hEdges[cy * n + cx],
hEdges[(cy + 1) * n + cx],
);
if (!isCliffPlaced(code)) continue;
const x = (baseX + cx) * CLIFF_GRID_SIZE + CLIFF_CELL_CENTER_X;
const y = (baseY + cy) * CLIFF_GRID_SIZE + CLIFF_CELL_CENTER_Y;
if (rejected(code, x, y) || cellRejects?.(code, x, y) === true) kill.push(cx, cy);
//
// `rejectionCascades` re-runs that to a fixpoint. Zeroing a cell's
// edges changes its neighbours' codes, and a changed code is a
// changed ORIENTATION, so a neighbour can become rejectable when
// it was not before. Whether the game does that is a measurement,
// not a deduction - see `cliffOreCascade.spec.ts`.
for (let pass = 0; ; pass++) {
const kill: number[] = [];
for (let cy = 0; cy < n; cy++) {
for (let cx = 0; cx < n; cx++) {
const code = cellCode(
v[cy * (n + 1) + cx],
v[cy * (n + 1) + cx + 1],
hEdges[cy * n + cx],
hEdges[(cy + 1) * n + cx],
);
if (!isCliffPlaced(code)) continue;
const x = (baseX + cx) * CLIFF_GRID_SIZE + CLIFF_CELL_CENTER_X;
const y = (baseY + cy) * CLIFF_GRID_SIZE + CLIFF_CELL_CENTER_Y;
if (rejected(code, x, y) || cellRejects?.(code, x, y) === true)
kill.push(cx, cy);
}
}
}
for (let i = 0; i < kill.length; i += 2) {
const cx = kill[i];
const cy = kill[i + 1];
v[cy * (n + 1) + cx] = 0;
v[cy * (n + 1) + cx + 1] = 0;
hEdges[cy * n + cx] = 0;
hEdges[(cy + 1) * n + cx] = 0;
for (let i = 0; i < kill.length; i += 2) {
const cx = kill[i];
const cy = kill[i + 1];
v[cy * (n + 1) + cx] = 0;
v[cy * (n + 1) + cx + 1] = 0;
hEdges[cy * n + cx] = 0;
hEdges[(cy + 1) * n + cx] = 0;
}
// One pass is the shipping model; the cascade stops when a pass
// finds nothing, and `pass` is bounded by the cell count anyway.
if (bands.rejectionCascades !== true || kill.length === 0 || pass > 64) break;
}
}

Expand Down
22 changes: 22 additions & 0 deletions src/noise/cliffs/vulcanusOreRejection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,28 @@
* overlapped cell - and whether that is a cascade along cliff connections or
* a wider box is open. Widening the box until all 31 fall out is exactly how
* #88 shipped a wrong model that scored perfectly.
*
* **Half of that is now settled, and the remainder count is down by two**
* (`test/cliffOreCascade.spec.ts`):
*
* - **The cascade half is REFUTED.** #108 established that a rejection zeroes
* the cell's edge registers, so a neighbour's code - hence its orientation,
* hence its collision box - changes. Re-testing to a fixpoint is exactly
* "a cascade along cliff connections", and `rejectionCascades` measures it:
* a bit-for-bit no-op at the shipping settings, and net harmful on the
* collapsed rule. Rejected cells do not turn neighbours rejectable.
* - **The crossing STAGE explains 2 of the remainders with no tuning at all.**
* The predicate fires on 20 placed cells; the placement loses 22, because
* zeroing a rejected cell's edges leaves two neighbours with codes that no
* longer place.
* - Scored against the lever rather than by totals, the rule is
* **precision 1.000, recall 0.710** (22 of 31): exactly right where it
* fires, simply too narrow. Of the 9 it misses, **4 are geyser** cells the
* `includeGeyser` default deliberately excludes, and 5 are calcite; all 9
* are adjacent to another suppressed cell.
*
* That leaves the wider-box half open - and it is the half #88 says must not
* be tuned into fitting.
*/
import type { VulcanusResourceControls } from "../eval/ctx";
import type { VulcanusResources } from "../expressions/vulcanusResources";
Expand Down
Loading