/** * The wall batching, as arithmetic: columns in, runs out. What matters is * that the runs say the same thing the columns did — every column covered * exactly once, in order — while there are far fewer of them. */ import { expect, test } from "bun:test"; import { covered, GEOM, hazeStep, makeColumns, SKY, wallRuns } from "../../static/engine/slices.js"; type Col = { level: number; side?: number; top?: number; high?: number; haze?: number }; /** Columns from a list, so a case reads as the picture it describes. */ function columns(list: Col[]) { const cols = makeColumns(list.length); list.forEach((c, i) => { cols.level[i] = c.level; cols.side[i] = c.side ?? 0; cols.top[i] = c.top ?? 100; cols.high[i] = c.high ?? 200; cols.haze[i] = c.haze ?? 0; }); return cols; } const flat = (n: number, over: Col = { level: 7 }) => columns(Array.from({ length: n }, () => over)); test("a run of equal levels is one rectangle, however long it is", () => { const out: never[] = []; const used = wallRuns(flat(1280), out); expect(used).toBe(1); expect(out[0]).toMatchObject({ from: 0, to: 1279, level: 7 }); }); test("the picture's column count is unchanged: every column covered once", () => { const cols = columns([ { level: 3 }, { level: 3 }, { level: 4 }, { level: 4 }, { level: 4 }, { level: 9, side: 1 }, ]); const out: { from: number; to: number }[] = []; const used = wallRuns(cols, out); expect(used).toBe(3); expect(covered(out, used)).toBe(6); // In order, end to end, no gaps and no overlap. expect(out.slice(0, used).map((r) => [r.from, r.to])).toEqual([ [0, 1], [2, 4], [5, 5], ]); }); test("each column its own level is one rectangle each: no picture is lost", () => { const cols = columns(Array.from({ length: 40 }, (_, i) => ({ level: i }))); const out: never[] = []; const used = wallRuns(cols, out); expect(used).toBe(40); expect(covered(out, used)).toBe(40); }); test("the same level on a different face is a different run", () => { const cols = columns([{ level: 5, side: 0 }, { level: 5, side: 1 }, { level: 5, side: 1 }]); const out: { side: number }[] = []; const used = wallRuns(cols, out); expect(used).toBe(2); expect(out[1]!.side).toBe(1); }); test("sky ends a run and is covered by nothing", () => { const cols = columns([{ level: 2 }, { level: SKY }, { level: 2 }, { level: 2 }]); const out: { from: number; to: number }[] = []; const used = wallRuns(cols, out); expect(used).toBe(2); expect(covered(out, used)).toBe(3); expect(out[1]).toMatchObject({ from: 2, to: 3 }); }); test("slices that differ in height by more than half a pixel do not merge", () => { const near = columns([ { level: 6, top: 100, high: 200 }, { level: 6, top: 100 + GEOM / 2, high: 200 }, ]); const far = columns([ { level: 6, top: 100, high: 200 }, { level: 6, top: 100, high: 200 + GEOM * 4 }, ]); expect(wallRuns(near, [])).toBe(1); expect(wallRuns(far, [])).toBe(2); }); test("haze joins the run's identity, quantised, so fog is batched too", () => { expect(hazeStep(0)).toBe(0); expect(hazeStep(1)).toBe(16); expect(hazeStep(2)).toBe(16); // clamped: nothing is fogged past opaque const cols = columns([ { level: 6, haze: 0.5 }, { level: 6, haze: 0.51 }, // the same step: one rectangle { level: 6, haze: 0.9 }, ]); const out: { haze: number }[] = []; const used = wallRuns(cols, out); expect(used).toBe(2); expect(out[0]!.haze).toBeCloseTo(0.5, 5); }); test("the run buffer is reused: a smaller frame allocates nothing new", () => { const out: unknown[] = []; wallRuns(columns(Array.from({ length: 30 }, (_, i) => ({ level: i }))), out); const grown = out.length; expect(grown).toBe(30); const used = wallRuns(flat(30), out); expect(used).toBe(1); expect(out.length).toBe(grown); });