publish more complex generator
This commit is contained in:
parent
a096c81c03
commit
8d6da965b3
1 changed files with 489 additions and 0 deletions
489
snapshots/2024-01-13-archipelago.html
Normal file
489
snapshots/2024-01-13-archipelago.html
Normal 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>
|
Loading…
Add table
Reference in a new issue