|
| 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 | +}); |
0 commit comments