Files
synthesis/tests/world.test.ts
Денис Шкабатур bc472d0f77 Phase 3: World generation — procedural tilemap for Catalytic Wastes
- simplex-noise with seeded PRNG (mulberry32) for deterministic generation
- Biome data: 8 tile types (scorched earth, cracked ground, ash sand,
  acid pools, acid shallow, crystals, geysers, mineral veins)
- Elevation noise → base terrain; detail noise → geyser/mineral overlays
- Canvas-based tileset with per-pixel brightness variation
- Phaser tilemap with collision on non-walkable tiles
- Camera: WASD movement, mouse wheel zoom (0.5x–3x), world bounds
- Minimap: 160x160 canvas overview with viewport indicator
- 21 world generation tests passing (95 total)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 12:47:21 +03:00

184 lines
6.3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import biomeDataArray from '../src/data/biomes.json';
import { createSeededNoise, sampleNoise } from '../src/world/noise';
import { generateWorld } from '../src/world/generator';
import type { BiomeData } from '../src/world/types';
// Load the first biome — structural compatibility with BiomeData
const biome = biomeDataArray[0] as BiomeData;
// ─── Noise ──────────────────────────────────────────────────────
describe('Seeded Noise', () => {
it('is deterministic with same seed', () => {
const n1 = createSeededNoise(42);
const n2 = createSeededNoise(42);
expect(n1(0.5, 0.5)).toBe(n2(0.5, 0.5));
});
it('produces different results with different seeds', () => {
const n1 = createSeededNoise(42);
const n2 = createSeededNoise(99);
expect(n1(0.5, 0.5)).not.toBe(n2(0.5, 0.5));
});
it('normalizes to [0, 1] via sampleNoise', () => {
const noise = createSeededNoise(42);
for (let i = 0; i < 200; i++) {
const val = sampleNoise(noise, i * 0.3, i * 0.7, 0.1);
expect(val).toBeGreaterThanOrEqual(0);
expect(val).toBeLessThanOrEqual(1);
}
});
it('varies across coordinates', () => {
const noise = createSeededNoise(42);
const values = new Set<number>();
for (let i = 0; i < 50; i++) {
values.add(sampleNoise(noise, i, 0, 0.1));
}
// Should have significant variety (not all same value)
expect(values.size).toBeGreaterThan(20);
});
});
// ─── Biome Data ─────────────────────────────────────────────────
describe('Biome Data', () => {
it('has valid structure', () => {
expect(biome.id).toBe('catalytic-wastes');
expect(biome.tileSize).toBe(32);
expect(biome.mapWidth).toBe(80);
expect(biome.mapHeight).toBe(80);
});
it('has 8 tile types', () => {
expect(biome.tiles).toHaveLength(8);
});
it('tile IDs are sequential starting from 0', () => {
biome.tiles.forEach((tile, index) => {
expect(tile.id).toBe(index);
});
});
it('has both walkable and non-walkable tiles', () => {
const walkable = biome.tiles.filter(t => t.walkable);
const blocked = biome.tiles.filter(t => !t.walkable);
expect(walkable.length).toBeGreaterThan(0);
expect(blocked.length).toBeGreaterThan(0);
});
it('elevation rules cover full [0, 1] range', () => {
const rules = biome.generation.elevationRules;
expect(rules.length).toBeGreaterThan(0);
// Last rule should cover up to 1.0
expect(rules[rules.length - 1].below).toBe(1);
// Rules should be sorted ascending
for (let i = 1; i < rules.length; i++) {
expect(rules[i].below).toBeGreaterThan(rules[i - 1].below);
}
});
it('all elevation rule tileIds reference valid tiles', () => {
const validIds = new Set(biome.tiles.map(t => t.id));
for (const rule of biome.generation.elevationRules) {
expect(validIds.has(rule.tileId)).toBe(true);
}
});
});
// ─── World Generation ───────────────────────────────────────────
describe('World Generation', () => {
it('generates grid with correct dimensions', () => {
const world = generateWorld(biome, 42);
expect(world.grid).toHaveLength(biome.mapHeight);
for (const row of world.grid) {
expect(row).toHaveLength(biome.mapWidth);
}
});
it('all tile IDs in grid are valid', () => {
const world = generateWorld(biome, 42);
const validIds = new Set(biome.tiles.map(t => t.id));
for (const row of world.grid) {
for (const tileId of row) {
expect(validIds.has(tileId)).toBe(true);
}
}
});
it('is deterministic — same seed same map', () => {
const w1 = generateWorld(biome, 42);
const w2 = generateWorld(biome, 42);
expect(w1.grid).toEqual(w2.grid);
});
it('different seeds produce different maps', () => {
const w1 = generateWorld(biome, 42);
const w2 = generateWorld(biome, 99);
expect(w1.grid).not.toEqual(w2.grid);
});
it('stores seed and biome reference', () => {
const world = generateWorld(biome, 12345);
expect(world.seed).toBe(12345);
expect(world.biome).toBe(biome);
});
it('has diverse tile distribution (no single tile > 60%)', () => {
const world = generateWorld(biome, 42);
const counts = new Map<number, number>();
for (const row of world.grid) {
for (const tileId of row) {
counts.set(tileId, (counts.get(tileId) ?? 0) + 1);
}
}
const total = biome.mapWidth * biome.mapHeight;
// At least 4 different tile types present
expect(counts.size).toBeGreaterThanOrEqual(4);
// No single type dominates
for (const count of counts.values()) {
expect(count / total).toBeLessThan(0.6);
}
});
it('generates acid pools (low elevation)', () => {
const world = generateWorld(biome, 42);
const acidId = biome.tiles.find(t => t.name === 'acid-pool')?.id;
const hasAcid = world.grid.some(row => row.includes(acidId!));
expect(hasAcid).toBe(true);
});
it('generates crystal formations (high elevation)', () => {
const world = generateWorld(biome, 42);
const crystalId = biome.tiles.find(t => t.name === 'crystal')?.id;
const hasCrystals = world.grid.some(row => row.includes(crystalId!));
expect(hasCrystals).toBe(true);
});
it('generates mineral veins (overlay on ground)', () => {
const world = generateWorld(biome, 42);
const mineralId = biome.tiles.find(t => t.name === 'mineral-vein')?.id;
const hasMinerals = world.grid.some(row => row.includes(mineralId!));
expect(hasMinerals).toBe(true);
});
it('generates geysers (overlay near acid)', () => {
const world = generateWorld(biome, 42);
const geyserId = biome.tiles.find(t => t.name === 'geyser')?.id;
const hasGeysers = world.grid.some(row => row.includes(geyserId!));
expect(hasGeysers).toBe(true);
});
it('produces unique map every seed (sample 5 seeds)', () => {
const grids = [1, 2, 3, 4, 5].map(s => generateWorld(biome, s).grid);
for (let i = 0; i < grids.length; i++) {
for (let j = i + 1; j < grids.length; j++) {
expect(grids[i]).not.toEqual(grids[j]);
}
}
});
});