2024-01-13 13:06:16 -05:00
|
|
|
import { Prng, UINT_MAX, mulberry32 } from "../lib/prng";
|
2024-01-13 12:19:17 -05:00
|
|
|
import { BEACH, ICECAP } from "./data";
|
2024-01-13 12:55:17 -05:00
|
|
|
import {
|
|
|
|
ALL_ISLANDS,
|
|
|
|
BIG_ISLANDS,
|
|
|
|
LobeGenerator,
|
|
|
|
NO_ISLAND,
|
|
|
|
} from "./generators";
|
2024-01-13 11:34:25 -05:00
|
|
|
|
|
|
|
export class IslandGrid {
|
|
|
|
data: number[];
|
|
|
|
rng: Prng;
|
|
|
|
|
2024-01-13 12:10:37 -05:00
|
|
|
generators: LobeGenerator[] = [];
|
2024-01-13 11:34:25 -05:00
|
|
|
|
|
|
|
done = false;
|
|
|
|
|
|
|
|
constructor(public width: number, public height: number, seed: number) {
|
|
|
|
this.data = Array(width * height).fill(0);
|
|
|
|
this.rng = mulberry32(seed);
|
|
|
|
|
2024-01-13 12:55:17 -05:00
|
|
|
const islandBag = this.shuffle([
|
|
|
|
this.choose(BIG_ISLANDS),
|
|
|
|
this.choose(BIG_ISLANDS),
|
|
|
|
this.choose(ALL_ISLANDS),
|
|
|
|
this.choose(ALL_ISLANDS),
|
|
|
|
this.choose(ALL_ISLANDS),
|
|
|
|
this.choose(ALL_ISLANDS),
|
|
|
|
NO_ISLAND,
|
|
|
|
NO_ISLAND,
|
2024-01-13 13:06:16 -05:00
|
|
|
NO_ISLAND,
|
|
|
|
NO_ISLAND,
|
|
|
|
NO_ISLAND,
|
2024-01-13 12:55:17 -05:00
|
|
|
]);
|
|
|
|
|
|
|
|
const islandCount = islandBag.length;
|
|
|
|
const spacing = (Math.PI * 2) / islandCount;
|
2024-01-13 13:06:16 -05:00
|
|
|
const rootX = width / 2;
|
|
|
|
const rootY = height / 2;
|
|
|
|
const xScale = width / 4;
|
|
|
|
const yScale = height / 4;
|
2024-01-13 12:55:17 -05:00
|
|
|
for (let i = 0; i < islandCount; i++) {
|
2024-01-13 13:06:16 -05:00
|
|
|
const rScale = this.rng() / UINT_MAX + 0.5;
|
|
|
|
const y = rootY + Math.sin(spacing * i) * yScale * rScale;
|
|
|
|
const x = rootX + Math.cos(spacing * i) * xScale * rScale;
|
2024-01-13 12:55:17 -05:00
|
|
|
|
|
|
|
this.generators.push(islandBag[i](this, this.xy(x | 0, y | 0)));
|
|
|
|
}
|
2024-01-13 11:34:25 -05:00
|
|
|
}
|
|
|
|
|
2024-01-13 11:34:37 -05:00
|
|
|
public xy(x: number, y: number): number {
|
|
|
|
return (
|
|
|
|
(((x % this.width) + this.width) % this.width) +
|
|
|
|
this.width * (((y % this.height) + this.height) % this.height)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-01-13 11:34:25 -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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
check((pos - this.width) % this.data.length);
|
|
|
|
check((pos - 1) % this.data.length);
|
|
|
|
check((pos + 1) % this.data.length);
|
|
|
|
check((pos + this.width) % this.data.length);
|
|
|
|
|
|
|
|
if (lowerNeighbors.length > 0) {
|
|
|
|
const downhill = lowerNeighbors[this.rng() % lowerNeighbors.length];
|
|
|
|
return this.drop(downhill);
|
|
|
|
}
|
|
|
|
|
|
|
|
// try to roll in diagonal directions
|
|
|
|
|
|
|
|
check((pos - this.width - 1) % this.data.length);
|
|
|
|
check((pos - this.width + 1) % this.data.length);
|
|
|
|
check((pos + this.width - 1) % this.data.length);
|
|
|
|
check((pos + this.width + 1) % this.data.length);
|
|
|
|
|
|
|
|
if (lowerNeighbors.length > 0) {
|
|
|
|
const downhill = lowerNeighbors[this.rng() % lowerNeighbors.length];
|
|
|
|
return this.drop(downhill);
|
|
|
|
}
|
|
|
|
|
|
|
|
// flat, increase elevation
|
|
|
|
const newValue = ++this.data[pos];
|
2024-01-13 11:44:36 -05:00
|
|
|
if (newValue == ICECAP) {
|
2024-01-13 11:34:25 -05:00
|
|
|
this.done = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-13 12:19:17 -05:00
|
|
|
public choose<T>(list: T[]) {
|
|
|
|
if (list.length == 0) {
|
|
|
|
throw new Error("Picking from empty list");
|
|
|
|
}
|
|
|
|
return list[this.rng() % list.length];
|
|
|
|
}
|
|
|
|
|
2024-01-13 12:55:17 -05:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-01-13 11:34:25 -05:00
|
|
|
public dropWithin(tiles: number[]) {
|
|
|
|
if (tiles.length > 0) {
|
2024-01-13 12:19:17 -05:00
|
|
|
this.drop(this.choose(tiles));
|
2024-01-13 11:34:25 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public step() {
|
2024-01-13 12:10:37 -05:00
|
|
|
this.done = this.generators
|
|
|
|
.map((generator) => generator())
|
|
|
|
.every((done) => done);
|
2024-01-13 11:34:25 -05:00
|
|
|
}
|
|
|
|
}
|