42 lines
2.7 KiB
TypeScript
42 lines
2.7 KiB
TypeScript
import type { FloorState, TileKind, TilePosition } from '../../domain/src/types.js'
|
|
|
|
function hash(seed: string): number {
|
|
let value = 2166136261
|
|
for (const c of seed) { value ^= c.charCodeAt(0); value = Math.imul(value, 16777619) }
|
|
return value >>> 0
|
|
}
|
|
function rng(seed: string): () => number {
|
|
let state = hash(seed) || 1
|
|
return () => { state ^= state << 13; state ^= state >>> 17; state ^= state << 5; return (state >>> 0) / 4294967296 }
|
|
}
|
|
const key = (p: TilePosition) => `${p.x},${p.y}`
|
|
|
|
export function validateFloor(floor: FloorState): boolean {
|
|
if (floor.spawnTiles.length === 0 || floor.tiles.flat().filter(t => t === 'exit').length !== 1) return false
|
|
const seen = new Set<string>(), queue = [floor.spawnTiles[0]!]
|
|
while (queue.length) {
|
|
const p = queue.shift()!; if (seen.has(key(p))) continue; seen.add(key(p))
|
|
for (const n of [{x:p.x,y:p.y-1},{x:p.x-1,y:p.y},{x:p.x+1,y:p.y},{x:p.x,y:p.y+1}]) {
|
|
if (n.x >= 0 && n.y >= 0 && n.x < floor.width && n.y < floor.height && floor.tiles[n.y]?.[n.x] !== 'wall' && !seen.has(key(n))) queue.push(n)
|
|
}
|
|
}
|
|
return seen.has(key(floor.exitPosition)) && floor.tiles[0]?.every(t => t === 'wall') === true && floor.tiles.at(-1)?.every(t => t === 'wall') === true
|
|
}
|
|
|
|
export function generateFloor(seed: string): FloorState {
|
|
const random = rng(seed), width = 18 + Math.floor(random() * 5), height = 11 + Math.floor(random() * 4)
|
|
const tiles: TileKind[][] = Array.from({length: height}, () => Array<TileKind>(width).fill('wall'))
|
|
const spawnW = 4 + Math.floor(random()*3), spawnH = 4 + Math.floor(random()*2)
|
|
const exitW = 4 + Math.floor(random()*3), exitH = 4 + Math.floor(random()*2)
|
|
const sy = 2 + Math.floor(random() * Math.max(1, height-spawnH-3)), ey = 2 + Math.floor(random() * Math.max(1, height-exitH-3))
|
|
const carve = (x:number,y:number,w:number,h:number) => { for(let yy=y;yy<y+h;yy++) for(let xx=x;xx<x+w;xx++) tiles[yy]![xx]='floor' }
|
|
carve(1,sy,spawnW,spawnH); carve(width-exitW-1,ey,exitW,exitH)
|
|
const start={x:1+Math.floor(spawnW/2),y:sy+Math.floor(spawnH/2)}, end={x:width-exitW-1+Math.floor(exitW/2),y:ey+Math.floor(exitH/2)}
|
|
if(random()<0.5){ for(let x=start.x;x<=end.x;x++) tiles[start.y]![x]='floor'; for(let y=Math.min(start.y,end.y);y<=Math.max(start.y,end.y);y++) tiles[y]![end.x]='floor' }
|
|
else { for(let y=Math.min(start.y,end.y);y<=Math.max(start.y,end.y);y++) tiles[y]![start.x]='floor'; for(let x=start.x;x<=end.x;x++) tiles[end.y]![x]='floor' }
|
|
tiles[end.y]![end.x]='exit'
|
|
const floor: FloorState={floorId:`floor-${hash(seed).toString(16)}`,width,height,tiles,spawnTiles:[start],exitPosition:end,goblinGuardPosition:{x:end.x-1,y:end.y},generationSeed:seed}
|
|
if(!validateFloor(floor)) throw new Error(`Generated invalid floor for seed ${seed}`)
|
|
return floor
|
|
}
|