publish more complex generator

This commit is contained in:
Tangent Wantwight 2024-01-13 17:53:48 -05:00
parent a096c81c03
commit 8d6da965b3
1 changed files with 489 additions and 0 deletions

View File

@ -0,0 +1,489 @@
<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>