<html> <head> <title>ProcGen Island</title> </head> <body> <script> "use strict"; (() => { // 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]; } // lib/prng.ts 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.ts var BLOWUP = 4; var WIDTH = 240; var HEIGHT = 135; var DEFAULT_SEED = 128; function dim(width, height) { return function xy(x, y) { return ( (((x % width) + width) % width) + width * (((y % height) + height) % height) ); }; } var IslandGrid = class { constructor(width, height, seed) { this.width = width; this.height = height; 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 ); } data; rng; xy; basePos; lobePos1; lobePos2; done = false; get(x, y) { return this.data[this.xy(x, y)]; } set(x, y, tile) { this.data[this.xy(x, y)] = tile; console.log(x, y, this.xy(x, y), this.data[this.xy(x, y)]); } 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); } }; 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); } 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); } const newValue = ++this.data[pos]; if (newValue == 9) { this.done = true; } } dropWithin(tiles) { if (tiles.length > 0) { this.drop(tiles[this.rng() % tiles.length]); } } step() { const lowlandTiles1 = this.floodSearch( this.lobePos1, (tile) => tile > 0 ); const lowlandTiles2 = this.floodSearch( this.lobePos2, (tile) => tile > 0 ); const shoreTiles1 = lowlandTiles1.filter( (pos) => this.data[pos] == 0 ); this.dropWithin(shoreTiles1); const shoreTiles2 = lowlandTiles2.filter( (pos) => this.data[pos] == 0 ); this.dropWithin(shoreTiles2); const beachTiles = lowlandTiles1.filter( (pos) => this.data[pos] == 1 ); this.dropWithin(beachTiles); const forestLobe = this.floodSearch( this.lobePos1, (tile) => tile > 1 ); const forestTiles = forestLobe.filter((pos) => this.data[pos] == 2); this.dropWithin(forestTiles); this.dropWithin(forestTiles); this.dropWithin(forestTiles); const mountainTiles = this.floodSearch( this.basePos, (tile) => tile > 4 ); this.dropWithin(mountainTiles); } }; 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 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); } } } function IslandApplet() { let timerId; 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); 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); }, 1e3 / 30); }, }, "Generate" ); renderIslands(islands, cx); return [canvas, seedLabel, generateButton]; } globalThis.IslandApplet = IslandApplet; })(); document.body.append(...IslandApplet()); </script> </body> </html>