2024.js/island/grid.ts

208 lines
5.3 KiB
TypeScript

import { Prng, UINT_MAX, mulberry32 } from "../lib/prng";
import { WATER } from "./data";
import {
ALL_ISLANDS,
BIG_ISLANDS,
GREEN_ISLANDS,
LobeGenerator,
NO_ISLAND,
ROCKY_ISLANDS,
SMALL_ISLANDS,
VOIDS,
} from "./generators";
export class IslandGrid {
data: number[];
rng: Prng;
generators: LobeGenerator[] = [];
done = false;
constructor(public width: number, public height: number, seed: number) {
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)));
}
}
public xy(x: number, y: number): number {
return (
(((x % this.width) + this.width) % this.width) +
this.width * (((y % this.height) + this.height) % this.height)
);
}
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;
}
public drop(pos: number): void {
const lowerNeighbors: number[] = [];
const check = (adjPos: number) => {
if (this.data[adjPos] < this.data[pos]) {
lowerNeighbors.push(adjPos);
}
};
// try to roll in cardinal directions
this.forCardinals(pos, check);
if (lowerNeighbors.length > 0) {
const downhill = lowerNeighbors[this.rng() % lowerNeighbors.length];
return this.drop(downhill);
}
// try to roll in diagonal directions
this.forDiagonals(pos, check);
if (lowerNeighbors.length > 0) {
const downhill = lowerNeighbors[this.rng() % lowerNeighbors.length];
return this.drop(downhill);
}
// flat, increase elevation
++this.data[pos];
}
public sinkhole(pos: number): void {
const higherNeighbors: number[] = [];
// try to pull from neighbors
this.forNeighbors(pos, (adjPos: number) => {
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);
}
// flat, decrease elevation
this.data[pos] = Math.max(this.data[pos] - 1, -3);
}
public forCardinals(pos: number, action: (adjPos: number) => void) {
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);
}
public forDiagonals(pos: number, action: (adjPos: number) => void) {
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);
}
public forNeighbors(pos: number, action: (adjPos: number) => void) {
this.forCardinals(pos, action);
this.forDiagonals(pos, action);
}
public 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;
}
}
}
}
public choose<T>(list: T[]) {
if (list.length == 0) {
throw new Error("Picking from empty list");
}
return list[this.rng() % list.length];
}
public shuffle<T>(list: T[]) {
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;
}
public dropWithin(tiles: number[]) {
if (tiles.length > 0) {
this.drop(this.choose(tiles));
}
}
public step() {
this.done = this.generators
.map((generator) => generator())
.every((done) => done);
}
}