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(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); } }