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.
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;
}
}
