157 lines
4.5 KiB
TypeScript
157 lines
4.5 KiB
TypeScript
import {
|
|
Position,
|
|
TileType,
|
|
GRID_WIDTH,
|
|
} from '@spacerace/shared';
|
|
import { GAME_CONFIG } from '../config.js';
|
|
|
|
export class Grid {
|
|
private cols: Map<number, TileType[]> = 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<number, (TileType | null)[]> {
|
|
const state: Record<number, (TileType | null)[]> = {};
|
|
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)];
|
|
}
|
|
}
|