- 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>
184 lines
6.3 KiB
TypeScript
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]);
|
|
}
|
|
}
|
|
});
|
|
});
|