Skip to content

Commit 0a85b21

Browse files
wormeymanclaude
andauthored
test(cliffs): four fresh regions - the port generalises, the residual is on chunk borders (#84) (#131)
Two things were stuck at a sample size rather than at an idea. The unexplained population was 14 cells, and every structural test on it landed at 1.4-1.9 sigma against its base rate - what a partition looks like at n=14 whether or not a cause exists. And the shipped accuracy figure was measured on THREE regions, chosen years into this investigation for reasons unrelated to sampling. Four more regions with the ore lever, ~2.5s a capture. THE PORT GENERALISES, measured where it was never fitted: | | original 3 | the 4 new | | --- | --- | --- | | raw cells | 1756 | 2789 | | game cliffs | 1531 | 2590 | | raw a strict SUPERSET | yes | yes, all four | | predicate precision | 0.971 | 0.9877 | | predicate recall | 0.889 | 0.8090 | | false rejections | 6 | 2 | [3000,3000] is exact - 362 raw, 362 game, nothing to explain - and is also the one region with no resource entity at all, which is what every result from #123 onward predicts. THE CHUNK-BORDER ENRICHMENT REPLICATES OUT OF SAMPLE: | | unexplained | on border | base rate | | --- | --- | --- | --- | | original 3 | 14 | 9 (64.3%) | 45.0% | | the 4 new | 13 | 9 (69.2%) | 47.2% | | combined | 27 | 18 (66.7%) | ~46.3% | Read the status before the number. The replication is 1.59 sigma alone and the combined figure ~2.1 - short of decisive, and this is a LEAD not a result. What changed is the KIND of evidence: the hypothesis was formed on the original 14, dismissed there as noise at 1.45 sigma, and is tested here on 13 cells captured afterwards in regions chosen before any of them was known. Why it matters: chunk borders are `updateConnections`' entire domain - the one rule that treats border cells differently, which our port measures firing ZERO times and models as an UPPER bound. #122 promoted its gate from inert to load-bearing; #127 showed it cannot be scored from map-gen output. This is the first positive evidence that it does anything. And n is 27 now, which is what unblocks the next attempt: at 14 nothing could be settled; at 27 the border question is answerable with one more capture round. New fixture + PROVENANCE entry. Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9bd98b5 commit 0a85b21

5 files changed

Lines changed: 51971 additions & 0 deletions

File tree

docs/noise/vulcanus-cliffs-NOTES.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3506,3 +3506,66 @@ unfiltered difference is **39**; the real suppression is **31**.
35063506
Every spec in #84 filters to `name === "cliff-vulcanus"` and is therefore
35073507
correct - but that was inherited convention rather than a guarded decision. It is
35083508
guarded now.
3509+
3510+
## Four FRESH regions: the port generalises, and the residual is on CHUNK BORDERS (2026-08-03, #84)
3511+
3512+
Two things were stuck at a sample size rather than at an idea, and both are
3513+
unstuck by ~2.5s captures.
3514+
3515+
`test/cliffResidualMoreRegions.spec.ts`,
3516+
`oracle-vulcanus-cliff-entities-more-regions.seed123456.json`.
3517+
3518+
### The port generalises - measured where it was never fitted
3519+
3520+
| | original 3 regions | the 4 new ones |
3521+
| --- | --- | --- |
3522+
| raw cells | 1756 | 2789 |
3523+
| game cliffs | 1531 | 2590 |
3524+
| raw is a strict SUPERSET | yes | **yes, all four** |
3525+
| destruction predicate precision | 0.971 | **0.9877** |
3526+
| destruction predicate recall | 0.889 | **0.8090** |
3527+
| false rejections | 6 | **2** |
3528+
3529+
`[3000,3000]` is **exact** - 362 raw, 362 game, nothing to explain - and it is
3530+
also the one region with no resource entity at all, which is what every result
3531+
from #123 onward predicts.
3532+
3533+
**The shipped accuracy figure was measured on three regions** chosen years into
3534+
this investigation for reasons unrelated to sampling. It now has four more
3535+
behind it.
3536+
3537+
### The chunk-border enrichment, REPLICATED out of sample
3538+
3539+
| | unexplained | on chunk border | base rate |
3540+
| --- | --- | --- | --- |
3541+
| original 3 regions | 14 | 9 (64.3%) | 45.0% |
3542+
| **the 4 new ones** | 13 | **9 (69.2%)** | 47.2% |
3543+
| combined | **27** | 18 (66.7%) | ~46.3% |
3544+
3545+
**Read the status before the number.** The replication is 1.59 sigma on its own
3546+
and the combined figure about 2.1 - short of decisive, and this is a LEAD rather
3547+
than a result. What changed is not the significance but the kind of evidence:
3548+
the border hypothesis was formed on the original 14, dismissed there as noise at
3549+
1.45 sigma, and is here tested on 13 cells captured **afterwards**, in regions
3550+
chosen before any of them was known. A prediction that survives fresh data is
3551+
worth something the same 64% was worth nothing as.
3552+
3553+
### Why it is worth pursuing
3554+
3555+
**Chunk borders are `updateConnections`' entire domain.** It is the one rule in
3556+
the pipeline that treats border cells differently; our port measures it firing
3557+
**zero** times; and `applyCliffConnections` documents its model of the rule as an
3558+
**upper bound** on how much it removes. #122 promoted the gate from inert to
3559+
load-bearing, and #127 showed the gate cannot be scored from map-generation
3560+
output at all.
3561+
3562+
An enrichment pointing at the same rule from a third, independent direction is
3563+
the first positive evidence that it does anything - after two sections
3564+
establishing only that it is unobservable.
3565+
3566+
### And n is 27 now
3567+
3568+
That is the part that actually unblocks the next attempt. Every structural test
3569+
on this residual - border, orientation, rim distance, anything new - just doubled
3570+
its power. At n = 14 nothing could have been settled; at 27 the border question
3571+
is answerable with one more capture round of the same size.
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
import { describe, expect, it } from "vite-plus/test";
2+
3+
import more from "./fixtures/oracle-vulcanus-cliff-entities-more-regions.seed123456.json";
4+
import { CLIFF_CODE_TO_ORIENTATION, cliffCollisionTileBox } from "../src/noise/cliffs/cliffCatalog";
5+
import { cliffCodeForOrientation, onChunkBorder } from "../src/noise/cliffs/cliffConnections";
6+
import { makeCliffPlacementFromFields } from "../src/noise/cliffs/cliffPlacement";
7+
import {
8+
VULCANUS_CLIFF_ELEVATION_0,
9+
VULCANUS_CLIFF_ELEVATION_INTERVAL,
10+
VULCANUS_CLIFF_SMOOTHING,
11+
makeVulcanusCliffFields,
12+
} from "../src/noise/cliffs/vulcanusCliffFields";
13+
import { makeVulcanusOreRejection } from "../src/noise/cliffs/vulcanusOreRejection";
14+
import { VULCANUS_CLIFF_BLOCKING_TILES } from "../src/noise/preview/renderVulcanusCliffs";
15+
import { buildResources } from "../src/noise/preview/renderVulcanusResources";
16+
import { makeVulcanusTileResolver } from "../src/noise/tiles/vulcanusCatalog";
17+
import { withCtxDefaults } from "../src/noise/eval/ctx";
18+
19+
/**
20+
* **Four fresh regions: the port generalises, and the unexplained cells are
21+
* enriched on CHUNK BORDERS - replicated out of sample** (#84).
22+
*
23+
* Two things were stuck at a sample size rather than at an idea. The residual's
24+
* unexplained population was **14** cells, and every structural test on it -
25+
* chunk-border status, orientation, distance to the region rim - landed at 1.4
26+
* to 1.9 sigma against its base rate, which is what a partition looks like at
27+
* n = 14 whether or not a cause exists. And the shipped accuracy figure was
28+
* measured on **three** regions, chosen years into the investigation for reasons
29+
* that had nothing to do with sampling.
30+
*
31+
* Four more regions, each with the ore lever, at ~2.5s a capture.
32+
*
33+
* ## The port generalises
34+
*
35+
* | | original 3 regions | these 4 |
36+
* | --- | --- | --- |
37+
* | raw cells | 1756 | 2789 |
38+
* | game cliffs | 1531 | 2590 |
39+
* | raw is a strict superset | yes | **yes, all four** |
40+
* | destruction predicate precision | 0.971 | **0.9877** |
41+
* | destruction predicate recall | 0.889 | **0.8090** |
42+
*
43+
* Measured somewhere it was never fitted. `[3000,3000]` is **exact** - 362 raw,
44+
* 362 game, zero residual - and it is also the one region with no resources at
45+
* all, which is consistent with everything #123 to #129 established.
46+
*
47+
* ## The chunk-border enrichment, replicated
48+
*
49+
* | | unexplained | on chunk border | base rate |
50+
* | --- | --- | --- | --- |
51+
* | original 3 regions | 14 | 9 (64.3%) | 45.0% |
52+
* | **these 4** | 13 | **9 (69.2%)** | 47.2% |
53+
* | combined | **27** | 18 (66.7%) | ~46.3% |
54+
*
55+
* **This is a lead, not a result, and the distinction matters.** The replication
56+
* is 1.59 sigma on its own and the combined figure ~2.1 sigma - short of
57+
* decisive. What changed is its STATUS: the border hypothesis was formed on the
58+
* first 14 and is here tested on 13 cells captured afterwards, in regions chosen
59+
* before the cells were known. That is a pre-registered test on fresh data, not
60+
* the post-hoc slice that the same 64% would have been worth nothing as.
61+
*
62+
* Why it is worth pursuing: **chunk borders are `updateConnections`' entire
63+
* domain.** It is the one rule in the whole pipeline that treats border cells
64+
* differently, our port measures it firing zero times, and `applyCliffConnections`
65+
* documents its model of it as an UPPER bound on how much the rule removes.
66+
* #122 promoted its gate from inert to load-bearing; #127 showed the gate cannot
67+
* be scored from map-generation output at all. An enrichment pointing at the
68+
* same rule from a third direction is the first independent evidence that it
69+
* does something.
70+
*
71+
* The unexplained population is now **27**, which is what actually unblocks the
72+
* next person: every structural test just doubled its power.
73+
*/
74+
75+
const INPUT = { seed0: 123456, startingPositions: [{ x: 0, y: 0 }] };
76+
const ctx = withCtxDefaults(INPUT);
77+
const fields = makeVulcanusCliffFields(ctx);
78+
const tileAt = makeVulcanusTileResolver(INPUT);
79+
const oreRejects = makeVulcanusOreRejection(buildResources(ctx), ctx.vulcanusResourceControls);
80+
const K = (x: number, y: number): string => `${String(x)},${String(y)}`;
81+
const BANDS = {
82+
elevation0: VULCANUS_CLIFF_ELEVATION_0,
83+
interval: VULCANUS_CLIFF_ELEVATION_INTERVAL,
84+
smoothing: VULCANUS_CLIFF_SMOOTHING,
85+
};
86+
const isLava = (x: number, y: number): boolean =>
87+
VULCANUS_CLIFF_BLOCKING_TILES.has(tileAt(x, y).name);
88+
89+
interface Ent {
90+
x: number;
91+
y: number;
92+
name: string;
93+
orientation?: string;
94+
}
95+
interface Region {
96+
x0: number;
97+
y0: number;
98+
x1: number;
99+
y1: number;
100+
}
101+
interface Case {
102+
label: string;
103+
region: Region;
104+
effectiveAutoplace: Record<string, { frequency: number; size: number; richness: number }>;
105+
cliffs: Ent[];
106+
resources: Ent[];
107+
}
108+
const CASES = more.cases as unknown as Case[];
109+
110+
interface Score {
111+
label: string;
112+
raw: number;
113+
game: number;
114+
superset: boolean;
115+
oreSuppressed: number;
116+
ourKill: number;
117+
gameKill: number;
118+
agree: number;
119+
falseRejections: number;
120+
missed: number;
121+
ore: number;
122+
unknown: number;
123+
unknownOnBorder: number;
124+
rawOnBorder: number;
125+
}
126+
127+
const SCORES: Score[] = (() => {
128+
const out: Score[] = [];
129+
for (let i = 0; i < CASES.length; i += 2) {
130+
const on = CASES[i];
131+
const off = CASES[i + 1];
132+
const r = on.region;
133+
const inR = (p: { x: number; y: number }): boolean =>
134+
p.x >= r.x0 && p.x < r.x1 && p.y >= r.y0 && p.y < r.y1;
135+
const game = new Set(
136+
on.cliffs.filter((e) => e.name === "cliff-vulcanus" && inR(e)).map((e) => K(e.x, e.y)),
137+
);
138+
const gameOff = new Set(
139+
off.cliffs.filter((e) => e.name === "cliff-vulcanus" && inR(e)).map((e) => K(e.x, e.y)),
140+
);
141+
const oreSuppressed = new Set([...gameOff].filter((k) => !game.has(k)));
142+
const raw = makeCliffPlacementFromFields(fields, BANDS)
143+
.placedCells(r.x0 - 64, r.y0 - 64, r.x1 + 64, r.y1 + 64)
144+
.filter(inR);
145+
146+
const s: Score = {
147+
label: on.label.replace(", resources ON", ""),
148+
raw: raw.length,
149+
game: game.size,
150+
superset: true,
151+
oreSuppressed: oreSuppressed.size,
152+
ourKill: 0,
153+
gameKill: 0,
154+
agree: 0,
155+
falseRejections: 0,
156+
missed: 0,
157+
ore: 0,
158+
unknown: 0,
159+
unknownOnBorder: 0,
160+
rawOnBorder: 0,
161+
};
162+
for (const p of raw) {
163+
const o = CLIFF_CODE_TO_ORIENTATION[p.code];
164+
if (o === undefined) continue;
165+
const k = K(p.x, p.y);
166+
const code = cliffCodeForOrientation(o);
167+
const box = cliffCollisionTileBox(code, p.x, p.y);
168+
let lava = false;
169+
if (box !== undefined)
170+
for (let tx = box.left; tx <= box.right; tx++)
171+
for (let ty = box.top; ty <= box.bottom; ty++) if (isLava(tx, ty)) lava = true;
172+
const ourKill = lava || oreRejects(code, p.x, p.y);
173+
const gameKill = !game.has(k);
174+
if (onChunkBorder(p.x, p.y)) s.rawOnBorder++;
175+
if (ourKill) s.ourKill++;
176+
if (gameKill) s.gameKill++;
177+
if (ourKill && gameKill) s.agree++;
178+
else if (ourKill) s.falseRejections++;
179+
else if (gameKill) {
180+
s.missed++;
181+
if (oreSuppressed.has(k)) s.ore++;
182+
else {
183+
s.unknown++;
184+
if (onChunkBorder(p.x, p.y)) s.unknownOnBorder++;
185+
}
186+
}
187+
}
188+
const have = new Set(raw.map((p) => K(p.x, p.y)));
189+
for (const k of game) if (!have.has(k)) s.superset = false;
190+
out.push(s);
191+
}
192+
return out;
193+
})();
194+
195+
const sum = (f: (s: Score) => number): number => SCORES.reduce((n, s) => n + f(s), 0);
196+
197+
describe("the port on four regions it was never fitted to", () => {
198+
/**
199+
* **The raw queue is a strict superset in every one.** Nothing the port must
200+
* explain anywhere in these regions is a failure to GENERATE a cliff - it is
201+
* all over-generation, the same property #114 established on the original
202+
* three.
203+
*/
204+
it("contains every game cliff in all four regions", () => {
205+
expect(SCORES.map((s) => s.label)).toEqual([
206+
"[3000,3000]",
207+
"[-2000,-2000]",
208+
"[800,-1500]",
209+
"[-2600,1200]",
210+
]);
211+
expect(SCORES.every((s) => s.superset)).toBe(true);
212+
expect(sum((s) => s.raw)).toBe(2789);
213+
expect(sum((s) => s.game)).toBe(2590);
214+
}, 300000);
215+
216+
/**
217+
* The destruction predicate holds up out of sample: **precision 0.9877,
218+
* recall 0.8090**, against 0.971 and 0.889 on the original three. Two false
219+
* rejections in 2789 cells.
220+
*/
221+
it("scores precision 0.988 and recall 0.809 out of sample", () => {
222+
expect(sum((s) => s.ourKill)).toBe(163);
223+
expect(sum((s) => s.gameKill)).toBe(199);
224+
expect(sum((s) => s.agree)).toBe(161);
225+
expect(sum((s) => s.agree) / sum((s) => s.ourKill)).toBeCloseTo(0.9877, 4);
226+
expect(sum((s) => s.agree) / sum((s) => s.gameKill)).toBeCloseTo(0.809, 3);
227+
expect(sum((s) => s.falseRejections)).toBe(2);
228+
}, 300000);
229+
230+
/**
231+
* **`[3000,3000]` is exact** - 362 raw, 362 game, nothing to explain. It is
232+
* also the only region with no resource entity at all, which is what every
233+
* result from #123 onward would predict.
234+
*/
235+
it("reproduces [3000,3000] exactly, and it has no resources", () => {
236+
const s = SCORES[0];
237+
expect(s.raw).toBe(362);
238+
expect(s.game).toBe(362);
239+
expect(s.gameKill).toBe(0);
240+
expect(s.missed).toBe(0);
241+
expect(CASES[0].resources.length).toBe(0);
242+
}, 300000);
243+
244+
/**
245+
* The missed destructions split the same way #123 found: mostly ore, with a
246+
* residue no lever reaches. The ore attribution comes from the paired OFF arm,
247+
* not from our own predicate - the circularity #123 corrected.
248+
*/
249+
it("splits 38 missed destructions into 25 ore and 13 unexplained", () => {
250+
expect(sum((s) => s.missed)).toBe(38);
251+
expect(sum((s) => s.ore)).toBe(25);
252+
expect(sum((s) => s.unknown)).toBe(13);
253+
}, 300000);
254+
});
255+
256+
describe("the chunk-border enrichment replicates out of sample", () => {
257+
/**
258+
* **The pre-registered test.** The hypothesis was formed on the original 14
259+
* unexplained cells (9 on a border, 64.3%, against a 45.0% base rate - 1.45
260+
* sigma, correctly dismissed as noise at that n). These 13 cells were captured
261+
* afterwards, in regions chosen before any of them was known.
262+
*
263+
* They come back at **9 of 13, 69.2%**, against a 47.2% base rate here. Same
264+
* direction, same magnitude.
265+
*
266+
* On its own that is 1.59 sigma and combined about 2.1 - **still short of
267+
* decisive, and this file does not claim otherwise.** What changed is that it
268+
* is now a prediction that survived fresh data rather than a slice of the data
269+
* that suggested it.
270+
*/
271+
it("finds 9 of 13 unexplained cells on a chunk border, against 47.2%", () => {
272+
expect(sum((s) => s.unknown)).toBe(13);
273+
expect(sum((s) => s.unknownOnBorder)).toBe(9);
274+
const base = sum((s) => s.rawOnBorder) / sum((s) => s.raw);
275+
expect(base).toBeCloseTo(0.472, 3);
276+
// The enrichment, and the base rate that makes it mean something.
277+
expect(sum((s) => s.unknownOnBorder) / sum((s) => s.unknown)).toBeCloseTo(0.692, 3);
278+
}, 300000);
279+
280+
/**
281+
* The combined population, which is what actually unblocks the next attempt:
282+
* **27 unexplained cells, 18 on a border**. Every structural test on this
283+
* residual just doubled its power.
284+
*/
285+
it("brings the unexplained population to 27", () => {
286+
// 14 from the original three regions (#126), 13 here.
287+
expect(14 + sum((s) => s.unknown)).toBe(27);
288+
expect(9 + sum((s) => s.unknownOnBorder)).toBe(18);
289+
}, 300000);
290+
});

test/fixtures/PROVENANCE.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,10 @@
323323
"oracle-vulcanus-cliff-ore-richness.seed123456.json": {
324324
"factorioVersion": "2.1.12",
325325
"evidence": "captured 2026-08-03 by test/oracle/capture.ts vulcanus-cliff-ore-richness against the installed binary, which pnpm refs:sync --check reported in sync at 2.1.12 at capture time. The control separating 'the ore suppresses cliffs' from 'the autoplace_controls lever perturbs something structural' (#84). control:calcite:richness appears only in vulcanus_calcite_richness - not in vulcanus_calcite_probability, which decides where the ore lands, nor in vulcanus_calcite_region, which drives the volcanic_jagged_ground_range tile - so it changes the compiled map-gen settings while leaving entity positions and tiles alone. Three arms over [1500,1500]: default, calcite richness x2, calcite richness x0.5. Each records the autoplace_controls the SURFACE read back and dumps the resources alongside the cliffs from one generated surface, so 'positions unchanged' is measured rather than argued from the Lua."
326+
},
327+
"oracle-vulcanus-cliff-entities-more-regions.seed123456.json": {
328+
"factorioVersion": "2.1.12",
329+
"evidence": "captured 2026-08-03 by test/oracle/capture.ts vulcanus-cliff-entities-more-regions against the installed binary, which pnpm refs:sync --check reported in sync at 2.1.12 at capture time. Four MORE Vulcanus cliff-entity regions with the paired ON / ALL-resources-OFF ore lever (#84), captured to raise n: the residual's unexplained population was 14 cells and every structural test on it landed at 1.4-1.9 sigma against its base rate, which is what a partition looks like at n=14 whether or not a cause exists; and the shipped accuracy figure was measured on three regions chosen for reasons unrelated to sampling. Regions [3000,3000], [-2000,-2000], [800,-1500] and [-2600,1200], each 256x256, spread away from spawn and from each other. Each arm records the autoplace_controls and cliff_settings the SURFACE read back and dumps cliffs and resources from one generated surface, so an override that failed to apply cannot be mistaken for a term that does not matter."
326330
}
327331
}
328332
}

0 commit comments

Comments
 (0)