1/**2 * 3D occupancy for the endless void: cell (x, z) → occupied vertical bands.3 *4 * The void has no ground, so nothing is "the" height of a cell — a column may5 * carry a channel at y −3.25, an island slab from −1 to 0, and an older6 * channel at +1.5, all legal as long as bands don't overlap. New track claims7 * a band of CHANNEL_CLEARANCE above its base (walls + marble headroom);8 * islands claim their mass. Crossing over/under/around earlier track is9 * exactly "my band fits between yours".10 *11 * Bands carry a `ref` (e.g. "seg/12", "isle/3") so a disposed segment or12 * culled island can release everything it claimed — memory stays bounded by13 * the streaming window, never by how far the line has travelled.14 */1516export interface Band {17 y0: number;18 y1: number;19 ref: string;20}2122/** Vertical claim of a channel above its base: walls plus marble headroom.23 * Two stacked channels at Δy = 1.0 (the kit's bridge separation) just clear:24 * [y, y+0.99] vs [y+1, y+1.99]. */25export const CHANNEL_CLEARANCE = 0.99;2627const key = (x: number, z: number) => `${x},${z}`;2829/** What a planner needs of the occupancy — satisfied by both the real map30 * and a transaction overlay. */31export interface OccView {32 isFree(x: number, z: number, y0: number, y1: number): boolean;33 at(x: number, z: number): readonly Band[];34 claim(x: number, z: number, y0: number, y1: number, ref: string): void;35}3637export class Occupancy implements OccView {38 private cells = new Map<string, Band[]>();39 /** ref → cell keys it claimed (for O(claims) release). */40 private byRef = new Map<string, string[]>();4142 /** True when [y0, y1] overlaps no existing band at (x, z). */43 isFree(x: number, z: number, y0: number, y1: number): boolean {44 const bands = this.cells.get(key(x, z));45 if (!bands) return true;46 for (const b of bands) {47 if (y0 < b.y1 && b.y0 < y1) return false;48 }49 return true;50 }5152 /** Bands at a cell (empty array if none). */53 at(x: number, z: number): readonly Band[] {54 return this.cells.get(key(x, z)) ?? [];55 }5657 claim(x: number, z: number, y0: number, y1: number, ref: string): void {58 const k = key(x, z);59 const bands = this.cells.get(k) ?? [];60 bands.push({ y0, y1, ref });61 this.cells.set(k, bands);62 const keys = this.byRef.get(ref) ?? [];63 keys.push(k);64 this.byRef.set(ref, keys);65 }6667 /** Claim a channel band: base y plus walls + marble headroom. */68 claimChannel(x: number, z: number, baseY: number, ref: string): void {69 this.claim(x, z, baseY, baseY + CHANNEL_CLEARANCE, ref);70 }7172 /** Release every band claimed under `ref`. */73 release(ref: string): void {74 const keys = this.byRef.get(ref);75 if (!keys) return;76 this.byRef.delete(ref);77 for (const k of keys) {78 const bands = this.cells.get(k);79 if (!bands) continue;80 const kept = bands.filter((b) => b.ref !== ref);81 if (kept.length === 0) this.cells.delete(k);82 else this.cells.set(k, kept);83 }84 }8586 /** Every band claimed under `ref`, with its cell coordinates. Keys are87 * deduped: a cell claimed twice under one ref (island mass + canopy)88 * must not report its bands twice — that duplicated every island block89 * placement (found via doubled collision boxes). */90 claimsOf(ref: string): { x: number; z: number; y0: number; y1: number }[] {91 const keys = new Set(this.byRef.get(ref) ?? []);92 const out: { x: number; z: number; y0: number; y1: number }[] = [];93 for (const k of keys) {94 const [x, z] = k.split(",").map(Number);95 for (const b of this.cells.get(k) ?? []) {96 if (b.ref === ref) out.push({ x, z, y0: b.y0, y1: b.y1 });97 }98 }99 return out;100 }101102 /** Claimed cell count — bounded-memory sanity in tests. */103 get size(): number {104 return this.cells.size;105 }106}107108/**109 * Write overlay over a base Occupancy: planning a segment claims here; a110 * stuck plan is discarded wholesale (nothing touched the base), a finished111 * plan commits its claims through. Cheap — O(claims of one segment).112 */113export class OccupancyTx implements OccView {114 private added: {115 x: number;116 z: number;117 y0: number;118 y1: number;119 ref: string;120 }[] = [];121 private byCell = new Map<string, Band[]>();122123 /** Base may itself be a transaction — probe plans stack a second layer. */124 constructor(private readonly base: OccView) {}125126 isFree(x: number, z: number, y0: number, y1: number): boolean {127 if (!this.base.isFree(x, z, y0, y1)) return false;128 const bands = this.byCell.get(key(x, z));129 if (!bands) return true;130 for (const b of bands) {131 if (y0 < b.y1 && b.y0 < y1) return false;132 }133 return true;134 }135136 at(x: number, z: number): readonly Band[] {137 const mine = this.byCell.get(key(x, z));138 const base = this.base.at(x, z);139 return mine ? [...base, ...mine] : base;140 }141142 claim(x: number, z: number, y0: number, y1: number, ref: string): void {143 this.added.push({ x, z, y0, y1, ref });144 const k = key(x, z);145 const bands = this.byCell.get(k) ?? [];146 bands.push({ y0, y1, ref });147 this.byCell.set(k, bands);148 }149150 /** Flush every overlay claim into the base map, refs preserved. */151 commit(): void {152 for (const c of this.added) {153 this.base.claim(c.x, c.z, c.y0, c.y1, c.ref);154 }155 this.added = [];156 this.byCell.clear();157 }158}