2024.js/snapshots/2024-01-12-island.html

248 lines
7.9 KiB
HTML

<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 forestTiles = lowlandTiles1.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() {
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);
let timerId;
let islands = new IslandGrid(WIDTH, HEIGHT, DEFAULT_SEED);
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>