| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- 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([]);
- });
- });
|