Files
feuerwerk/tests/core/tilemap-cells.test.ts
T

149 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
import type { TilemapAsset } from '../../src/core/model/Asset';
import { findAsset } from '../../src/core/model/Project';
import { cellKey } from '../../src/core/model/tilemap';
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
import { decodeCells, encodeCells, MAX_RUN } from '../../src/core/store/tilemapCells';
import { makeProject, roundTrip } from '../helpers';
/** Cells from a picture: one character per cell, `.` is empty, any other names the tile `t<char>`. */
function cellsFrom(rows: string[], x0 = 0, y0 = 0): Record<string, string> {
const cells: Record<string, string> = {};
rows.forEach((row, y) =>
[...row].forEach((c, x) => {
if (c !== '.') cells[cellKey(x0 + x, y0 + y)] = `t${c}`;
}),
);
return cells;
}
function fullMap(size: number, tileOf: (x: number, y: number) => string): Record<string, string> {
const cells: Record<string, string> = {};
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) cells[cellKey(x, y)] = tileOf(x, y);
return cells;
}
/** A project with one tilemap whose single layer holds `cells`, as the editor would have it. */
function projectWith(cells: Record<string, string>) {
const f = makeProject();
const create = new CreateAssetCommand('tilemap', 'Ground');
f.stack.execute(create);
const asset = findAsset(f.project, create.asset.id) as TilemapAsset;
asset.layers[0]!.cells = { ...cells };
return { project: f.project, id: create.asset.id };
}
function storedLayer(raw: unknown, id: string): Record<string, unknown> {
const assets = (raw as { assets: { id: string; layers?: Record<string, unknown>[] }[] }).assets;
return assets.find((a) => a.id === id)!.layers![0]!;
}
describe('the cells of a tilemap in the file', () => {
it('store an empty layer as nothing at all', () => {
expect(encodeCells({})).toEqual({ tiles: [], origin: { x: 0, y: 0 }, rows: [] });
expect(decodeCells(encodeCells({}))).toEqual({});
});
it('store a single cell as its position and one run', () => {
expect(encodeCells({ '7,3': 'grass' })).toEqual({
tiles: ['grass'],
origin: { x: 7, y: 3 },
rows: ['1'],
});
});
it('write runs, gaps and several tiles row by row, and read them back', () => {
const cells = cellsFrom(['aaab', '..b.', '', 'a..a']);
const stored = encodeCells(cells);
expect(stored).toEqual({
tiles: ['ta', 'tb'],
origin: { x: 0, y: 0 },
rows: ['1*3 2', '0*2 2', '', '1 0*2 1'],
});
expect(decodeCells(stored)).toEqual(cells);
});
it('keep cells outside the grid, on either side of it', () => {
const cells = { '-3,-2': 'a', '40,-2': 'a', '0,25': 'b' };
const stored = encodeCells(cells);
expect(stored.origin).toEqual({ x: -3, y: -2 });
expect(stored.rows).toHaveLength(28);
expect(decodeCells(stored)).toEqual(cells);
});
it('bring a full map with several tiles back exactly', () => {
const cells = fullMap(100, (x, y) => (x < 30 ? 'water' : y % 7 === 0 ? 'path' : 'grass'));
const stored = encodeCells(cells);
// Row 0 is a path row (0 % 7), so path is met before grass.
expect(stored.tiles).toEqual(['water', 'path', 'grass']);
expect(stored.rows).toHaveLength(100);
expect(decodeCells(stored)).toEqual(cells);
});
it('bring a scattered map back exactly', () => {
let seed = 7;
const random = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
const ids = ['a', 'b', 'c', 'd'];
const cells: Record<string, string> = {};
for (let i = 0; i < 500; i++)
cells[cellKey(Math.floor(random() * 60) - 10, Math.floor(random() * 40) - 5)] =
ids[Math.floor(random() * ids.length)]!;
expect(decodeCells(encodeCells(cells))).toEqual(cells);
});
it('are written compactly by the project file and read back by it', () => {
const cells = cellsFrom(['aa.b']);
const { project, id } = projectWith(cells);
const layer = storedLayer(serializeProject(project), id);
expect(layer.cells).toBeUndefined();
expect(layer).toMatchObject({ tiles: ['ta', 'tb'], origin: { x: 0, y: 0 }, rows: ['1*2 0 2'] });
expect((findAsset(roundTrip(project), id) as TilemapAsset).layers[0]!.cells).toEqual(cells);
});
it('leave the map in memory untouched when saving', () => {
const cells = cellsFrom(['ab']);
const { project, id } = projectWith(cells);
serializeProject(project);
expect((findAsset(project, id) as TilemapAsset).layers[0]!.cells).toEqual(cells);
});
// No migration any more (#137): the map of the first version (#128) is not read.
it('read a layer holding the map itself as empty', () => {
const { project, id } = projectWith({});
const raw = serializeProject(project) as { assets: Record<string, unknown>[] };
raw.assets.find((a) => a.id === id)!.layers = [{ id: 'L1', name: 'Ground', cells: { '1,2': 'grass' } }];
const layer = (findAsset(deserializeProject(raw), id) as TilemapAsset).layers[0]!;
expect(layer).toMatchObject({ id: 'L1', name: 'Ground' });
expect(layer.cells).toEqual({});
});
it('shrink a full map of 100 × 100 with two layers to a small part of the old size', () => {
const ground = fullMap(100, (x) => (x < 50 ? 'grass' : 'dirt'));
const decor = fullMap(100, (x, y) => ((x * 7 + y * 3) % 11 === 0 ? 'flower' : 'bush'));
const old = JSON.stringify([{ cells: ground }, { cells: decor }], null, 4).length;
const now = JSON.stringify([encodeCells(ground), encodeCells(decor)], null, 4).length;
expect(now).toBeLessThan(old / 5);
});
});
describe('reading stored cells leniently', () => {
it('ends a row at a run that does not parse, since every later run would land wrongly', () => {
const cells = decodeCells({ tiles: ['a'], origin: { x: 0, y: 0 }, rows: ['1 x 1', '1*2', '1*-1 1'] });
expect(cells).toEqual({ '0,0': 'a', '0,1': 'a', '1,1': 'a' });
});
it('counts an index that names no tile as an empty cell', () => {
const cells = decodeCells({ tiles: ['a', 5], origin: { x: 0, y: 0 }, rows: ['3 2 1'] });
expect(cells).toEqual({ '2,0': 'a' });
});
it('refuses a run longer than any grid, and a row that is not a string', () => {
expect(decodeCells({ tiles: ['a'], rows: [`1*${MAX_RUN + 1}`, 7, '1'] })).toEqual({ '0,2': 'a' });
});
it('starts at the top-left corner when the origin is missing or broken', () => {
expect(decodeCells({ tiles: ['a'], origin: { x: 1.5, y: 'no' }, rows: ['0 1'] })).toEqual({ '1,0': 'a' });
});
});