2024.js/island.ts

181 lines
4.2 KiB
TypeScript
Raw Normal View History

2024-01-12 20:46:09 -05:00
import { canvas2d } from "./lib/html";
2024-01-12 21:36:45 -05:00
import { Prng, mulberry32 } from "./lib/prng";
2024-01-12 20:46:09 -05:00
const BLOWUP = 4 * 5;
const WIDTH = 240 / 5;
const HEIGHT = 135 / 5;
2024-01-12 20:21:58 -05:00
2024-01-12 21:21:09 -05:00
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[];
2024-01-12 21:36:45 -05:00
rng: Prng;
2024-01-12 21:21:09 -05:00
xy: Lookup2d;
2024-01-12 21:36:45 -05:00
constructor(public width: number, public height: number, seed: number) {
2024-01-12 21:21:09 -05:00
this.data = Array(width * height).fill(0);
2024-01-12 21:36:45 -05:00
this.rng = mulberry32(seed);
2024-01-12 21:21:09 -05:00
this.xy = dim(width, height);
}
2024-01-12 21:36:00 -05:00
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)]);
}
2024-01-12 22:19:54 -05:00
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;
}
2024-01-12 21:21:09 -05:00
}
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;
2024-01-12 23:24:01 -05:00
case 4:
case 5:
case 6:
case 7:
case 8:
2024-01-12 21:21:09 -05:00
cx.fillStyle = "#666666";
break;
2024-01-12 23:24:01 -05:00
default:
cx.fillStyle = "#88aaff";
break;
2024-01-12 21:21:09 -05:00
}
cx.fillRect(x, y, 1, 1);
}
}
}
2024-01-12 20:21:58 -05:00
export function IslandApplet() {
2024-01-12 20:46:09 -05:00
const [canvas, cx] = canvas2d({
2024-01-12 20:21:58 -05:00
width: WIDTH * BLOWUP,
height: HEIGHT * BLOWUP,
2024-01-12 20:46:09 -05:00
});
2024-01-12 20:21:58 -05:00
cx.scale(BLOWUP, BLOWUP);
2024-01-12 21:36:45 -05:00
const islands = new IslandGrid(WIDTH, HEIGHT, 128);
2024-01-12 22:50:21 -05:00
const len = islands.data.length;
const basePos = len >> 1;
const width = islands.width;
2024-01-12 23:30:18 -05:00
let timerId: number;
2024-01-12 22:50:21 -05:00
function drop(pos: number) {
const lowerNeighbors: number[] = [];
function check(adjPos: number) {
if (islands.data[adjPos] < islands.data[pos]) {
lowerNeighbors.push(adjPos);
}
}
2024-01-12 23:25:09 -05:00
// try to roll in cardinal directions
2024-01-12 22:50:21 -05:00
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];
2024-01-12 23:25:09 -05:00
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);
2024-01-12 22:50:21 -05:00
}
2024-01-12 23:25:09 -05:00
// flat, increase elevation
2024-01-12 23:30:18 -05:00
const newValue = ++islands.data[pos];
if (newValue == 9) {
clearInterval(timerId);
}
2024-01-12 22:50:21 -05:00
}
2024-01-12 22:31:48 -05:00
function tick() {
const islandTiles = islands.floodSearch(basePos, (tile) => tile > 0);
2024-01-12 23:11:48 -05:00
drop(islandTiles[islands.rng() % islandTiles.length]);
drop(islandTiles[islands.rng() % islandTiles.length]);
drop(basePos - 8);
2024-01-12 22:31:48 -05:00
2024-01-12 23:25:26 -05:00
const erodePos = islandTiles[islands.rng() % islandTiles.length];
islands.data[erodePos] = Math.max(islands.data[erodePos] - 1, 0);
2024-01-12 22:31:48 -05:00
renderIslands(islands, cx);
}
tick();
2024-01-12 23:30:18 -05:00
timerId = setInterval(tick, 1000 / 30);
2024-01-12 20:21:58 -05:00
return [canvas];
}
(globalThis as any).IslandApplet = IslandApplet;