import { describe, expect, it } from 'vitest'; import { Rng } from '../src/shared/rng'; import { TerrainBitmap } from '../src/shared/terrain/bitmap'; import { applyOp, isValidOp, type TerrainOp } from '../src/shared/terrain/ops'; import { bitmapFrom, realMaterials } from './helpers'; const materials = realMaterials(); function filled(w: number, h: number, mat: number): TerrainBitmap { return new TerrainBitmap(w, h, new Uint8Array(w * h).fill(mat)); } function randomOps(seed: number, count: number, w: number, h: number): TerrainOp[] { const rng = new Rng(seed); const ops: TerrainOp[] = []; for (let i = 0; i < count; i++) { const x = rng.int(0, w - 1); const y = rng.int(0, h - 1); const r = rng.int(1, 30); const k = rng.int(0, 2); if (k === 0) ops.push({ kind: 'carve', x, y, r, power: rng.int(0, 80) }); else if (k === 1) ops.push({ kind: 'fill', x, y, r, material: rng.int(1, 5) }); else ops.push({ kind: 'convert', x, y, r, from: 1, to: 8 }); } return ops; } describe('terrain ops', () => { it('carve removes destructible pixels within the radius only', () => { const bm = filled(21, 21, 1); const res = applyOp(bm, { kind: 'carve', x: 10, y: 10, r: 3, power: 50 }, materials); expect(bm.get(10, 10)).toBe(0); expect(bm.get(13, 10)).toBe(0); expect(bm.get(14, 10)).toBe(1); expect(bm.get(12, 12)).toBe(0); // 2² + 2² = 8 <= 9 expect(bm.get(13, 11)).toBe(1); // 3² + 1² = 10 > 9 expect(res.removedByMaterial.get(1)).toBe(29); // integer disc of radius 3 has 29 pixels }); it('power is compared to hardness: chocolate resists a weak carve', () => { const bm = filled(9, 9, 2); applyOp(bm, { kind: 'carve', x: 4, y: 4, r: 3, power: 40 }, materials); expect(bm.get(4, 4)).toBe(2); applyOp(bm, { kind: 'carve', x: 4, y: 4, r: 3, power: 60 }, materials); expect(bm.get(4, 4)).toBe(0); }); it('the plate is indestructible', () => { const bm = filled(9, 9, 255); const res = applyOp(bm, { kind: 'carve', x: 4, y: 4, r: 4, power: 0xffff }, materials); expect(res.changed).toBeNull(); expect(bm.get(4, 4)).toBe(255); }); it('fill only writes empty pixels; convert only touches the source material', () => { const bm = bitmapFrom(['.....', '.111.', '.121.', '.111.', '.....']); applyOp(bm, { kind: 'fill', x: 2, y: 2, r: 2, material: 7 }, materials); expect(bm.get(2, 0)).toBe(7); expect(bm.get(2, 2)).toBe(2); expect(bm.get(1, 1)).toBe(1); applyOp(bm, { kind: 'convert', x: 2, y: 2, r: 2, from: 1, to: 8 }, materials); expect(bm.get(1, 1)).toBe(8); expect(bm.get(2, 2)).toBe(2); expect(bm.get(2, 0)).toBe(7); }); it('ops are clamped at the map border', () => { const bm = filled(10, 10, 1); expect(() => applyOp(bm, { kind: 'carve', x: -5, y: 12, r: 8, power: 99 }, materials)).not.toThrow(); expect(bm.get(0, 9)).toBe(0); }); it('rejects invalid ops (floats, plate fill, unknown kind)', () => { expect(isValidOp({ kind: 'carve', x: 1.5, y: 1, r: 3, power: 1 })).toBe(false); expect(isValidOp({ kind: 'fill', x: 1, y: 1, r: 3, material: 255 })).toBe(false); expect(isValidOp({ kind: 'nuke', x: 1, y: 1, r: 3 })).toBe(false); expect(isValidOp({ kind: 'carve', x: 1, y: 1, r: 3, power: 1 })).toBe(true); }); it('is deterministic: same op sequence -> bit-identical bitmaps', () => { const ops = randomOps(42, 500, 256, 128); const a = filled(256, 128, 1); const b = filled(256, 128, 1); for (const op of ops) applyOp(a, op, materials); for (const op of ops) applyOp(b, op, materials); expect(a.hash()).toBe(b.hash()); expect(Buffer.from(a.data).equals(Buffer.from(b.data))).toBe(true); // Golden value: changing it means the op semantics changed, which breaks network compatibility. expect(a.hash()).toMatchInlineSnapshot(`2655657281`); }); it('marks the chunks around the change dirty', () => { const bm = filled(256, 256, 1); bm.takeDirtyChunks(); applyOp(bm, { kind: 'carve', x: 64, y: 10, r: 2, power: 99 }, materials); // Change straddles the chunk border at x = 64 -> chunks 0 and 1 of the first row. expect(bm.takeDirtyChunks()).toEqual([0, 1]); expect(bm.takeDirtyChunks()).toEqual([]); }); });