import { describe, expect, it } from 'vitest';
import { treeModel, boulderModel } from '../generators';
import { decodeModel } from '../format';
import { VoxelWorld } from '../../world/world';
import { stampModel } from '../stamp';
const NEI = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]] as const;
describe('генераторы деревьев', () => {
it('детерминированы: один (вид, сид) → идентичная модель', () => {
for (const v of [0, 1, 2] as const) {
expect(JSON.stringify(treeModel(v, 7))).toBe(JSON.stringify(treeModel(v, 7)));
}
});
it('разные сиды дают разные данные', () => {
expect(treeModel(0, 1).data).not.toBe(treeModel(0, 2).data);
});
it('варианты различаются при одном сиде', () => {
const models = [treeModel(0, 5), treeModel(1, 5), treeModel(2, 5)];
expect(new Set(models.map((m) => JSON.stringify(m))).size).toBe(3);
});
for (const v of [0, 1, 2] as const) {
it(`вид ${v}: непустая, ствол есть и связан с кроной`, () => {
const g = decodeModel(treeModel(v, 123));
expect(g.count()).toBeGreaterThan(0);
let trunk = 0, linked = false;
for (let z = 0; z < g.sz; z++)
for (let y = 0; y < g.sy; y++)
for (let x = 0; x < g.sx; x++) {
if (g.get(x, y, z) !== 4) continue;
trunk++;
for (const [dx, dy, dz] of NEI) {
const n = g.get(x + dx, y + dy, z + dz);
if (n === 3 || n === 5) linked = true;
}
}
expect(trunk).toBeGreaterThan(0);
expect(linked).toBe(true);
});
}
});
describe('генераторы валунов', () => {
it('детерминированы; разные сиды различаются', () => {
expect(JSON.stringify(boulderModel(1, 9))).toBe(JSON.stringify(boulderModel(1, 9)));
expect(boulderModel(1, 9).data).not.toBe(boulderModel(1, 10).data);
});
for (const v of [0, 1, 2] as const) {
it(`вид ${v}: непустой, стоит на земле, свет на верхушках столбцов`, () => {
const g = decodeModel(boulderModel(v, 77));
expect(g.count()).toBeGreaterThan(0);
let ground = 0;
for (let z = 0; z < g.sz; z++)
for (let x = 0; x < g.sx; x++) {
let top = -1;
for (let y = 0; y < g.sy; y++) {
const c = g.get(x, y, z);
if (c !== 0) { if (y === 0) ground++; top = y; }
}
if (top >= 0) expect(g.get(x, top, z)).toBe(7); // свет сверху
}
expect(ground).toBeGreaterThan(0);
});
}
it('вариант = размер: s растёт с видом', () => {
expect(boulderModel(0, 3).size[0]).toBe(5);
expect(boulderModel(2, 3).size[0]).toBe(7);
});
});
describe('штамповка в мир', () => {
/** Пустой мир с «землёй» y=0..1 в 2×2 чанках. */
function groundWorld(): VoxelWorld {
const w = new VoxelWorld(24);
for (let x = 0; x < 32; x++) for (let z = 0; z < 32; z++) w.set(x, 0, z, 2);
w.takeDirty();
return w;
}
it('ставит все воксели модели и возвращает их число', () => {
const w = groundWorld();
const m = boulderModel(0, 5);
const expected = decodeModel(m).count();
expect(stampModel(w, m, 4, 2, 4)).toBe(expected);
// в области штампа столько непустых вокселей, сколько в модели
let nonzero = 0;
const [sx, sy, sz] = m.size;
for (let ly = 0; ly < sy; ly++)
for (let lz = 0; lz < sz; lz++)
for (let lx = 0; lx < sx; lx++) if (w.get(4 + lx, 2 + ly, 4 + lz) !== 0) nonzero++;
expect(nonzero).toBe(expected);
});
it('клиппинг обрезает выходящую за границу часть', () => {
const w = groundWorld();
const m = treeModel(0, 5);
const full = decodeModel(m);
// maxX=6 (half-open): столбцы x≥6 мира не ставятся
const placed = stampModel(w, m, 2, 2, 2, { maxX: 6 });
let clippedOut = 0;
for (let ly = 0; ly < full.sy; ly++)
for (let lz = 0; lz < full.sz; lz++)
for (let lx = 0; lx < full.sx; lx++)
if (full.get(lx, ly, lz) !== 0 && 2 + lx >= 6) clippedOut++;
expect(placed).toBe(full.count() - clippedOut);
expect(placed).toBeLessThan(full.count());
});
it('полный клиппинг → 0 поставленных, мир не растёт', () => {
const w = groundWorld();
const chunks = w.chunkCount;
const m = treeModel(1, 5);
expect(stampModel(w, m, 100, 2, 100, { minX: 0, maxX: 32, minZ: 0, maxZ: 32 })).toBe(0);
expect(w.chunkCount).toBe(chunks);
});
it('вне вертикали мира (y) не ставится', () => {
const w = new VoxelWorld(4);
const m = treeModel(1, 5);
expect(stampModel(w, m, 0, 3, 0)).toBeLessThan(decodeModel(m).count());
});
});