import { canvas2d, h } from "./lib/html"; import { Prng, mulberry32 } from "./lib/prng"; const BLOWUP = 4; const WIDTH = 240; const HEIGHT = 135; const DEFAULT_SEED = 128; type Lookup2d = (x: number, y: number) => number; function dim(width: number, height: number): Lookup2d { return function xy(x: number, y: number) { return ( (((x % width) + width) % width) + width * (((y % height) + height) % height) ); }; } class IslandGrid { data: number[]; rng: Prng; xy: Lookup2d; basePos: number; lobePos1: number; lobePos2: number; done = false; constructor(public width: number, public height: number, seed: number) { this.data = Array(width * height).fill(0); this.rng = mulberry32(seed); this.xy = dim(width, height); this.basePos = this.data.length >> 1; this.lobePos1 = this.xy( (WIDTH >> 1) + (this.rng() % 48) - 24, (HEIGHT >> 1) + (this.rng() % 48) - 24 ); this.lobePos2 = this.xy( (WIDTH >> 1) + (this.rng() % 48) - 24, (HEIGHT >> 1) + (this.rng() % 48) - 24 ); } public get(x: number, y: number): number { return this.data[this.xy(x, y)]; } public set(x: number, y: number, tile: number) { this.data[this.xy(x, y)] = tile; console.log(x, y, this.xy(x, y), this.data[this.xy(x, y)]); } public floodSearch( startPos: number, shouldExpand: (tile: number) => boolean ): number[] { const len = this.data.length; const width = this.width; const seen = new Uint8Array(len); const hitPositions: number[] = []; function enqueue(pos: number) { if (!seen[pos]) { hitPositions.push(pos); seen[pos] = 1; } } enqueue(startPos); for (let i = 0; i < hitPositions.length; i++) { const pos = hitPositions[i]; if (shouldExpand(this.data[pos])) { enqueue((pos - width) % len); enqueue((pos - 1) % len); enqueue((pos + 1) % len); enqueue((pos + width) % len); } } return hitPositions; } public drop(pos: number): void { const lowerNeighbors: number[] = []; const check = (adjPos: number) => { if (this.data[adjPos] < this.data[pos]) { lowerNeighbors.push(adjPos); } }; // try to roll in cardinal directions check((pos - this.width) % this.data.length); check((pos - 1) % this.data.length); check((pos + 1) % this.data.length); check((pos + this.width) % this.data.length); if (lowerNeighbors.length > 0) { const downhill = lowerNeighbors[this.rng() % lowerNeighbors.length]; return this.drop(downhill); } // try to roll in diagonal directions check((pos - this.width - 1) % this.data.length); check((pos - this.width + 1) % this.data.length); check((pos + this.width - 1) % this.data.length); check((pos + this.width + 1) % this.data.length); if (lowerNeighbors.length > 0) { const downhill = lowerNeighbors[this.rng() % lowerNeighbors.length]; return this.drop(downhill); } // flat, increase elevation const newValue = ++this.data[pos]; if (newValue == 9) { this.done = true; } } public dropWithin(tiles: number[]) { if (tiles.length > 0) { this.drop(tiles[this.rng() % tiles.length]); } } public step() { const lowlandTiles1 = this.floodSearch(this.lobePos1, (tile) => tile > 0); const lowlandTiles2 = this.floodSearch(this.lobePos2, (tile) => tile > 0); // grow shore const shoreTiles1 = lowlandTiles1.filter((pos) => this.data[pos] == 0); this.dropWithin(shoreTiles1); const shoreTiles2 = lowlandTiles2.filter((pos) => this.data[pos] == 0); this.dropWithin(shoreTiles2); // seed beach const beachTiles = lowlandTiles1.filter((pos) => this.data[pos] == 1); this.dropWithin(beachTiles); // expand forest const forestTiles = lowlandTiles1.filter((pos) => this.data[pos] == 2); this.dropWithin(forestTiles); this.dropWithin(forestTiles); this.dropWithin(forestTiles); // form mountain const mountainTiles = this.floodSearch(this.basePos, (tile) => tile > 4); this.dropWithin(mountainTiles); // GENERATOR // const erodePos = islandTiles[islands.rng() % islandTiles.length]; // islands.data[erodePos] = Math.max(islands.data[erodePos] - 1, 0); } } function renderIslands(islands: IslandGrid, cx: CanvasRenderingContext2D) { for (let y = 0; y < islands.height; y++) { for (let x = 0; x < islands.width; x++) { const tile = islands.data[islands.xy(x, y)]; switch (tile) { case 0: cx.fillStyle = "blue"; break; case 1: cx.fillStyle = "yellow"; break; case 2: cx.fillStyle = "#00ff00"; break; case 3: cx.fillStyle = "#008800"; break; case 4: case 5: case 6: case 7: case 8: cx.fillStyle = "#666666"; break; default: cx.fillStyle = "#88aaff"; break; } cx.fillRect(x, y, 1, 1); } } } export function IslandApplet() { const [canvas, cx] = canvas2d({ width: WIDTH * BLOWUP, height: HEIGHT * BLOWUP, }); cx.scale(BLOWUP, BLOWUP); const seedInput = h("input", { type: "number", valueAsNumber: DEFAULT_SEED }); const seedLabel = h("label", {}, "Seed:", seedInput); // STATE let timerId: number; let islands = new IslandGrid(WIDTH, HEIGHT, DEFAULT_SEED); // CONTROLS const generateButton = h( "button", { onclick: () => { clearInterval(timerId); islands = new IslandGrid(WIDTH, HEIGHT, seedInput.valueAsNumber); timerId = setInterval(function tick() { islands.step(); islands.step(); islands.step(); if (islands.done) { clearInterval(timerId); } renderIslands(islands, cx); }, 1000 / 30); }, }, "Generate" ); renderIslands(islands, cx); return [canvas, seedLabel, generateButton]; } (globalThis as any).IslandApplet = IslandApplet;