172 lines
4 KiB
TypeScript
172 lines
4 KiB
TypeScript
import { canvas2d } from "./lib/html";
|
|
import { Prng, mulberry32 } from "./lib/prng";
|
|
|
|
const BLOWUP = 4 * 5;
|
|
const WIDTH = 240 / 5;
|
|
const HEIGHT = 135 / 5;
|
|
|
|
type Lookup2d = (x: number, y: number) => number;
|
|
function dim(width: number, height: number): Lookup2d {
|
|
return function xy(x: number, y: number) {
|
|
return (
|
|
(((x % width) + width) % width) +
|
|
width * (((y % height) + height) % height)
|
|
);
|
|
};
|
|
}
|
|
|
|
class IslandGrid {
|
|
data: number[];
|
|
rng: Prng;
|
|
|
|
xy: Lookup2d;
|
|
|
|
constructor(public width: number, public height: number, seed: number) {
|
|
this.data = Array(width * height).fill(0);
|
|
this.rng = mulberry32(seed);
|
|
this.xy = dim(width, height);
|
|
}
|
|
|
|
public get(x: number, y: number): number {
|
|
return this.data[this.xy(x, y)];
|
|
}
|
|
|
|
public set(x: number, y: number, tile: number) {
|
|
this.data[this.xy(x, y)] = tile;
|
|
console.log(x, y, this.xy(x, y), this.data[this.xy(x, y)]);
|
|
}
|
|
|
|
public floodSearch(
|
|
startPos: number,
|
|
shouldExpand: (tile: number) => boolean
|
|
): number[] {
|
|
const len = this.data.length;
|
|
const width = this.width;
|
|
const seen = new Uint8Array(len);
|
|
|
|
const hitPositions: number[] = [];
|
|
|
|
function enqueue(pos: number) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
function renderIslands(islands: IslandGrid, cx: CanvasRenderingContext2D) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function IslandApplet() {
|
|
const [canvas, cx] = canvas2d({
|
|
width: WIDTH * BLOWUP,
|
|
height: HEIGHT * BLOWUP,
|
|
});
|
|
cx.scale(BLOWUP, BLOWUP);
|
|
|
|
const islands = new IslandGrid(WIDTH, HEIGHT, 128);
|
|
|
|
const len = islands.data.length;
|
|
const basePos = len >> 1;
|
|
const width = islands.width;
|
|
|
|
function drop(pos: number) {
|
|
const lowerNeighbors: number[] = [];
|
|
|
|
function check(adjPos: number) {
|
|
if (islands.data[adjPos] < islands.data[pos]) {
|
|
lowerNeighbors.push(adjPos);
|
|
}
|
|
}
|
|
|
|
// try to roll in cardinal directions
|
|
|
|
check((pos - width) % len);
|
|
check((pos - 1) % len);
|
|
check((pos + 1) % len);
|
|
check((pos + width) % len);
|
|
|
|
if (lowerNeighbors.length > 0) {
|
|
const downhill = lowerNeighbors[islands.rng() % lowerNeighbors.length];
|
|
return drop(downhill);
|
|
}
|
|
|
|
// try to roll in diagonal directions
|
|
|
|
check((pos - width - 1) % len);
|
|
check((pos - width + 1) % len);
|
|
check((pos + width - 1) % len);
|
|
check((pos + width + 1) % len);
|
|
|
|
if (lowerNeighbors.length > 0) {
|
|
const downhill = lowerNeighbors[islands.rng() % lowerNeighbors.length];
|
|
return drop(downhill);
|
|
}
|
|
|
|
// flat, increase elevation
|
|
islands.data[pos]++;
|
|
}
|
|
|
|
function tick() {
|
|
const islandTiles = islands.floodSearch(basePos, (tile) => tile > 0);
|
|
|
|
drop(islandTiles[islands.rng() % islandTiles.length]);
|
|
drop(islandTiles[islands.rng() % islandTiles.length]);
|
|
drop(basePos - 8);
|
|
|
|
renderIslands(islands, cx);
|
|
}
|
|
|
|
tick();
|
|
|
|
setInterval(tick, 1000 / 10);
|
|
|
|
return [canvas];
|
|
}
|
|
|
|
(globalThis as any).IslandApplet = IslandApplet;
|