<html> <head> <title>ProcGen Island</title> </head> <body> <script> "use strict"; (() => { // lib/prng.ts var UINT_MAX = 4294967295; function mulberry32(state) { return function () { let t = (state += 1831565813); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return (t ^ (t >>> 14)) >>> 0; }; } // island/data.ts var WATER = 0; var BEACH = 1; var LIGHT_FOREST = 2; var DENSE_FOREST = 3; var MOUNTAIN = 4; var ICECAP = 7; // island/generators.ts var SMALL_MOUNTAIN = (islands, basePos) => () => { const islandTiles = islands.floodSearch( basePos, (tile) => tile > WATER ); const edgeTiles = islandTiles.filter( (pos) => islands.data[pos] <= BEACH ); islands.dropWithin(edgeTiles); const mountainTiles = islands.floodSearch( basePos, (tile) => tile > MOUNTAIN ); islands.dropWithin(mountainTiles); return true; }; var BIG_MOUNTAIN = (islands, basePos) => () => { const mountainTiles = islands.floodSearch( basePos, (tile) => tile > MOUNTAIN ); islands.dropWithin(mountainTiles); return mountainTiles.some((pos) => islands.data[pos] == ICECAP); }; var SMALL_BEACH = (islands, basePos) => () => { const islandTiles = islands.floodSearch( basePos, (tile) => tile > WATER ); const shoreTiles = islandTiles.filter( (pos) => islands.data[pos] <= WATER ); islands.dropWithin(shoreTiles); return true; }; var BIG_BEACH = (islands, basePos) => { let dropped = 0; return () => { const islandTiles = islands.floodSearch( basePos, (tile) => tile > WATER ); const shoreTiles = islandTiles.filter( (pos) => islands.data[pos] <= WATER ); islands.dropWithin(shoreTiles); dropped++; const landTiles = islandTiles.filter( (pos) => islands.data[pos] > WATER ); return landTiles.length > dropped; }; }; var SCATTERED_FOREST = (islands, basePos) => () => { const islandTiles = islands.floodSearch( basePos, (tile) => tile > WATER ); const shoreTiles = islandTiles.filter( (pos) => islands.data[pos] <= WATER ); islands.dropWithin(shoreTiles); const beachTiles = islandTiles.filter( (pos) => islands.data[pos] == BEACH ); islands.dropWithin(beachTiles); islands.dropWithin(beachTiles); const forestLobe = islands.floodSearch( basePos, (tile) => tile > WATER ); const forestTiles = forestLobe.filter( (pos) => islands.data[pos] == LIGHT_FOREST ); islands.dropWithin(forestTiles); return true; }; var CONTIGUOUS_FOREST = (islands, basePos) => () => { const islandTiles = islands.floodSearch( basePos, (tile) => tile > WATER ); const shoreTiles = islandTiles.filter( (pos) => islands.data[pos] <= WATER ); islands.dropWithin(shoreTiles); const beachTiles = islandTiles.filter( (pos) => islands.data[pos] == BEACH ); islands.dropWithin(beachTiles); const forestLobe = islands.floodSearch( basePos, (tile) => tile > BEACH ); const forestTiles = forestLobe.filter( (pos) => islands.data[pos] == LIGHT_FOREST ); islands.dropWithin(forestTiles); islands.dropWithin(forestTiles); return true; }; var HILLY_FOREST = (islands, basePos) => () => { const islandTiles = islands.floodSearch( basePos, (tile) => tile > WATER ); const shoreTiles = islandTiles.filter( (pos) => islands.data[pos] <= WATER ); islands.dropWithin(shoreTiles); const beachTiles = islandTiles.filter( (pos) => islands.data[pos] == BEACH ); islands.dropWithin(beachTiles); const centralForest = islands.floodSearch( basePos, (tile) => tile > BEACH ); islands.dropWithin(centralForest); const edgeTiles = centralForest.filter( (pos) => islands.data[pos] == LIGHT_FOREST ); islands.dropWithin(edgeTiles); const hillTiles = centralForest.filter( (pos) => islands.data[pos] >= MOUNTAIN ); return hillTiles.length > 10; }; var ERODED_BEACH = (islands, basePos) => () => { const islandTiles = islands.floodSearch( basePos, (tile) => tile > WATER ); const shoreTiles = islandTiles.filter( (pos) => islands.data[pos] <= WATER ); islands.dropWithin(shoreTiles); if (islandTiles.length > 1) { const erodePos = islandTiles[islands.rng() % islandTiles.length]; islands.data[erodePos] = Math.max( islands.data[erodePos] - 1, WATER ); } return true; }; var NO_ISLAND = (islands, basePos) => () => { return true; }; var SINKHOLE_BURNOUT = 1500; var SINKHOLE = (islands, basePos) => { let ticks = 0; return () => { if (ticks++ < SINKHOLE_BURNOUT) { const sunk = islands.floodSearch(basePos, (tile) => tile < 0); islands.sinkhole(islands.choose(sunk)); } return true; }; }; var WIDE_SINKHOLE = (islands, basePos) => { let ticks = 0; return () => { if (ticks++ < SINKHOLE_BURNOUT) { const sunk = islands.floodSearch(basePos, (tile) => tile < 0); const sunkEdge = sunk.filter((pos) => islands.data[pos] >= WATER); if (sunkEdge.length > 0) { islands.sinkhole(islands.choose(sunkEdge)); } else { islands.sinkhole(islands.choose(sunk)); } } return true; }; }; var BIG_ISLANDS = [BIG_MOUNTAIN, BIG_BEACH, HILLY_FOREST]; var ROCKY_ISLANDS = [SMALL_MOUNTAIN, BIG_MOUNTAIN, HILLY_FOREST]; var GREEN_ISLANDS = [SCATTERED_FOREST, CONTIGUOUS_FOREST, HILLY_FOREST]; var SMALL_ISLANDS = [SMALL_BEACH, SCATTERED_FOREST, ERODED_BEACH]; var ALL_ISLANDS = [ SMALL_MOUNTAIN, BIG_MOUNTAIN, SMALL_BEACH, BIG_BEACH, SCATTERED_FOREST, CONTIGUOUS_FOREST, HILLY_FOREST, ERODED_BEACH, ]; var VOIDS = [NO_ISLAND, SINKHOLE, WIDE_SINKHOLE]; // island/grid.ts var IslandGrid = class { constructor(width, height, seed) { this.width = width; this.height = height; this.data = Array(width * height).fill(WATER); this.rng = mulberry32(seed); const islandBag = this.shuffle([ this.choose(BIG_ISLANDS), this.choose(ROCKY_ISLANDS), this.choose(GREEN_ISLANDS), this.choose(SMALL_ISLANDS), this.choose(SMALL_ISLANDS), this.choose(ALL_ISLANDS), this.choose(ALL_ISLANDS), this.choose(VOIDS), this.choose(VOIDS), this.choose(VOIDS), this.choose(VOIDS), NO_ISLAND, NO_ISLAND, ]); const islandCount = islandBag.length; const spacing = (Math.PI * 2) / islandCount; const rootX = width / 2; const rootY = height / 2; const xScale = width / 4; const yScale = height / 4; for (let i = 0; i < islandCount; i++) { const rScale = (this.rng() / UINT_MAX) * 2 - 1; const y = rootY + Math.sin(spacing * i) * yScale * rScale; const x = rootX + Math.cos(spacing * i) * xScale * rScale; this.generators.push(islandBag[i](this, this.xy(x | 0, y | 0))); } } data; rng; generators = []; done = false; xy(x, y) { return ( (((x % this.width) + this.width) % this.width) + this.width * (((y % this.height) + this.height) % this.height) ); } floodSearch(startPos, shouldExpand) { const len = this.data.length; const width = this.width; const seen = new Uint8Array(len); const hitPositions = []; function enqueue(pos) { 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; } drop(pos) { const lowerNeighbors = []; const check = (adjPos) => { if (this.data[adjPos] < this.data[pos]) { lowerNeighbors.push(adjPos); } }; this.forCardinals(pos, check); if (lowerNeighbors.length > 0) { const downhill = lowerNeighbors[this.rng() % lowerNeighbors.length]; return this.drop(downhill); } this.forDiagonals(pos, check); if (lowerNeighbors.length > 0) { const downhill = lowerNeighbors[this.rng() % lowerNeighbors.length]; return this.drop(downhill); } ++this.data[pos]; } sinkhole(pos) { const higherNeighbors = []; this.forNeighbors(pos, (adjPos) => { if (this.data[adjPos] > this.data[pos]) { higherNeighbors.push(adjPos); } }); if (higherNeighbors.length > 0) { const uphill = higherNeighbors[this.rng() % higherNeighbors.length]; return this.sinkhole(uphill); } this.data[pos] = Math.max(this.data[pos] - 1, -3); } forCardinals(pos, action) { action((pos - this.width) % this.data.length); action((pos - 1) % this.data.length); action((pos + 1) % this.data.length); action((pos + this.width) % this.data.length); } forDiagonals(pos, action) { action((pos - this.width - 1) % this.data.length); action((pos - this.width + 1) % this.data.length); action((pos + this.width - 1) % this.data.length); action((pos + this.width + 1) % this.data.length); } forNeighbors(pos, action) { this.forCardinals(pos, action); this.forDiagonals(pos, action); } deepenWater() { for (let i = 0; i < this.data.length; i++) { if (this.data[i] == WATER) { let isShore = false; this.forNeighbors(i, (adjPos) => { if (this.data[adjPos] > WATER) { isShore = true; } }); if (!isShore) { this.data[i] = WATER - 1; } } } } choose(list) { if (list.length == 0) { throw new Error("Picking from empty list"); } return list[this.rng() % list.length]; } shuffle(list) { const shuffled = list.slice(); for (let i = 0; i < shuffled.length - 1; i++) { const swapIndex = (this.rng() % (shuffled.length - i)) + i; [shuffled[i], shuffled[swapIndex]] = [ shuffled[swapIndex], shuffled[i], ]; } return shuffled; } dropWithin(tiles) { if (tiles.length > 0) { this.drop(this.choose(tiles)); } } step() { this.done = this.generators .map((generator) => generator()) .every((done) => done); } }; // island/render.ts function renderIslands(islands, cx) { 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 -3: case -2: case -1: cx.fillStyle = "#000088"; break; case WATER: cx.fillStyle = "blue"; break; case BEACH: cx.fillStyle = "yellow"; break; case LIGHT_FOREST: cx.fillStyle = "#00ff00"; break; case DENSE_FOREST: cx.fillStyle = "#008800"; break; case MOUNTAIN: case MOUNTAIN + 1: case MOUNTAIN + 2: cx.fillStyle = "#666666"; break; default: cx.fillStyle = "#88aaff"; break; } cx.fillRect(x, y, 1, 1); } } } // lib/html.ts function h(name, props, ...children) { const element = Object.assign(document.createElement(name), props); element.append(...children); return element; } function canvas2d(props) { const canvas = h("canvas", props); const cx = canvas.getContext("2d"); if (!cx) { throw new Error("2D rendering context not supported"); } return [canvas, cx]; } // island.ts var BLOWUP = 4; var WIDTH = 240; var HEIGHT = 135; var DEFAULT_SEED = 128; function IslandApplet() { let timerId; let ticks = 0; let islands = new IslandGrid(WIDTH, HEIGHT, DEFAULT_SEED); 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); const generateButton = h( "button", { onclick: () => { clearInterval(timerId); ticks = 0; islands = new IslandGrid( WIDTH, HEIGHT, seedInput.valueAsNumber ); timerId = setInterval(function tick() { islands.step(); islands.step(); islands.step(); ticks += 3; if (islands.done) { clearInterval(timerId); islands.deepenWater(); } renderIslands(islands, cx); }, 1e3 / 30); }, }, "Generate" ); renderIslands(islands, cx); return [canvas, seedLabel, generateButton]; } globalThis.IslandApplet = IslandApplet; })(); document.body.append(...IslandApplet()); </script> </body> </html>