/** * The fill of a tilemap layer (#133): the cells reached from the one clicked through their four * edge neighbours that hold the same tile – empty included – and nothing outside the grid. */ import { describe, expect, it } from 'vitest'; import type { TilemapLayer } from '../../src/core/model/Asset'; import { cellKey, floodFill } from '../../src/core/model/tilemap'; /** A layer drawn as text: one row per line, `.` empty, any other letter the tile of that name. */ function layerOf(rows: string[]): TilemapLayer { const cells: Record = {}; rows.forEach((row, y) => [...row].forEach((c, x) => { if (c !== '.') cells[cellKey(x, y)] = c; }), ); return { id: 'l', name: 'Layer', cells }; } /** The filled cells as text again, `*` where the fill landed – easier to read than a list. */ function shown(rows: string[], filled: { x: number; y: number }[]): string[] { const out = rows.map((row) => [...row]); for (const { x, y } of filled) out[y]![x] = '*'; return out.map((row) => row.join('')); } const GRID = { columns: 5, rows: 4 }; describe('filling a tilemap layer', () => { it('takes the connected cells through edges, not through corners', () => { const rows = ['..w..', '.w.w.', 'w...w', '.w.w.']; const cells = floodFill(GRID, layerOf(rows), { x: 2, y: 2 }, 'g'); expect(shown(rows, cells)).toEqual(['..w..', '.w*w.', 'w***w', '.w*w.']); }); it('fills a whole empty grid and stops at its edge', () => { const cells = floodFill(GRID, layerOf([]), { x: 0, y: 0 }, 'g'); expect(cells).toHaveLength(20); expect(cells.every(({ x, y }) => x >= 0 && y >= 0 && x < 5 && y < 4)).toBe(true); }); it('replaces one tile with another and leaves the empty cells beside it', () => { const rows = ['aa...', 'a.bb.', 'aaab.', '.....']; const cells = floodFill(GRID, layerOf(rows), { x: 0, y: 0 }, 'b'); expect(shown(rows, cells)).toEqual(['**...', '*.bb.', '***b.', '.....']); }); it('ignores cells outside the grid, even when they hold the same tile', () => { const layer = layerOf(['aaaaa']); layer.cells[cellKey(5, 0)] = 'a'; layer.cells[cellKey(-1, 0)] = 'a'; const cells = floodFill(GRID, layer, { x: 2, y: 0 }, 'g'); expect(cells.map((c) => c.x).sort()).toEqual([0, 1, 2, 3, 4]); expect(floodFill(GRID, layer, { x: 5, y: 0 }, 'g')).toEqual([]); }); it('changes nothing when the cell already holds the tile', () => { expect(floodFill(GRID, layerOf(['aaaaa']), { x: 1, y: 0 }, 'a')).toEqual([]); expect(floodFill(GRID, layerOf([]), { x: 1, y: 1 }, '')).toEqual([]); }); it('can empty a region, too', () => { const rows = ['aa.a.']; const cells = floodFill(GRID, layerOf(rows), { x: 1, y: 0 }, ''); expect(shown(rows, cells)).toEqual(['**.a.']); }); it('fills a large map in one go', () => { const props = { columns: 300, rows: 300 }; const started = performance.now(); expect(floodFill(props, layerOf([]), { x: 150, y: 150 }, 'g')).toHaveLength(90_000); expect(performance.now() - started).toBeLessThan(2000); }); });