Back to Big Rig Rush
SOURCE / PINNED RELEASE

Made of little things.

Big Rig Rush

Release
ec9b6549f752…
Author-recorded commit
74b729258b1a…
License
LICENSE
Author’s source reference
nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-0a99f0cae40

Archive hash verified: 900e22810340ccb1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/game/traffic-routing.ts
import type { Lane } from './city.js';

export interface RoadHazard { x: number; y: number; r: number; layer: number; hard: boolean }
interface Exposure extends RoadHazard { start: number; end: number; born: number; seen: number; active: boolean }

/** Shared physical evidence; drivers query it with their own awareness delay. */
export class TrafficRouting {
  private cells = new Map<string, Set<number>>();
  private exposures = new Map<number, Exposure[]>();
  constructor(private readonly lanes: Lane[]) {
    for (const lane of lanes) {
      for (let x = Math.floor(lane.x0 / 200); x <= Math.floor(lane.x1 / 200); x++) {
        for (let y = Math.floor(lane.y0 / 200); y <= Math.floor(lane.y1 / 200); y++) {
          const key = `${x},${y}`;
          if (!this.cells.has(key)) this.cells.set(key, new Set());
          this.cells.get(key)!.add(lane.id);
        }
      }
    }
  }
  clear(): void { this.exposures.clear(); }
  refresh(hazards: RoadHazard[], time: number): void {
    const previous = this.exposures;
    const next = new Map<number, Exposure[]>();
    for (const h of hazards) {
      const ids = new Set<number>();
      for (let x = Math.floor((h.x - h.r) / 200); x <= Math.floor((h.x + h.r) / 200); x++) {
        for (let y = Math.floor((h.y - h.r) / 200); y <= Math.floor((h.y + h.r) / 200); y++) {
          for (const id of this.cells.get(`${x},${y}`) ?? []) ids.add(id);
        }
      }
      for (const id of ids) {
        const lane = this.lanes[id];
        for (let i = 1; i < lane.pts.length; i++) {
          const a = lane.pts[i - 1], b = lane.pts[i];
          const length = lane.cum[i] - lane.cum[i - 1];
          if (length <= 0) continue;
          const dx = (b.x - a.x) / length, dy = (b.y - a.y) / length;
          const along = (h.x - a.x) * dx + (h.y - a.y) * dy;
          const side = (h.x - a.x) * dy - (h.y - a.y) * dx;
          if (Math.abs(side) > h.r) continue;
          const reach = Math.sqrt(h.r * h.r - side * side);
          let start = lane.cum[i - 1] + Math.max(0, along - reach);
          let end = lane.cum[i - 1] + Math.min(length, along + reach);
          if (lane.layer0 !== lane.layer1) {
            if (h.layer === lane.layer0) end = Math.min(end, lane.breakS);
            else if (h.layer === lane.layer1) start = Math.max(start, lane.breakS);
            else continue;
          } else if (h.layer !== lane.layer0) continue;
          if (end <= start) continue;
          const old = previous.get(id)?.find((e) => e.layer === h.layer && e.hard === h.hard && Math.hypot(e.x - h.x, e.y - h.y) < 12 && e.seen >= time - 1);
          if (!next.has(id)) next.set(id, []);
          next.get(id)!.push({ ...h, start, end, born: old?.born ?? time, seen: time, active: true });
        }
      }
    }
    // Cleared scenes retain soft caution briefly, never a stale physical barrier.
    for (const [id, entries] of previous) {
      for (const e of entries) {
        if (time - e.seen > 12 || next.get(id)?.some((n) => Math.hypot(n.x - e.x, n.y - e.y) < 12)) continue;
        if (!next.has(id)) next.set(id, []);
        next.get(id)!.push({ ...e, active: false });
      }
    }
    this.exposures = next;
  }
  cost(id: number, time: number, delay: number, x: number, y: number, from = 0): number {
    let cost = 0;
    for (const e of this.exposures.get(id) ?? []) {
      if (e.end < from || (time - e.born < delay && Math.hypot(x - e.x, y - e.y) > 160)) continue;
      const fade = Math.max(0, 1 - (time - e.seen) / 12);
      cost = Math.max(cost, (e.hard ? 120 : 40) * fade);
    }
    return cost;
  }
  /** Nearest live physical danger on this directed lane. */
  stopAt(id: number, s: number, time: number): number {
    let stop = Infinity;
    for (const e of this.exposures.get(id) ?? []) {
      if (!e.active || !e.hard || time - e.seen > 0.6 || e.end < s) continue;
      // A car caught inside the zone may leave; never force it to reverse instantly.
      if (e.start < s) continue;
      stop = Math.min(stop, e.start);
    }
    return stop;
  }
  safeSpawn(id: number, s: number, time: number): boolean {
    return !(this.exposures.get(id) ?? []).some((e) => e.active && time - e.seen < 1 && e.end >= s && e.start < s + 180);
  }
  /** Bounded corridor search, including the street beyond each connector. */
  choose(lane: Lane, time: number, delay: number, x: number, y: number, current: number, random = Math.random): number {
    if (!lane.next.length) return -1;
    let budget = 0;
    const score = (id: number, depth: number, visited: Set<number>): number => {
      const own = this.cost(id, time, delay, x, y);
      if (--budget < 0) return own + 15;
      if (visited.has(id)) return own + 25;
      const nextLane = this.lanes[id];
      if (depth <= 0 || !nextLane.next.length) return own;
      const seen = new Set(visited); seen.add(id);
      return own + 0.85 * Math.min(...nextLane.next.map((n) => score(n, depth - (nextLane.kind === 'connector' ? 0 : 1), seen)));
    };
    const scored = lane.next.map((id) => {
      budget = Math.max(8, Math.floor(96 / lane.next.length));
      return { id, score: score(id, 3, new Set([lane.id])) };
    });
    const best = Math.min(...scored.map((s) => s.score));
    const prior = scored.find((s) => s.id === current);
    if (prior && prior.score <= best + 12) return current;
    const choices = scored.filter((s) => s.score <= best + 8);
    if (lane.kind === 'highway' && choices.some((s) => s.id === lane.next[0]) && random() < 0.7) return lane.next[0];
    return choices[Math.floor(random() * choices.length)].id;
  }
}