import { Position, TileType, GRID_WIDTH, } from '@spacerace/shared'; import { GAME_CONFIG } from '../config.js'; export class Grid { private cols: Map = new Map(); private _deathLineX: number; private _generatedUpToX: number; private seed: number; constructor(seed: number = 42) { this.seed = seed; this._deathLineX = 0; this._generatedUpToX = 12; this.generateCols(0, 25); this.clearRunway(0, 20); } private clearRunway(fromX: number, toX: number): void { const start = Math.min(fromX, toX); const end = Math.max(fromX, toX); for (let x = start; x <= end; x++) { this.cols.set(x, Array(GRID_WIDTH).fill('space')); } } get deathLineY(): number { return this._deathLineX; } get generatedUpToY(): number { return this._generatedUpToX; } private randFor(seed: number): number { let h = ((this.seed + seed) * 31 + seed * 7 + seed * seed * 13) % 2147483647; return (h & 0x7fffffff) / 0x7fffffff; } getTile(pos: Position): TileType { this.ensureCol(pos.y); const col = this.cols.get(pos.y); if (!col) return 'space'; return col[pos.x] ?? 'space'; } private ensureCol(y: number): void { if (this.cols.has(y)) return; if (y > this._generatedUpToX) { this.generateCols(this._generatedUpToX, y); } else { this.generateCols(0, y); } } setTile(pos: Position, tile: TileType): void { this.ensureCol(pos.y); const col = this.cols.get(pos.y)!; col[pos.x] = tile; } isBlocked(pos: Position): boolean { const tile = this.getTile(pos); return tile !== 'space'; } isInBounds(pos: Position): boolean { return pos.x >= 0 && pos.x < GRID_WIDTH; } advanceDeathLine(leadingY?: number): number { let advance: number = GAME_CONFIG.DEATH_LINE_ADVANCE; if (leadingY !== undefined) { const maxBehind = 10; const distance = leadingY - this._deathLineX; if (distance > maxBehind) { advance = Math.max(advance, distance - maxBehind); } } this._deathLineX += advance; return this._deathLineX; } generateCols(fromX: number, toX: number): void { const start = Math.min(fromX, toX); const end = Math.max(fromX, toX); for (let x = start; x <= end; x++) { if (this.cols.has(x)) continue; const col: TileType[] = []; const r = this.randFor(x * 7919); const pattern = Math.abs((x * 13 + this.seed * 7) % 5); for (let lane = 0; lane < GRID_WIDTH; lane++) { const tileR = this.randFor(x * 1000 + lane); switch (pattern) { case 0: col.push(tileR < GAME_CONFIG.TRACK_DENSITY * 0.5 ? 'asteroid' : 'space'); break; case 1: col.push(lane < 3 && tileR < 0.6 ? 'asteroid' : 'space'); break; case 2: col.push(lane >= 5 && tileR < 0.6 ? 'asteroid' : 'space'); break; case 3: col.push((lane < 2 || lane > 5) && tileR < 0.7 ? 'asteroid' : 'space'); break; case 4: col.push(tileR < GAME_CONFIG.TRACK_DENSITY * 1.5 ? 'asteroid' : 'space'); break; } } // Ensure at least 1 lane is open const blocked = col.every((t) => t !== 'space'); if (blocked) { const openLane = Math.floor(this.randFor(x * 31337) * GRID_WIDTH); (col as any)[openLane] = 'space'; } this.cols.set(x, col); } if (toX > this._generatedUpToX) { this._generatedUpToX = Math.max(this._generatedUpToX, toX); } } ensureAheadOf(leadingX: number): number { const neededX = leadingX + 5; if (neededX > this._generatedUpToX) { this.generateCols(this._generatedUpToX + 1, neededX); this._generatedUpToX = neededX; } return this._generatedUpToX; } toState(): Record { const state: Record = {}; for (const [x, col] of this.cols) { state[x] = [...col]; } return state; } findSafeStart(usedPositions: Position[], startX: number, columnSpread: number = 2): Position | null { const candidates: Position[] = []; for (let lane = 0; lane < GRID_WIDTH; lane++) { for (let colX = startX; colX <= startX + columnSpread; colX++) { const pos = { x: lane, y: colX }; if ( !this.isBlocked(pos) && !usedPositions.some((p) => p.y === pos.y && p.x === pos.x) ) { candidates.push(pos); } } } if (candidates.length === 0) return null; return candidates[Math.floor(this.randFor(startX * 7) * candidates.length)]; } }