|
| 1 | +import { readFileSync } from "node:fs"; |
| 2 | +import { join } from "node:path"; |
| 3 | + |
| 4 | +import { beforeAll, describe, expect, it } from "vite-plus/test"; |
| 5 | + |
| 6 | +import { surfaceSeedForPlanet } from "../src/model/planetSurfaceSeed"; |
| 7 | +import { |
| 8 | + runRenderRequest, |
| 9 | + type ElevationRenderRequest, |
| 10 | +} from "../src/noise/preview/elevationRenderRequest"; |
| 11 | +import { compileEngine, instantiateEngine, type EngineExports } from "../src/noise/wasm/engine"; |
| 12 | +import { foldPixels, frozen } from "./tier3Frozen"; |
| 13 | + |
| 14 | +/** |
| 15 | + * The request dispatch for the Vulcanus volcanism control, tested separately |
| 16 | + * from the engine. |
| 17 | + * |
| 18 | + * The engine has read `volcanismFrequency` and `volcanismSize` since phase 5, |
| 19 | + * and `test/wasmVulcanusParity.spec.ts` sweeps both moved. What nothing graded |
| 20 | + * was the line in between: `renderVulcanusThroughWasm` wrote a literal `1` for |
| 21 | + * each, so the Terrain tab's slider was inert on the preview while every |
| 22 | + * render spec stayed green - the same gap the Fulgora islands block in |
| 23 | + * `fulgoraSurfaceSeed.spec.ts` was written for. A lever that silently does |
| 24 | + * nothing is exactly the failure the request layer can hide, and 1 is the one |
| 25 | + * value that hides it: `slider_rescale(1, 3)` is exactly 1, so at the default |
| 26 | + * both terms vanish. |
| 27 | + * |
| 28 | + * **The window must contain the thing it grades.** The first draft of this |
| 29 | + * spec used the `square at origin` window - 64 tiles across at 1 tile per |
| 30 | + * pixel - and stayed red AFTER the slider was wired, because that whole square |
| 31 | + * is inside the starting area, where the volcano spots are excluded and the |
| 32 | + * mountain noise is flattened. Measured 2026-09-07 on the wired code, pixels |
| 33 | + * moved out of the window when a lever is taken off 1: |
| 34 | + * |
| 35 | + * | window (parity spec's) | frequency 2 | size 3 | |
| 36 | + * | -------------------------- | ----------: | ----------: | |
| 37 | + * | square at origin, 1 px/tile | 0/4096 | 0/4096 | |
| 38 | + * | wide, offset, 1 px/tile | 1828/2304 | 1813/2304 | |
| 39 | + * | tall, coarse, 8 tiles/px | 1583/2304 | 698/2304 | |
| 40 | + * | fine, far field, 0.5 t/px | 1214/1536 | 1300/1536 | |
| 41 | + * |
| 42 | + * So a spec on the origin window passes a hardcoded 1, which is the defect it |
| 43 | + * exists to catch. `WIDE_OFFSET` below is the parity spec's second window, |
| 44 | + * field for field, so the neutral arm can be held to that spec's frozen row. |
| 45 | + */ |
| 46 | +let engine: EngineExports; |
| 47 | +beforeAll(async () => { |
| 48 | + const wasmPath = join(import.meta.dirname, "..", "src", "noise", "wasm", "engine.wasm"); |
| 49 | + engine = await instantiateEngine(await compileEngine(readFileSync(wasmPath))); |
| 50 | +}); |
| 51 | + |
| 52 | +const SEED0 = surfaceSeedForPlanet("vulcanus", 123456); |
| 53 | + |
| 54 | +const COMMON = { |
| 55 | + id: 1, |
| 56 | + planet: "vulcanus", |
| 57 | + view: "terrain", |
| 58 | + seed0: SEED0, |
| 59 | + waterLevel: 0, |
| 60 | + segmentationMultiplier: 1, |
| 61 | + startingPositions: [{ x: 0, y: 0 }], |
| 62 | +} as const satisfies Partial<ElevationRenderRequest>; |
| 63 | + |
| 64 | +/** `wasmVulcanusRenderParity.spec.ts`'s `wide, offset` window, field for field. */ |
| 65 | +const WIDE_OFFSET: ElevationRenderRequest = { |
| 66 | + ...COMMON, |
| 67 | + width: 96, |
| 68 | + height: 24, |
| 69 | + originX: 512.5, |
| 70 | + originY: -1024.25, |
| 71 | + tilesPerPixel: 1, |
| 72 | +}; |
| 73 | + |
| 74 | +/** That spec's `square at origin` window - the one the slider cannot reach. */ |
| 75 | +const SQUARE_AT_ORIGIN: ElevationRenderRequest = { |
| 76 | + ...COMMON, |
| 77 | + width: 64, |
| 78 | + height: 64, |
| 79 | + originX: -32, |
| 80 | + originY: -32, |
| 81 | + tilesPerPixel: 1, |
| 82 | +}; |
| 83 | + |
| 84 | +const NEUTRAL = { frequency: 1, size: 1 } as const; |
| 85 | + |
| 86 | +function render(req: ElevationRenderRequest): Uint8ClampedArray { |
| 87 | + return new Uint8ClampedArray(runRenderRequest(req, engine).buffer); |
| 88 | +} |
| 89 | + |
| 90 | +/** Pixels whose RGB differ between two renders of the same window. */ |
| 91 | +function pixelsMoved(a: Uint8ClampedArray, b: Uint8ClampedArray): number { |
| 92 | + expect(b.length).toBe(a.length); |
| 93 | + let moved = 0; |
| 94 | + for (let i = 0; i < a.length; i += 4) { |
| 95 | + if (a[i] !== b[i] || a[i + 1] !== b[i + 1] || a[i + 2] !== b[i + 2]) moved++; |
| 96 | + } |
| 97 | + return moved; |
| 98 | +} |
| 99 | + |
| 100 | +describe("vulcanus volcanism dispatch", () => { |
| 101 | + it("threads control:vulcanus_volcanism:frequency through to the render", () => { |
| 102 | + const neutral = render({ ...WIDE_OFFSET, vulcanusVolcanismControls: NEUTRAL }); |
| 103 | + const moved = render({ ...WIDE_OFFSET, vulcanusVolcanismControls: { frequency: 2, size: 1 } }); |
| 104 | + // Most of the window, not a sliver: 1828 of 2304 measured. The bound is |
| 105 | + // loose on purpose - the claim is that the lever reaches the render, not |
| 106 | + // any particular count. |
| 107 | + expect(pixelsMoved(neutral, moved)).toBeGreaterThan(1000); |
| 108 | + }); |
| 109 | + |
| 110 | + it("threads control:vulcanus_volcanism:size through to the render", () => { |
| 111 | + const neutral = render({ ...WIDE_OFFSET, vulcanusVolcanismControls: NEUTRAL }); |
| 112 | + const moved = render({ ...WIDE_OFFSET, vulcanusVolcanismControls: { frequency: 1, size: 3 } }); |
| 113 | + // 1813 of 2304 measured. |
| 114 | + expect(pixelsMoved(neutral, moved)).toBeGreaterThan(1000); |
| 115 | + }); |
| 116 | + |
| 117 | + it("omitting the control equals passing the neutral pair", () => { |
| 118 | + // So the default really is the game's neutral position, not "unset". |
| 119 | + const omitted = render(WIDE_OFFSET); |
| 120 | + const neutral = render({ ...WIDE_OFFSET, vulcanusVolcanismControls: NEUTRAL }); |
| 121 | + expect(pixelsMoved(omitted, neutral)).toBe(0); |
| 122 | + }); |
| 123 | + |
| 124 | + it("the neutral pair renders the bytes frozen before the control was wired", () => { |
| 125 | + // The tier-3 row was recorded while the request layer wrote a literal 1 |
| 126 | + // for both levers. Wiring the slider must not move a default render; if |
| 127 | + // this row moves, the default is no longer the game's neutral position. |
| 128 | + const want = frozen("vulcanus:render", "wide, offset", "terrain"); |
| 129 | + expect(want).toBeDefined(); |
| 130 | + const neutral = render({ ...WIDE_OFFSET, vulcanusVolcanismControls: NEUTRAL }); |
| 131 | + expect(foldPixels(neutral)).toBe(want); |
| 132 | + }); |
| 133 | + |
| 134 | + it("the cliffs view moves with the slider too", () => { |
| 135 | + // Volcanism moves the elevation contours the cliff bands sit on - the |
| 136 | + // lever #84 wants for separating an elevation-side residual from a |
| 137 | + // placement-side one - so the cliffs view has to see it as well as terrain. |
| 138 | + const neutral = render({ ...WIDE_OFFSET, view: "cliffs" }); |
| 139 | + const moved = render({ |
| 140 | + ...WIDE_OFFSET, |
| 141 | + view: "cliffs", |
| 142 | + vulcanusVolcanismControls: { frequency: 2, size: 3 }, |
| 143 | + }); |
| 144 | + expect(pixelsMoved(neutral, moved)).toBeGreaterThan(1000); |
| 145 | + }); |
| 146 | + |
| 147 | + it("the square at the origin cannot see the slider - the control for the window choice", () => { |
| 148 | + // Deliberately frozen at ZERO. This is the window the first draft graded |
| 149 | + // on, and the reason the wired code still read as unwired. If it ever |
| 150 | + // moves, volcanism has started reaching the starting area, which is a |
| 151 | + // finding about the engine rather than about this spec. |
| 152 | + const neutral = render(SQUARE_AT_ORIGIN); |
| 153 | + for (const c of [ |
| 154 | + { frequency: 2, size: 1 }, |
| 155 | + { frequency: 1, size: 3 }, |
| 156 | + ]) { |
| 157 | + const moved = render({ ...SQUARE_AT_ORIGIN, vulcanusVolcanismControls: c }); |
| 158 | + expect(pixelsMoved(neutral, moved), JSON.stringify(c)).toBe(0); |
| 159 | + } |
| 160 | + }); |
| 161 | +}); |
0 commit comments