249 lines
8 KiB
HTML
249 lines
8 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 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>
|