Skip to content

Commit 9bd98b5

Browse files
wormeymanclaude
andauthored
test(cliffs): crater-cliff moves under the lever too - a SECOND effect (#84) (#130)
Found while checking something overdue: are the 31 suppressed cells actually ABSENT, or merely renamed? #94's lesson was that a `name === "cliff-vulcanus"` filter threw away an answer a fixture already held. They are absent - no cliff entity of any prototype at those positions or within a tile. But the same dump shows something nobody had looked at. CALCITE SUPPRESSES CRATER-CLIFFS TOO: 0 with the resources on, 8 with calcite off, 8 with all off, 0 with the geyser off - the same eight in the same places in both calcite-off arms. And `crater_cliff`'s probability expression cannot see calcite either: its 47-node closure holds no resource region, and neither do the two Vulcanus biomes it reads. The same impossibility, in a second entity type. BUT IT IS A DIFFERENT EFFECT. Crater-cliffs are autoplaced ENTITIES placed during `applyEntities`, and entity autoplace ROLLS. Removing calcite removes rolls, shifting the per-chunk RandomGenerator stream and moving everything drawn after it - a mechanism class #84 has never considered, and one that explains the craters with no noise dependency at all. It cannot explain the 31: `cliff-vulcanus` comes from `generateCliffs` and `applyCliffs`, which run before any entity is rolled. A cliff decided before the first roll cannot be moved by a change to the roll sequence. The craters are also 16.4 tiles from the nearest suppressed cell at closest approach, against their own 2.8-tile box. Recorded as a HYPOTHESIS, not a result - nothing here tests it. It is worth writing down because it is the first mechanism class in this investigation that is not already closed. FIXTURE HAZARD, NOW GUARDED: the arms disagree about how many crater-cliffs exist, so any cross-arm comparison over "all cliff entities" is confounded. The unfiltered difference is 39; the real suppression is 31. Every spec in #84 filters by prototype name and is correct, but that was inherited convention rather than a guarded decision. Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent fc1cc6d commit 9bd98b5

2 files changed

Lines changed: 258 additions & 0 deletions

File tree

docs/noise/vulcanus-cliffs-NOTES.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3446,3 +3446,63 @@ Ideas that are still untouched, for whoever picks this up:
34463446
`applyEntities` and is the one thing in the entity stage that touches cliffs at
34473447
all. Ruled out for the residual by position (#84's crater-cliff note) but never
34483448
ruled out as a mechanism.
3449+
3450+
## `crater-cliff` moves under the lever too - a SECOND effect, and an RNG lead (2026-08-03, #84)
3451+
3452+
Found while checking something that should have been checked long ago: are the 31
3453+
suppressed `cliff-vulcanus` cells actually **absent**, or merely renamed? #94's
3454+
lesson was that a `name === "cliff-vulcanus"` filter threw away an answer a
3455+
fixture already held, so the question was overdue.
3456+
3457+
They are absent - no cliff entity of any prototype at those positions, or within
3458+
a tile of them. But the same dump shows something nobody had looked at.
3459+
3460+
`test/craterCliffLeverSensitivity.spec.ts`.
3461+
3462+
### Calcite suppresses crater-cliffs as well
3463+
3464+
| arm | `crater-cliff` in `[1500,1500]` |
3465+
| --- | --- |
3466+
| resources ON | **0** |
3467+
| calcite OFF | **8** |
3468+
| ALL resources OFF | **8** |
3469+
| geyser OFF | **0** |
3470+
3471+
The same eight, in the same places, in both calcite-off arms. And
3472+
`crater_cliff`'s probability expression cannot see calcite either - its 47-node
3473+
closure holds no resource region, and neither do the two Vulcanus biomes it
3474+
reads. **The same impossibility, in a second entity type.**
3475+
3476+
### But it is a DIFFERENT effect, and it names a new mechanism class
3477+
3478+
Crater-cliffs are **autoplaced entities**, placed during `applyEntities`, and
3479+
entity autoplace **rolls**. Removing calcite removes rolls, which shifts the
3480+
per-chunk `RandomGenerator` stream and moves everything drawn after it. That is a
3481+
mechanism class #84 has never considered, and it explains the craters with no
3482+
noise dependency at all.
3483+
3484+
**It cannot explain the 31.** `cliff-vulcanus` comes from `generateCliffs` and
3485+
`applyCliffs`, which run before any entity is rolled - `computeInternal` calls
3486+
`generateCliffs` at `+0x2c`, before it even builds the `NoiseCache` the
3487+
`generateEntities` passes use. A cliff decided before the first roll cannot be
3488+
moved by a change to the roll sequence.
3489+
3490+
**Recorded as a HYPOTHESIS, not a result.** Nothing here tests it. It is worth
3491+
writing down because it is the first mechanism class in this investigation that
3492+
is not already closed - and because the alternative, that craters share the 31's
3493+
unknown cause, is a much bigger claim needing much better evidence.
3494+
3495+
The craters are also **16.4 tiles** from the nearest suppressed cell at closest
3496+
approach, against their own 2.8-tile collision box, so crater placement is not
3497+
the 31's mechanism even before the ordering argument.
3498+
3499+
### The practical takeaway: a fixture hazard, now guarded
3500+
3501+
The arms of `oracle-vulcanus-cliff-ore-direction` **disagree about how many
3502+
crater-cliffs exist**, so any cross-arm comparison over "all cliff entities" is
3503+
confounded by an effect that has nothing to do with the cliff generator. The
3504+
unfiltered difference is **39**; the real suppression is **31**.
3505+
3506+
Every spec in #84 filters to `name === "cliff-vulcanus"` and is therefore
3507+
correct - but that was inherited convention rather than a guarded decision. It is
3508+
guarded now.
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { describe, expect, it } from "vite-plus/test";
2+
3+
import ore from "./fixtures/oracle-vulcanus-cliff-ore-direction.seed123456.json";
4+
5+
/**
6+
* **`crater-cliff` moves under the ore lever too - and it is a DIFFERENT effect
7+
* from the 31, with a mechanism class nobody in #84 has considered** (#84).
8+
*
9+
* Found while checking whether the 31 suppressed `cliff-vulcanus` cells were
10+
* really absent or merely renamed. They are absent (first block). But the same
11+
* dump shows something nobody had looked at: the arms disagree about
12+
* **crater-cliff**.
13+
*
14+
* | arm | `crater-cliff` in `[1500,1500]` |
15+
* | --- | --- |
16+
* | resources ON | **0** |
17+
* | calcite OFF | **8** |
18+
* | ALL resources OFF | **8** |
19+
* | geyser OFF | **0** |
20+
*
21+
* So calcite suppresses crater-cliffs, exactly as it suppresses `cliff-vulcanus`
22+
* - and `crater_cliff`'s probability expression cannot see it either:
23+
*
24+
* ```lua
25+
* -- decoratives-vulcanus.lua
26+
* expression = "0.5 * (vulcanus_rock_noise + 0.5 * aux - 0.5 * moisture)
27+
* * (1 - max(vulcanus_basalts_biome, vulcanus_ashlands_biome))
28+
* * place_every_n(21,21,0,0)"
29+
* ```
30+
*
31+
* Its 47-node closure contains no resource region, and neither do the two biomes
32+
* it reads. Same impossibility, second entity type.
33+
*
34+
* **But crater-cliffs differ from the 31 in the one way that matters.** They are
35+
* AUTOPLACED entities, placed during `applyEntities`, and entity autoplace
36+
* **rolls**. Removing calcite removes rolls, which shifts the per-chunk
37+
* `RandomGenerator` stream and moves everything drawn after it. That is a
38+
* mechanism class #84 has never considered, and it explains craters without any
39+
* noise dependency at all.
40+
*
41+
* **It cannot explain the 31.** `cliff-vulcanus` comes from `generateCliffs` and
42+
* `applyCliffs`, both of which run before any entity is rolled -
43+
* `computeInternal` calls `generateCliffs` at `+0x2c`, before it even builds the
44+
* `NoiseCache` the `generateEntities` passes use. A cliff decided before the
45+
* first roll cannot be moved by a change to the roll sequence.
46+
*
47+
* **The RNG-stream reading is a HYPOTHESIS here, not a result.** Nothing in this
48+
* file tests it; it is recorded because it is the first mechanism class in this
49+
* investigation that is not already closed, and because the alternative - that
50+
* craters share the 31's unknown cause - would be a much bigger claim needing
51+
* much better evidence.
52+
*
53+
* **The fixture hazard, which is the practical takeaway.** The arms of
54+
* `oracle-vulcanus-cliff-ore-direction` disagree about how many crater-cliffs
55+
* exist, so any comparison across arms over "all cliff entities" is confounded by
56+
* an effect that has nothing to do with the cliff generator. Every spec in #84
57+
* filters to `name === "cliff-vulcanus"` and is therefore correct - but that was
58+
* inherited convention rather than a guarded decision, and this file makes it a
59+
* guarded one.
60+
*/
61+
62+
const K = (x: number, y: number): string => `${String(x)},${String(y)}`;
63+
64+
interface Ent {
65+
x: number;
66+
y: number;
67+
name: string;
68+
orientation?: string;
69+
}
70+
interface Region {
71+
x0: number;
72+
y0: number;
73+
x1: number;
74+
y1: number;
75+
}
76+
interface Case {
77+
label: string;
78+
region: Region;
79+
cliffs: Ent[];
80+
}
81+
const CASES = ore.cases as unknown as Case[];
82+
const arm = (label: string): Case => {
83+
const c = CASES.find((q) => q.label === label);
84+
if (c === undefined) throw new Error(`no arm ${label}`);
85+
return c;
86+
};
87+
const inRegion = (c: Case, e: { x: number; y: number }): boolean =>
88+
e.x >= c.region.x0 && e.x < c.region.x1 && e.y >= c.region.y0 && e.y < c.region.y1;
89+
const named = (label: string, name: string): Ent[] =>
90+
arm(label).cliffs.filter((e) => e.name === name && inRegion(arm(label), e));
91+
92+
describe("the 31 are absent, not renamed", () => {
93+
/**
94+
* **The check the repo has been burned by not doing.** #94's lesson was that a
95+
* `name === "cliff-vulcanus"` filter threw away the answer a fixture already
96+
* held. So: is there a cliff entity of ANY prototype at the 31 positions in
97+
* the resources-ON arm?
98+
*
99+
* There is not - not at the position, and not within a tile of it. The 31 are
100+
* genuinely missing rather than reclassified, which is a prerequisite of every
101+
* result in #84 and had never been stated.
102+
*/
103+
it("finds no cliff entity of any name at the 31 suppressed positions", () => {
104+
const on = arm("entity region, resources ON");
105+
const off = arm("entity region, ALL resources OFF");
106+
const onVulc = new Set(
107+
named("entity region, resources ON", "cliff-vulcanus").map((e) => K(e.x, e.y)),
108+
);
109+
const offVulc = named("entity region, ALL resources OFF", "cliff-vulcanus").map((e) =>
110+
K(e.x, e.y),
111+
);
112+
const suppressed = offVulc.filter((k) => !onVulc.has(k));
113+
expect(suppressed.length).toBe(31);
114+
115+
const onAny = on.cliffs.filter((e) => inRegion(on, e));
116+
const held = suppressed.filter((k) => {
117+
const [xs, ys] = k.split(",");
118+
const x = Number(xs);
119+
const y = Number(ys);
120+
return onAny.some((e) => Math.abs(e.x - x) <= 1 && Math.abs(e.y - y) <= 1);
121+
});
122+
expect(held).toEqual([]);
123+
// Non-vacuity: the ON arm is not simply empty of cliff entities.
124+
expect(onAny.length).toBeGreaterThan(800);
125+
void off;
126+
});
127+
});
128+
129+
describe("crater-cliff moves under the lever, and it is a separate effect", () => {
130+
/**
131+
* The counts. Calcite is the control that moves them; the geyser is not.
132+
*/
133+
it("has 0 crater-cliffs with the resources on and 8 with calcite off", () => {
134+
expect(named("entity region, resources ON", "crater-cliff").length).toBe(0);
135+
expect(named("entity region, geyser OFF", "crater-cliff").length).toBe(0);
136+
expect(named("entity region, calcite OFF", "crater-cliff").length).toBe(8);
137+
expect(named("entity region, ALL resources OFF", "crater-cliff").length).toBe(8);
138+
});
139+
140+
/**
141+
* The same eight, in the same places, in both arms that switch calcite off -
142+
* so this is one reproducible ring rather than a scatter of near-misses. Their
143+
* fractional coordinates are the signature of the entity generator's jitter,
144+
* which is what puts them off the 4-tile cliff lattice.
145+
*/
146+
it("places the same eight in both calcite-off arms", () => {
147+
const a = named("entity region, calcite OFF", "crater-cliff")
148+
.map((e) => K(e.x, e.y))
149+
.sort((x, y) => x.localeCompare(y));
150+
const b = named("entity region, ALL resources OFF", "crater-cliff")
151+
.map((e) => K(e.x, e.y))
152+
.sort((x, y) => x.localeCompare(y));
153+
expect(a).toEqual(b);
154+
expect(a.length).toBe(8);
155+
// Off-lattice: cliff cells sit at integer x and half-integer y.
156+
expect(a.every((k) => !Number.isInteger(Number(k.split(",")[0])))).toBe(true);
157+
});
158+
159+
/**
160+
* **And they are nowhere near the 31**, so crater placement cannot be the
161+
* mechanism behind them even before the ordering argument. The closest a
162+
* crater gets to any suppressed cell is **16.375** tiles - against a crater's
163+
* own 2.8-tile collision box, and with the next-nearest at 28.4.
164+
*/
165+
it("keeps the craters far from every suppressed cell", () => {
166+
const onVulc = new Set(
167+
named("entity region, resources ON", "cliff-vulcanus").map((e) => K(e.x, e.y)),
168+
);
169+
const suppressed = named("entity region, ALL resources OFF", "cliff-vulcanus")
170+
.filter((e) => !onVulc.has(K(e.x, e.y)))
171+
.map((e) => ({ x: e.x, y: e.y }));
172+
const craters = named("entity region, ALL resources OFF", "crater-cliff");
173+
let nearest = Infinity;
174+
for (const s of suppressed)
175+
for (const c of craters)
176+
nearest = Math.min(nearest, Math.max(Math.abs(c.x - s.x), Math.abs(c.y - s.y)));
177+
expect(suppressed.length).toBe(31);
178+
expect(craters.length).toBe(8);
179+
expect(nearest).toBeCloseTo(16.375, 3);
180+
});
181+
182+
/**
183+
* **The fixture hazard, guarded.** Comparing "all cliff entities" across these
184+
* arms mixes the 31 with the 8, and the two have different causes. Anything in
185+
* #84 that compares arms must filter by prototype name; this arm fails loudly
186+
* if the unfiltered counts are ever treated as comparable.
187+
*/
188+
it("shows the unfiltered totals are NOT comparable across arms", () => {
189+
const all = (label: string): number =>
190+
arm(label).cliffs.filter((e) => inRegion(arm(label), e)).length;
191+
const vulcOnly = (label: string): number => named(label, "cliff-vulcanus").length;
192+
// The unfiltered difference over-counts the suppression by exactly the 8.
193+
expect(all("entity region, ALL resources OFF") - all("entity region, resources ON")).toBe(39);
194+
expect(
195+
vulcOnly("entity region, ALL resources OFF") - vulcOnly("entity region, resources ON"),
196+
).toBe(31);
197+
});
198+
});

0 commit comments

Comments
 (0)