Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
- [x] Cellular automata cave/organic generator utilities
- [x] Poisson disk sampling for even point distribution
- [x] Voronoi diagram helpers for biome/territory generation
- [ ] Diamond-square terrain height map generator
- [x] Diamond-square terrain height map generator
- [ ] L-system generator for foliage and organic structures
- [ ] Dungeon generation suite (BSP subdivision, rooms & corridors variants)
- [ ] Maze algorithms pack (Recursive backtracking, Prim's, Kruskal's, Wilson's, Aldous–Broder, Recursive Division)
Expand Down
34 changes: 34 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const examples: {
readonly cellularAutomataCave: 'examples/cellularAutomata.ts';
readonly poissonDiskSampling: 'examples/poissonDisk.ts';
readonly computeVoronoiDiagram: 'examples/voronoi.ts';
readonly diamondSquare: 'examples/diamondSquare.ts';
};
readonly spatial: {
readonly Quadtree: 'examples/sat.ts';
Expand Down Expand Up @@ -387,6 +388,7 @@ export interface PoissonDiskOptions {
export function poissonDiskSampling(options: PoissonDiskOptions): Point[];

/**
<<<<<<< HEAD
* Voronoi site definition.
* Use for: labelling regions, associating metadata to cells.
* Import: procedural/voronoi.ts
Expand Down Expand Up @@ -437,6 +439,38 @@ export function computeVoronoiDiagram(
sites: ReadonlyArray<VoronoiSite>,
options?: VoronoiOptions
): VoronoiCell[];
=======
* Options configuring the diamond-square fractal algorithm.
* Use for: tuning roughness, deterministic height map generation.
* Import: procedural/diamondSquare.ts
*/
export interface DiamondSquareOptions {
size: number;
roughness?: number;
initialAmplitude?: number;
seed?: number;
normalize?: boolean;
}

/**
* Diamond-square result containing sampled grid values and extrema.
* Use for: rendering terrain meshes, post-processing passes, biome assignment.
* Import: procedural/diamondSquare.ts
*/
export interface DiamondSquareResult {
grid: number[][];
min: number;
max: number;
}

/**
* Generates fractal height maps via the diamond-square algorithm.
* Use for: terrain synthesis, cloud height fields, noise layering.
* Performance: O(size^2) where size = 2^n + 1.
* Import: procedural/diamondSquare.ts
*/
export function diamondSquare(options: DiamondSquareOptions): DiamondSquareResult;
>>>>>>> c977c3c (feat: add diamond-square terrain generator)

/**
* Simplex noise generator for smooth gradients without directional artifacts.
Expand Down
11 changes: 11 additions & 0 deletions examples/diamondSquare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { diamondSquare } from '../src/index.js';

const { grid, min, max } = diamondSquare({
size: 9,
roughness: 0.6,
seed: 42,
});

console.log('Min height:', min.toFixed(3));
console.log('Max height:', max.toFixed(3));
console.log('Sample row:', grid[4]?.map((value) => value.toFixed(3)).join(', '));
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const examples = {
cellularAutomataCave: 'examples/cellularAutomata.ts',
poissonDiskSampling: 'examples/poissonDisk.ts',
computeVoronoiDiagram: 'examples/voronoi.ts',
diamondSquare: 'examples/diamondSquare.ts',
},
spatial: {
Quadtree: 'examples/sat.ts',
Expand Down Expand Up @@ -244,11 +245,19 @@ export { cellularAutomataCave } from './procedural/cellularAutomata.js';
export { poissonDiskSampling } from './procedural/poissonDisk.js';

/**
<<<<<<< HEAD
* Voronoi diagram helper returning polygonal cells for each site.
*
* Example file: examples/voronoi.ts
*/
export { computeVoronoiDiagram } from './procedural/voronoi.js';
=======
* Diamond-square fractal terrain generator.
*
* Example file: examples/diamondSquare.ts
*/
export { diamondSquare } from './procedural/diamondSquare.js';
>>>>>>> c977c3c (feat: add diamond-square terrain generator)

// ============================================================================
// 🎯 SPATIAL & COLLISION
Expand Down
150 changes: 150 additions & 0 deletions src/procedural/diamondSquare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { createLinearCongruentialGenerator } from '../util/prng.js';

export interface DiamondSquareOptions {
/**
* Grid size must be 2^n + 1 (e.g. 5, 9, 17).
*/
size: number;
/**
* Controls how quickly perturbations shrink each iteration.
*/
roughness?: number;
/**
* Starting amplitude for corner offsets.
*/
initialAmplitude?: number;
/**
* Seed for deterministic noise generation.
*/
seed?: number;
/**
* Clamp height map to [0, 1] after generation.
*/
normalize?: boolean;
}

export interface DiamondSquareResult {
grid: number[][];
min: number;
max: number;
}

const DEFAULT_ROUGHNESS = 0.55;
const DEFAULT_AMPLITUDE = 1;

/**
* Generates a height map using the diamond-square fractal algorithm.
* Useful for: terrain synthesis, cloud layers, noise-based textures.
* Performance: O(size^2) where size = (2^n + 1).
*/
export function diamondSquare({
size,
roughness = DEFAULT_ROUGHNESS,
initialAmplitude = DEFAULT_AMPLITUDE,
seed = Date.now(),
normalize = true,
}: DiamondSquareOptions): DiamondSquareResult {
validateSize(size);
if (roughness <= 0 || roughness >= 1) {
throw new Error('roughness must be between 0 and 1 (exclusive).');
}
if (initialAmplitude <= 0) {
throw new Error('initialAmplitude must be greater than zero.');
}

const grid = Array.from({ length: size }, () => new Array<number>(size).fill(0));
const random = createLinearCongruentialGenerator(seed);
const last = size - 1;

setCorner(grid, 0, 0, randomAmplitude(random, initialAmplitude));
setCorner(grid, last, 0, randomAmplitude(random, initialAmplitude));
setCorner(grid, 0, last, randomAmplitude(random, initialAmplitude));
setCorner(grid, last, last, randomAmplitude(random, initialAmplitude));

let step = last;
let amplitude = initialAmplitude;

while (step > 1) {
const half = step / 2;

// Diamond step
for (let y = 0; y < last; y += step) {
for (let x = 0; x < last; x += step) {
const average =
(grid[y][x] + grid[y][x + step] + grid[y + step][x] + grid[y + step][x + step]) / 4;
grid[y + half][x + half] = average + randomAmplitude(random, amplitude);
}
}

// Square step
for (let y = 0; y <= last; y += half) {
for (let x = (y + half) % step; x <= last; x += step) {
let sum = 0;
let count = 0;

if (x - half >= 0) {
sum += grid[y][x - half];
count += 1;
}
if (x + half <= last) {
sum += grid[y][x + half];
count += 1;
}
if (y - half >= 0) {
sum += grid[y - half][x];
count += 1;
}
if (y + half <= last) {
sum += grid[y + half][x];
count += 1;
}

const average = sum / count;
grid[y][x] = average + randomAmplitude(random, amplitude);
}
}

amplitude *= roughness;
step = half;
}

let min = Infinity;
let max = -Infinity;
for (const row of grid) {
for (const value of row) {
if (value < min) min = value;
if (value > max) max = value;
}
}

if (normalize) {
const range = max - min || 1;
for (let y = 0; y < size; y += 1) {
for (let x = 0; x < size; x += 1) {
grid[y][x] = (grid[y][x] - min) / range;
}
}
min = 0;
max = 1;
}

return { grid, min, max };
}

function validateSize(size: number): void {
if (!Number.isInteger(size) || size < 3) {
throw new Error('size must be an integer >= 3.');
}
const exponent = Math.log2(size - 1);
if (!Number.isInteger(exponent)) {
throw new Error('size must satisfy size = 2^n + 1 (e.g. 5, 9, 17).');
}
}

function randomAmplitude(random: () => number, amplitude: number): number {
return (random() * 2 - 1) * amplitude;
}

function setCorner(grid: number[][], x: number, y: number, value: number): void {
grid[y][x] = value;
}
30 changes: 30 additions & 0 deletions tests/diamondSquare.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';

import { diamondSquare } from '../src/index.js';

describe('diamondSquare', () => {
it('produces deterministic grids for identical seeds', () => {
const options = { size: 9, seed: 123, roughness: 0.55, initialAmplitude: 1 } as const;
const a = diamondSquare(options);

Check failure on line 8 in tests/diamondSquare.test.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe call of an `any` typed value

Check failure on line 8 in tests/diamondSquare.test.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe assignment of an `any` value
const b = diamondSquare(options);

Check failure on line 9 in tests/diamondSquare.test.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe call of an `any` typed value

Check failure on line 9 in tests/diamondSquare.test.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe assignment of an `any` value
expect(a.grid).toEqual(b.grid);

Check failure on line 10 in tests/diamondSquare.test.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .grid on an `any` value

Check failure on line 10 in tests/diamondSquare.test.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .grid on an `any` value
expect(a.min).toBeCloseTo(b.min, 10);

Check failure on line 11 in tests/diamondSquare.test.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .min on an `any` value

Check failure on line 11 in tests/diamondSquare.test.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe argument of type `any` assigned to a parameter of type `number`

Check failure on line 11 in tests/diamondSquare.test.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .min on an `any` value
expect(a.max).toBeCloseTo(b.max, 10);

Check failure on line 12 in tests/diamondSquare.test.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .max on an `any` value
});

it('normalizes heights to [0, 1] when requested', () => {
const { grid, min, max } = diamondSquare({
size: 17,
seed: 99,
roughness: 0.6,
initialAmplitude: 2,
normalize: true,
});

expect(min).toBe(0);
expect(max).toBe(1);
for (const row of grid) {
expect(row.every((value) => value >= 0 && value <= 1)).toBe(true);
}
});
});
2 changes: 2 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('package entry point', () => {
expect(examples.procedural.cellularAutomataCave).toBe('examples/cellularAutomata.ts');
expect(examples.procedural.poissonDiskSampling).toBe('examples/poissonDisk.ts');
expect(examples.procedural.computeVoronoiDiagram).toBe('examples/voronoi.ts');
expect(examples.procedural.diamondSquare).toBe('examples/diamondSquare.ts');
expect(examples.search.Trie).toBe('examples/search.ts');
expect(examples.pathfinding.buildNavMesh).toBe('examples/navMesh.ts');
});
Expand Down Expand Up @@ -52,6 +53,7 @@ describe('package entry point', () => {
| 'cellularAutomataCave'
| 'poissonDiskSampling'
| 'computeVoronoiDiagram'
| 'diamondSquare'
>();
});
});