SpaceRace: Full-width TV field, death line scrolling, camera following leader, jump fix, path never fully blocked, player color on controller

This commit is contained in:
2026-06-24 16:41:37 +02:00
commit 5e2ab43ca3
46 changed files with 6581 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
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)];
}
}