import { Prng, UINT_MAX, mulberry32 } from "../lib/prng"; import { BEACH, ICECAP } from "./data"; import { ALL_ISLANDS, BIG_ISLANDS, GREEN_ISLANDS, LobeGenerator, NO_ISLAND, ROCKY_ISLANDS, SMALL_ISLANDS, } 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(0); 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(SMALL_ISLANDS), this.choose(ALL_ISLANDS), NO_ISLAND, NO_ISLAND, NO_ISLAND, NO_ISLAND, 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 + 0.5; 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 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]; if (newValue == ICECAP) { this.done = true; } } public choose(list: T[]) { if (list.length == 0) { throw new Error("Picking from empty list"); } return list[this.rng() % list.length]; } public shuffle(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); } }