|
| 1 | +import { describe, expect, it } from "vite-plus/test"; |
| 2 | + |
| 3 | +import ore from "./fixtures/oracle-vulcanus-cliff-ore-direction.seed123456.json"; |
| 4 | +import levers from "./fixtures/oracle-vulcanus-cliff-suppressor-levers.seed123456.json"; |
| 5 | + |
| 6 | +/** |
| 7 | + * **The crater-cliff RNG hypothesis: n is ONE, both of its supporting rows are |
| 8 | + * VACUOUS, and a documented mechanism explains craters without it** (#84). |
| 9 | + * |
| 10 | + * #130 recorded "calcite suppresses crater-cliffs" and proposed a mechanism |
| 11 | + * class new to #84 - entity autoplace ROLLS, so removing calcite shifts the |
| 12 | + * per-chunk RNG stream. It marked itself a hypothesis and said "nothing here |
| 13 | + * tests it". This is that test, and it needs no capture: every fact below is in |
| 14 | + * fixtures that were already committed, one of them since #111. |
| 15 | + * |
| 16 | + * The hypothesis is not refuted. What is refuted is the evidence for it. |
| 17 | + * |
| 18 | + * ## 1. n is ONE crater, not eight |
| 19 | + * |
| 20 | + * The eight `crater-cliff` entities are eight segments of a **single ring** - |
| 21 | + * centre `(1646.62, 1679.75)`, radii 4.95 to 7.00, against the prototype's own |
| 22 | + * `crater_radius = 7` (`decoratives-vulcanus.lua`). FFF #386 describes exactly |
| 23 | + * this: "a ring of special cliffs where sections of the ring can randomly be |
| 24 | + * removed." |
| 25 | + * |
| 26 | + * So the effective sample size is **1**, and #130's table invites reading it as |
| 27 | + * 8. This is [[below-chance-needs-a-clustered-null]] in a second place. |
| 28 | + * |
| 29 | + * ## 2. The spatial reading would have been vacuous |
| 30 | + * |
| 31 | + * All eight segments sit within **0.33 tiles** of a calcite entity, which looks |
| 32 | + * like a smoking gun for a local mechanism and is not one: their chunk `(51,52)` |
| 33 | + * holds **805 calcite entities over 1024 tiles - 78.6% coverage**. A ring landing |
| 34 | + * on calcite there is the expected outcome, not a surprising one. Worth |
| 35 | + * recording because the check is cheap and the wrong conclusion is attractive. |
| 36 | + * |
| 37 | + * ## 3. #130's control row is VACUOUS |
| 38 | + * |
| 39 | + * Its table offers "geyser OFF -> 0 craters" as the arm distinguishing calcite |
| 40 | + * from resources generally. **There are zero geysers in the crater's chunk** - |
| 41 | + * all 19 in the region sit in chunks `(48,48)`, `(48,49)` and `(53,54)`. The |
| 42 | + * lever is non-vacuous globally (it removes all 19, and moves `cliff-vulcanus` |
| 43 | + * 885 -> 889) and inert *for this chunk's stream by construction*. It cannot |
| 44 | + * discriminate anything about the ring, and #130 read it as if it could. |
| 45 | + * |
| 46 | + * ## 4. A lever with NOTHING to do with calcite also produces craters |
| 47 | + * |
| 48 | + * Unread in `oracle-vulcanus-cliff-suppressor-levers` since #111: |
| 49 | + * |
| 50 | + * | arm | `cliff-vulcanus` | `crater-cliff` | |
| 51 | + * | --- | --- | --- | |
| 52 | + * | default, resources ON | 885 | **0** | |
| 53 | + * | resources OFF via controls | 916 | 8 | |
| 54 | + * | **LAVA TILES OFF only** | 1053 | **7** | |
| 55 | + * | resources OFF + LAVA TILES OFF | 1082 | **18** | |
| 56 | + * |
| 57 | + * **Craters appear with calcite still ON.** Every #84 spec filters to |
| 58 | + * `name === "cliff-vulcanus"`, so the column had never been looked at - the same |
| 59 | + * failure #94 caught once already. |
| 60 | + * |
| 61 | + * ## 5. And that one is a DATA FACT, not a hypothesis |
| 62 | + * |
| 63 | + * `crater-cliff` overrides the default cliff mask with an explicit |
| 64 | + * `{item, object, player, water_tile}`. On Vulcanus only `lava` and `lava-hot` |
| 65 | + * carry `water_tile`. So lava blocks crater segments **by prototype**, and row 3 |
| 66 | + * of that table needs no RNG at all. |
| 67 | + * |
| 68 | + * ## Where that leaves calcite |
| 69 | + * |
| 70 | + * Its route is still unexplained, and the RNG stream is still the only unclosed |
| 71 | + * candidate - but two others are closed here rather than assumed: |
| 72 | + * |
| 73 | + * - **Not entity collision.** `collision-mask-defaults.lua` gives `resource` the |
| 74 | + * mask `{resource}`, disjoint from every cliff mask including |
| 75 | + * `crater-cliff`'s. That is #124's prototype-level argument, extended to the |
| 76 | + * second entity type rather than assumed to carry over. |
| 77 | + * - **Not lava tiles.** #128 measured calcite moving **zero** blocking tiles. |
| 78 | + * |
| 79 | + * A third fact falls out of the same file and closes a route this session had to |
| 80 | + * consider: the default `cliff` mask carries **`not_colliding_with_itself`**, so |
| 81 | + * cliffs never collide with cliffs of their own prototype. `applyCliffs` adds |
| 82 | + * each cliff to the surface *before* testing the next one, so without that flag |
| 83 | + * the whole run would be order-dependent. |
| 84 | + * |
| 85 | + * ## Status |
| 86 | + * |
| 87 | + * **The RNG hypothesis is UNSUPPORTED, not refuted.** It has n = 1, no surviving |
| 88 | + * control, and a competing documented mechanism for the phenomenon it was |
| 89 | + * invented to explain. Testing it properly needs a lever that changes the roll |
| 90 | + * count in the crater's own chunk without touching that chunk's tiles or lava - |
| 91 | + * switching off one of the rock prototypes listed before `crater-cliff` in |
| 92 | + * `autoplace_settings.entity` would do it. That is one new capture arm, and it |
| 93 | + * is worth having a real control before spending it. |
| 94 | + */ |
| 95 | + |
| 96 | +interface Ent { |
| 97 | + x: number; |
| 98 | + y: number; |
| 99 | + name: string; |
| 100 | +} |
| 101 | +interface Res { |
| 102 | + x: number; |
| 103 | + y: number; |
| 104 | + name: string; |
| 105 | +} |
| 106 | +interface Case { |
| 107 | + label: string; |
| 108 | + cliffs: Ent[]; |
| 109 | + resources?: Res[]; |
| 110 | +} |
| 111 | + |
| 112 | +const oreCases = ore.cases as unknown as Case[]; |
| 113 | +const leverCases = levers.cases as unknown as Case[]; |
| 114 | +const arm = (cases: Case[], label: string): Case => { |
| 115 | + const c = cases.find((v) => v.label === label); |
| 116 | + if (c === undefined) throw new Error(`no arm ${label}`); |
| 117 | + return c; |
| 118 | +}; |
| 119 | +const named = (c: Case, name: string): Ent[] => c.cliffs.filter((e) => e.name === name); |
| 120 | +const chunkOf = (v: number): number => Math.floor(v / 32); |
| 121 | + |
| 122 | +const ON = arm(oreCases, "entity region, resources ON"); |
| 123 | +const CALCITE_OFF = arm(oreCases, "entity region, calcite OFF"); |
| 124 | +const GEYSER_OFF = arm(oreCases, "entity region, geyser OFF"); |
| 125 | +const RING = named(CALCITE_OFF, "crater-cliff"); |
| 126 | + |
| 127 | +describe("the eight craters are ONE ring, so n is 1", () => { |
| 128 | + it("has all eight segments on a circle of the prototype's crater_radius", () => { |
| 129 | + expect(RING.length).toBe(8); |
| 130 | + const cx = RING.reduce((a, e) => a + e.x, 0) / RING.length; |
| 131 | + const cy = RING.reduce((a, e) => a + e.y, 0) / RING.length; |
| 132 | + const radii = RING.map((e) => Math.hypot(e.x - cx, e.y - cy)); |
| 133 | + // `crater_radius = 7` in decoratives-vulcanus.lua, and the segment centres |
| 134 | + // sit on it. The bound carries a tolerance because `(cx, cy)` is the |
| 135 | + // centroid of the SURVIVING segments, not the crater's true centre - a ring |
| 136 | + // missing a section has a centroid slightly off it, which is worth a hair |
| 137 | + // rather than a rounder-looking assertion that would fail on the next ring. |
| 138 | + for (const r of radii) { |
| 139 | + expect(r).toBeGreaterThan(4.9); |
| 140 | + expect(r).toBeLessThan(7.1); |
| 141 | + } |
| 142 | + // One ring means one chunk, which is what makes the effective sample size 1. |
| 143 | + const chunks = new Set(RING.map((e) => `${String(chunkOf(e.x))},${String(chunkOf(e.y))}`)); |
| 144 | + expect(chunks.size).toBe(1); |
| 145 | + }); |
| 146 | +}); |
| 147 | + |
| 148 | +describe("the spatial coincidence with calcite is vacuous", () => { |
| 149 | + /** |
| 150 | + * Every segment is within a third of a tile of a calcite entity - and the |
| 151 | + * chunk is 78.6% calcite, so that is the expected outcome. Both halves are |
| 152 | + * asserted, because the first without the second is the wrong conclusion. |
| 153 | + */ |
| 154 | + it("puts every segment within 0.4 tiles of calcite", () => { |
| 155 | + const cal = (ON.resources ?? []).filter((r) => r.name === "calcite"); |
| 156 | + for (const e of RING) { |
| 157 | + const d = Math.min(...cal.map((r) => Math.hypot(e.x - r.x, e.y - r.y))); |
| 158 | + expect(d).toBeLessThan(0.4); |
| 159 | + } |
| 160 | + }); |
| 161 | + |
| 162 | + it("but the ring's chunk is 78.6% calcite, so that means nothing", () => { |
| 163 | + const cal = (ON.resources ?? []).filter((r) => r.name === "calcite"); |
| 164 | + const [cx, cy] = [chunkOf(RING[0].x), chunkOf(RING[0].y)]; |
| 165 | + const inChunk = cal.filter((r) => chunkOf(r.x) === cx && chunkOf(r.y) === cy).length; |
| 166 | + expect(inChunk).toBe(805); |
| 167 | + expect(inChunk / 1024).toBeGreaterThan(0.75); |
| 168 | + }); |
| 169 | +}); |
| 170 | + |
| 171 | +describe("#130's geyser control cannot discriminate anything", () => { |
| 172 | + /** |
| 173 | + * The lever works - it removes all 19 geysers and moves `cliff-vulcanus` - and |
| 174 | + * it is inert for the crater's chunk by construction, because no geyser is in |
| 175 | + * it. Both arms are needed: "the lever did something" and "it did nothing |
| 176 | + * HERE" are different claims. |
| 177 | + */ |
| 178 | + it("removes every geyser and still moves cliff-vulcanus", () => { |
| 179 | + expect((ON.resources ?? []).filter((r) => r.name === "sulfuric-acid-geyser").length).toBe(19); |
| 180 | + expect( |
| 181 | + (GEYSER_OFF.resources ?? []).filter((r) => r.name === "sulfuric-acid-geyser").length, |
| 182 | + ).toBe(0); |
| 183 | + expect(named(ON, "cliff-vulcanus").length).toBe(885); |
| 184 | + expect(named(GEYSER_OFF, "cliff-vulcanus").length).toBe(889); |
| 185 | + }); |
| 186 | + |
| 187 | + it("but touches no entity in the crater's own chunk", () => { |
| 188 | + const [cx, cy] = [chunkOf(RING[0].x), chunkOf(RING[0].y)]; |
| 189 | + const gey = (ON.resources ?? []).filter((r) => r.name === "sulfuric-acid-geyser"); |
| 190 | + expect(gey.length).toBeGreaterThan(0); |
| 191 | + expect(gey.filter((r) => chunkOf(r.x) === cx && chunkOf(r.y) === cy).length).toBe(0); |
| 192 | + expect(named(GEYSER_OFF, "crater-cliff").length).toBe(0); |
| 193 | + }); |
| 194 | +}); |
| 195 | + |
| 196 | +describe("a lever unrelated to calcite also produces craters", () => { |
| 197 | + /** |
| 198 | + * The row that had been sitting unread in the #111 fixture. Resources are ON |
| 199 | + * in the lava arm, so calcite is not necessary for craters to appear - and |
| 200 | + * `crater-cliff`'s explicit mask carries `water_tile`, which on Vulcanus only |
| 201 | + * `lava` and `lava-hot` have, so the mechanism is a prototype-level data fact. |
| 202 | + */ |
| 203 | + it("brings back 7 craters with LAVA TILES OFF and calcite still ON", () => { |
| 204 | + expect(named(arm(leverCases, "default, resources ON"), "crater-cliff").length).toBe(0); |
| 205 | + expect(named(arm(leverCases, "LAVA TILES OFF only"), "crater-cliff").length).toBe(7); |
| 206 | + }); |
| 207 | + |
| 208 | + it("and 18 when both levers are pulled, more than either alone", () => { |
| 209 | + const both = named(arm(leverCases, "resources OFF, LAVA TILES OFF"), "crater-cliff").length; |
| 210 | + const res = named(arm(leverCases, "resources OFF via controls"), "crater-cliff").length; |
| 211 | + const lava = named(arm(leverCases, "LAVA TILES OFF only"), "crater-cliff").length; |
| 212 | + expect(res).toBe(8); |
| 213 | + expect(lava).toBe(7); |
| 214 | + expect(both).toBe(18); |
| 215 | + expect(both).toBeGreaterThan(Math.max(res, lava)); |
| 216 | + }); |
| 217 | +}); |
0 commit comments