feat(08-02): implement the pure landmark compute core — L-suite GREEN
farthestPointAnchors + landmarkTables + landmarkCoverage over the ĉ metric: edge weights floor-quantized once (Math.floor(seconds/q)), exact integer Dijkstras (Int32 labels, binary heap, lazy deletion), 0xFFFF sentinel, 65000-quantum loud overflow. Traversal law mirrors astar.ts verbatim: bit0 edges never relaxed in EITHER direction. Test-vector corrections caught at GREEN (expectations were wrong, implementation was right): - L4: rows index by anchor POSITION (row-major [l*N+v]), not node id - L6: the c-metric farthest node from corner 0 is interior node 18 (297q: interior hops cost 2x perimeter), not corner 24 (264q) — verified against an independent brute-force Dijkstra
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
/*
|
||||
* landmark.ts — the ALT landmark preprocessing core (plan 08-02, D-02).
|
||||
* ----------------------------------------------------------------------------
|
||||
* One pure, typed-array module for everything integer about landmarks:
|
||||
* deterministic farthest-point anchor selection + the per-anchor distance
|
||||
* tables, both over the ĉ metric. The fixture builder
|
||||
* (test/fixtures/build-mini-graph.mjs), the real build script
|
||||
* (scripts/build-road-graph.mjs) and the bot suites ALL compute their
|
||||
* landmark blocks through these exports — there is no second
|
||||
* integer-Dijkstra implementation anywhere (single-authority law).
|
||||
*
|
||||
* THE ĉ METRIC (research Pattern 2 — quantization WITHOUT an epsilon):
|
||||
* edge weights are floor-quantized ONCE, ĉ(u,v) = floor(seconds/quantumS)
|
||||
* in fixed 0.1 s units, and the landmark Dijkstras run over that integer
|
||||
* metric in EXACT arithmetic. Stored distances are never rounded; because
|
||||
* ĉ <= c edge-wise and Dijkstra distances are 1-Lipschitz in their own
|
||||
* edges, the triangle bounds Plan 08-03 derives from these tables are
|
||||
* exactly admissible and exactly consistent against the real f32 seconds.
|
||||
*
|
||||
* TRAVERSAL LAW (mirrors astar.ts verbatim — the A1/625-pair precedent):
|
||||
* an edge with bit0 set ("oneway-reverse", graph.ts) is NEVER relaxed —
|
||||
* not in the forward pass, and not in the reversed pass either (astar.ts's
|
||||
* backward search skips bit0 in-edges at line 321). The reversed pass
|
||||
* relaxes a stored edge u->v FROM v (toward u) exactly when its bit0 is
|
||||
* clear, which is what makes to-row / from-row asymmetry representable.
|
||||
*
|
||||
* RANGE LAW: 0xFFFF is DATA (the unreachable sentinel), so any label
|
||||
* beyond LANDMARK_MAX_QUANTA (65 000 — headroom below the sentinel)
|
||||
* throws at build time, naming the distance and the fix
|
||||
* (raise landmarkQuantumS). Nothing is clamped, ever.
|
||||
*
|
||||
* DETERMINISM: no randomness of any kind — no PRNG, no Date, no
|
||||
* map-iteration order. Anchor greed starts at the first degree>=2
|
||||
* candidate by node id, ties break to the lower node id, and identical
|
||||
* input bytes give identical anchors.
|
||||
*
|
||||
* Pure module: no I/O, no logging, no storage, zero imports (T-06-05).
|
||||
*/
|
||||
|
||||
/** The ĉ unit in seconds — the header field default, a documented fork knob. */
|
||||
export const LANDMARK_QUANTUM_S = 0.1;
|
||||
/** u16 headroom below the 0xFFFF sentinel; any label past it fails loud. */
|
||||
export const LANDMARK_MAX_QUANTA = 65000;
|
||||
/** Stored sentinel for "unreachable" — DATA, never validated away. The
|
||||
* all-ones u16 pattern 0xffff is every table cell's out-of-domain value;
|
||||
* consumers check against this constant, never a per-callsite literal. */
|
||||
export const LANDMARK_SENTINEL = 0xffff;
|
||||
|
||||
/** The landmark compute input — the shape build scripts assemble. */
|
||||
export interface LandmarkInput {
|
||||
nodeCount: number;
|
||||
/**
|
||||
* Directed edges in any order. flags bit0 = oneway-reverse: relaxed by
|
||||
* NO direction (mirrors the CSR's edgeFlags; see the traversal law).
|
||||
*/
|
||||
edges: Array<{ from: number; to: number; seconds: number; flags: number }>;
|
||||
/** The ĉ quantum in seconds; named callers pass LANDMARK_QUANTUM_S. */
|
||||
quantumS: number;
|
||||
}
|
||||
|
||||
/** Internal "unvisited" distance — outside the u16 domain on purpose. */
|
||||
const UNVISITED = 0x7fffffff;
|
||||
|
||||
function fail(context: string, message: string): never {
|
||||
throw new Error(`${context}: ${message}`);
|
||||
}
|
||||
|
||||
/* ————— integer CSR (both directions) over the quantized weights ————— */
|
||||
|
||||
interface Prepared {
|
||||
n: number;
|
||||
/** Per-input-edge quantized weight ĉ (integer quanta). */
|
||||
qWeight: Int32Array;
|
||||
/** Per-input-edge traversal-relevant flags (bit0 only). */
|
||||
flags: Uint8Array;
|
||||
/** Per-input-edge endpoints (the reverse pass relaxes toward source). */
|
||||
target: Uint32Array;
|
||||
source: Uint32Array;
|
||||
/** Forward CSR: row u lists u's out-edges (input-edge indices). */
|
||||
fwdRow: Uint32Array;
|
||||
fwdEdge: Uint32Array;
|
||||
/** Reverse CSR: row v lists v's in-edges (input-edge indices). */
|
||||
revRow: Uint32Array;
|
||||
revEdge: Uint32Array;
|
||||
}
|
||||
|
||||
function prepare(input: LandmarkInput): Prepared {
|
||||
const context = "landmark";
|
||||
const n = input.nodeCount;
|
||||
if (!Number.isInteger(n) || n < 1) fail(context, "nodeCount must be a positive integer");
|
||||
if (!Number.isFinite(input.quantumS) || input.quantumS <= 0) {
|
||||
fail(context, "quantumS must be a positive finite number");
|
||||
}
|
||||
const edgeCount = input.edges.length;
|
||||
const qWeight = new Int32Array(edgeCount);
|
||||
const flags = new Uint8Array(edgeCount);
|
||||
const target = new Uint32Array(edgeCount);
|
||||
const source = new Uint32Array(edgeCount);
|
||||
const fwdRow = new Uint32Array(n + 1);
|
||||
const revRow = new Uint32Array(n + 1);
|
||||
for (let e = 0; e < edgeCount; e++) {
|
||||
const edge = input.edges[e]!;
|
||||
if (!Number.isInteger(edge.from) || edge.from < 0 || edge.from >= n) {
|
||||
fail(context, `edge ${e} from-node ${edge.from} out of range`);
|
||||
}
|
||||
if (!Number.isInteger(edge.to) || edge.to < 0 || edge.to >= n) {
|
||||
fail(context, `edge ${e} to-node ${edge.to} out of range`);
|
||||
}
|
||||
if (!Number.isFinite(edge.seconds) || edge.seconds < 0) {
|
||||
fail(context, `edge ${edge.from}->${edge.to} has a non-finite/negative weight`);
|
||||
}
|
||||
// THE ĉ RULE (Pattern 2): floor keeps the surrogate at or below the
|
||||
// live edge — the exactness hinge for admissibility downstream.
|
||||
qWeight[e] = Math.floor(edge.seconds / input.quantumS);
|
||||
flags[e] = edge.flags & 1;
|
||||
target[e] = edge.to;
|
||||
source[e] = edge.from;
|
||||
fwdRow[edge.from + 1] = fwdRow[edge.from + 1]! + 1;
|
||||
revRow[edge.to + 1] = revRow[edge.to + 1]! + 1;
|
||||
}
|
||||
for (let i = 0; i < n; i++) {
|
||||
fwdRow[i + 1] = fwdRow[i + 1]! + fwdRow[i]!;
|
||||
revRow[i + 1] = revRow[i + 1]! + revRow[i]!;
|
||||
}
|
||||
const fwdEdge = new Uint32Array(edgeCount);
|
||||
const revEdge = new Uint32Array(edgeCount);
|
||||
const fwdCursor = fwdRow.slice(0, n);
|
||||
const revCursor = revRow.slice(0, n);
|
||||
for (let e = 0; e < edgeCount; e++) {
|
||||
const fu = source[e]!;
|
||||
const fp = fwdCursor[fu]!;
|
||||
fwdEdge[fp] = e;
|
||||
fwdCursor[fu] = fp + 1;
|
||||
const rv = target[e]!;
|
||||
const rp = revCursor[rv]!;
|
||||
revEdge[rp] = e;
|
||||
revCursor[rv] = rp + 1;
|
||||
}
|
||||
return { n, qWeight, flags, target, source, fwdRow, fwdEdge, revRow, revEdge };
|
||||
}
|
||||
|
||||
/* ————— binary min-heap of (integer key, node) over preallocated arrays ————— */
|
||||
|
||||
class IntHeap {
|
||||
private keys = new Int32Array(1024);
|
||||
private nodes = new Uint32Array(1024);
|
||||
size = 0;
|
||||
|
||||
push(key: number, node: number): void {
|
||||
if (this.size === this.keys.length) this.grow();
|
||||
let i = this.size;
|
||||
this.size += 1;
|
||||
while (i > 0) {
|
||||
const parent = (i - 1) >> 1;
|
||||
if (this.keys[parent]! <= key) break;
|
||||
this.keys[i] = this.keys[parent]!;
|
||||
this.nodes[i] = this.nodes[parent]!;
|
||||
i = parent;
|
||||
}
|
||||
this.keys[i] = key;
|
||||
this.nodes[i] = node;
|
||||
}
|
||||
|
||||
peekKey(): number {
|
||||
return this.size === 0 ? UNVISITED : this.keys[0]!;
|
||||
}
|
||||
|
||||
/** Min-key node, or -1 when empty. Lazy deletion: stale entries are
|
||||
* skipped by the dist check in dijkstraPass (the astar.ts shape). */
|
||||
pop(): number {
|
||||
if (this.size === 0) return -1;
|
||||
const top = this.nodes[0]!;
|
||||
this.size -= 1;
|
||||
const lastKey = this.keys[this.size]!;
|
||||
const lastNode = this.nodes[this.size]!;
|
||||
let i = 0;
|
||||
for (;;) {
|
||||
const left = 2 * i + 1;
|
||||
if (left >= this.size) break;
|
||||
const right = left + 1;
|
||||
const child = right < this.size && this.keys[right]! < this.keys[left]! ? right : left;
|
||||
if (this.keys[child]! >= lastKey) break;
|
||||
this.keys[i] = this.keys[child]!;
|
||||
this.nodes[i] = this.nodes[child]!;
|
||||
i = child;
|
||||
}
|
||||
if (this.size > 0) {
|
||||
this.keys[i] = lastKey;
|
||||
this.nodes[i] = lastNode;
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
private grow(): void {
|
||||
const keys = new Int32Array(this.keys.length * 2);
|
||||
keys.set(this.keys);
|
||||
this.keys = keys;
|
||||
const nodes = new Uint32Array(this.nodes.length * 2);
|
||||
nodes.set(this.nodes);
|
||||
this.nodes = nodes;
|
||||
}
|
||||
}
|
||||
|
||||
/* ————— the ONE integer Dijkstra (both directions share it) ————— */
|
||||
|
||||
/**
|
||||
* Exact integer Dijkstra from `anchor` over the ĉ metric. `reverse=false`
|
||||
* relaxes stored out-edges (d̂(anchor, v)); `reverse=true` relaxes stored
|
||||
* in-edges from their head (d̂(v, anchor)) — the astar.ts backward-search
|
||||
* law: an in-edge with bit0 set is skipped, never traversed.
|
||||
* Returns per-node labels; UNVISITED means unreachable. Any improving
|
||||
* label beyond LANDMARK_MAX_QUANTA throws (context names the entry point).
|
||||
*/
|
||||
function dijkstraPass(p: Prepared, reverse: boolean, anchor: number, context: string): Int32Array {
|
||||
const n = p.n;
|
||||
const dist = new Int32Array(n).fill(UNVISITED);
|
||||
const heap = new IntHeap();
|
||||
dist[anchor] = 0;
|
||||
heap.push(0, anchor);
|
||||
const row = reverse ? p.revRow : p.fwdRow;
|
||||
const list = reverse ? p.revEdge : p.fwdEdge;
|
||||
while (heap.size > 0) {
|
||||
const d = heap.peekKey();
|
||||
const u = heap.pop();
|
||||
if (d > dist[u]!) continue; // stale heap entry (lazy deletion)
|
||||
const end = row[u + 1]!;
|
||||
for (let r = row[u]!; r < end; r++) {
|
||||
const e = list[r]!;
|
||||
if ((p.flags[e]! & 1) !== 0) continue; // oneway-reverse: never relaxed, either direction
|
||||
const v = reverse ? p.source[e]! : p.target[e]!;
|
||||
const nd = d + p.qWeight[e]!;
|
||||
if (nd < dist[v]!) {
|
||||
if (nd > LANDMARK_MAX_QUANTA) {
|
||||
fail(
|
||||
context,
|
||||
`quantized distance ${nd} exceeds ${LANDMARK_MAX_QUANTA} (raise landmarkQuantumS)`,
|
||||
);
|
||||
}
|
||||
dist[v] = nd;
|
||||
heap.push(nd, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
/** u16 finish: unreachable -> the 0xFFFF sentinel; finite labels are
|
||||
* already <= LANDMARK_MAX_QUANTA (the pass throws beyond it). */
|
||||
function finish(d: number): number {
|
||||
return d === UNVISITED ? LANDMARK_SENTINEL : d;
|
||||
}
|
||||
|
||||
/* ————— public surface ————— */
|
||||
|
||||
/**
|
||||
* Deterministic farthest-point anchor selection over the ĉ metric
|
||||
* (D-02's locked approach family). Candidates are nodes with stored
|
||||
* out-degree >= 2 (blocked mirrors included — the node still sits on the
|
||||
* road). Greed: the first anchor is the first candidate by node id; each
|
||||
* next anchor maximizes the per-node min-distance to the already-picked
|
||||
* set (ties -> lower node id). Each pick's FORWARD pass is reused to
|
||||
* maintain that min-distance field (one pass per pick, not per candidate).
|
||||
*
|
||||
* `opts.coverageFraction` (0..1): stop early once the fraction of nodes
|
||||
* within finite ĉ-distance of the picked set reaches it — a documented
|
||||
* knob that may return FEWER than `count` anchors. Without it, exactly
|
||||
* `min(count, candidates)` anchors return (early-stop when candidates run
|
||||
* out). Returns an empty array when no degree>=2 candidate exists.
|
||||
*/
|
||||
export function farthestPointAnchors(
|
||||
input: LandmarkInput,
|
||||
count: number,
|
||||
opts?: { coverageFraction?: number },
|
||||
): Uint32Array {
|
||||
const context = "farthestPointAnchors";
|
||||
if (!Number.isInteger(count) || count < 0) fail(context, "count must be a non-negative integer");
|
||||
const coverageFraction = opts?.coverageFraction;
|
||||
if (
|
||||
coverageFraction !== undefined &&
|
||||
(!Number.isFinite(coverageFraction) || coverageFraction < 0 || coverageFraction > 1)
|
||||
) {
|
||||
fail(context, "coverageFraction must be a number in [0, 1]");
|
||||
}
|
||||
const p = prepare(input);
|
||||
const n = p.n;
|
||||
const capacity = Math.min(count, n);
|
||||
const anchors = new Uint32Array(capacity);
|
||||
if (capacity === 0) return anchors;
|
||||
|
||||
const degree = (u: number): number => p.fwdRow[u + 1]! - p.fwdRow[u]!;
|
||||
let first = -1;
|
||||
for (let u = 0; u < n; u++) {
|
||||
if (degree(u) >= 2) {
|
||||
first = u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (first < 0) return anchors.slice(0, 0); // no junction-ish candidate exists
|
||||
|
||||
const minDist = new Int32Array(n).fill(UNVISITED);
|
||||
const isPicked = new Uint8Array(n);
|
||||
let picked = 0;
|
||||
let current = first;
|
||||
// Each loop turn folds ONE pass (the just-picked anchor's forward pass)
|
||||
// into the min-distance field, then greedily picks the next anchor.
|
||||
let dist = dijkstraPass(p, false, current, context);
|
||||
for (;;) {
|
||||
let covered = 0;
|
||||
for (let v = 0; v < n; v++) {
|
||||
const d = dist[v]!;
|
||||
if (d < minDist[v]!) minDist[v] = d;
|
||||
if (minDist[v]! !== UNVISITED) covered++;
|
||||
}
|
||||
anchors[picked] = current;
|
||||
isPicked[current] = 1;
|
||||
picked += 1;
|
||||
if (picked === capacity) break;
|
||||
if (coverageFraction !== undefined && covered / n >= coverageFraction) break;
|
||||
let next = -1;
|
||||
let best = -1;
|
||||
for (let v = 0; v < n; v++) {
|
||||
const d = minDist[v]!;
|
||||
if (d === UNVISITED || isPicked[v] !== 0 || degree(v) < 2) continue;
|
||||
if (d > best) {
|
||||
best = d;
|
||||
next = v; // strict > keeps the FIRST (lowest-id) node on ties
|
||||
}
|
||||
}
|
||||
if (next < 0) break; // candidates exhausted
|
||||
current = next;
|
||||
dist = dijkstraPass(p, false, current, context);
|
||||
}
|
||||
return anchors.slice(0, picked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Integer ĉ-metric Dijkstras per anchor. Returns row-major tables of
|
||||
* L·N u16 cells ([ℓ*N + v]):
|
||||
* to[ℓ*N+v] = d̂(v, ℓ) — the REVERSED-graph pass from ℓ
|
||||
* from[ℓ*N+v] = d̂(ℓ, v) — the forward pass from ℓ
|
||||
* with 0xFFFF wherever v cannot reach (resp. be reached from) ℓ. Throws
|
||||
* on an out-of-range anchor or any label beyond LANDMARK_MAX_QUANTA.
|
||||
*/
|
||||
export function landmarkTables(
|
||||
input: LandmarkInput,
|
||||
anchors: Uint32Array,
|
||||
): { to: Uint16Array; from: Uint16Array } {
|
||||
const context = "landmarkTables";
|
||||
const p = prepare(input);
|
||||
const n = p.n;
|
||||
const l = anchors.length;
|
||||
for (let i = 0; i < l; i++) {
|
||||
const a = anchors[i]!;
|
||||
if (!Number.isInteger(a) || a < 0 || a >= n) fail(context, `anchor ${a} out of range`);
|
||||
}
|
||||
const to = new Uint16Array(l * n);
|
||||
const from = new Uint16Array(l * n);
|
||||
for (let i = 0; i < l; i++) {
|
||||
const anchor = anchors[i]!;
|
||||
const distTo = dijkstraPass(p, true, anchor, context);
|
||||
const distFrom = dijkstraPass(p, false, anchor, context);
|
||||
const base = i * n;
|
||||
for (let v = 0; v < n; v++) {
|
||||
to[base + v] = finish(distTo[v]!);
|
||||
from[base + v] = finish(distFrom[v]!);
|
||||
}
|
||||
}
|
||||
return { to, from };
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-anchor coverage audit: the fraction of nodes whose to-row cell is
|
||||
* NOT the sentinel — the build script's per-anchor assert feed
|
||||
* (research Pattern 3, layer 1).
|
||||
*/
|
||||
export function landmarkCoverage(to: Uint16Array, anchors: Uint32Array, nodeCount: number): number[] {
|
||||
const coverage: number[] = [];
|
||||
for (let i = 0; i < anchors.length; i++) {
|
||||
let reached = 0;
|
||||
const base = i * nodeCount;
|
||||
for (let v = 0; v < nodeCount; v++) {
|
||||
if (to[base + v] !== LANDMARK_SENTINEL) reached++;
|
||||
}
|
||||
coverage.push(reached / nodeCount);
|
||||
}
|
||||
return coverage;
|
||||
}
|
||||
@@ -22,8 +22,8 @@
|
||||
* L4 asymmetry — to/from rows differ on a oneway graph
|
||||
* L5 overflow throws — any label > 65000 quanta throws, naming the
|
||||
* distance and the fix (raise landmarkQuantumS)
|
||||
* L6 5x5 grid shape — anchors [0, 24]; corner->corner 264 quanta
|
||||
* (8 perimeter hops x 33); coverage 1.0
|
||||
* L6 5x5 grid shape — anchors [0, 18] (interior node 18 is the ĉ
|
||||
* farthest at 297 quanta); coverage 1.0
|
||||
*
|
||||
* Run: npm run routing:test (exit 0 = all green)
|
||||
*/
|
||||
@@ -160,11 +160,13 @@ test("L3 sentinel + self-cell — unreachable pairs -> 0xFFFF; the landmark's ow
|
||||
test("L4 asymmetry — reversed vs forward rows differ on an oneway graph", async () => {
|
||||
const { landmarkTables } = await loadLandmark();
|
||||
const { to, from } = landmarkTables(onewayInput(), Uint32Array.of(1));
|
||||
// Anchor at the middle node 1: forward reaches 2 (15 quanta) but the
|
||||
// to-row for 2 is the sentinel (2 cannot get back to 1).
|
||||
assert.equal(from[1 * 4 + 2], 15, "1->2 traverses");
|
||||
assert.equal(to[1 * 4 + 2], 0xffff, "2->1 does not (oneway) — asymmetry is representable");
|
||||
assert.notEqual(to[1 * 4 + 2], from[1 * 4 + 2]);
|
||||
// Rows are indexed by anchor POSITION (row-major [ℓ*N+v], ℓ = 0 here),
|
||||
// not by the anchor's node id. Anchor at the middle node 1: forward
|
||||
// reaches 2 (15 quanta) but the to-row for 2 is the sentinel (2 cannot
|
||||
// get back to 1).
|
||||
assert.equal(from[0 * 4 + 2], 15, "1->2 traverses");
|
||||
assert.equal(to[0 * 4 + 2], 0xffff, "2->1 does not (oneway) — asymmetry is representable");
|
||||
assert.notEqual(to[0 * 4 + 2], from[0 * 4 + 2]);
|
||||
});
|
||||
|
||||
test("L5 overflow throws — any label > 65000 quanta fails loud, naming the fix", async () => {
|
||||
@@ -195,19 +197,21 @@ test("L5 overflow throws — any label > 65000 quanta fails loud, naming the fix
|
||||
);
|
||||
});
|
||||
|
||||
test("L6 5x5 grid shape — anchors [0,24]; corner->corner 264 quanta; coverage 1.0", async () => {
|
||||
test("L6 5x5 grid shape — anchors [0,18]; farthest 297 quanta; coverage 1.0", async () => {
|
||||
const { farthestPointAnchors, landmarkTables, landmarkCoverage } = await loadLandmark();
|
||||
const input = await gridInput();
|
||||
const anchors = farthestPointAnchors(input, 2);
|
||||
assert.deepEqual([...anchors], [0, 24], "first candidate is node 0; farthest is the opposite corner");
|
||||
// Farthest-point over the ĉ metric: interior hops cost 66 quanta (2x the
|
||||
// perimeter's 33), so the farthest node from corner 0 is the INTERIOR
|
||||
// node 18 at 297 quanta — NOT the opposite corner 24 (264). Verified
|
||||
// against an independent brute-force Dijkstra (297 unique max).
|
||||
assert.deepEqual([...anchors], [0, 18], "first candidate is node 0; farthest is node 18");
|
||||
const { to, from } = landmarkTables(input, anchors);
|
||||
assert.equal(to.length, 2 * 25, "L*N cells (L=2, N=25)");
|
||||
assert.equal(to[0 * 25 + 0], 0, "self-cell 0");
|
||||
assert.equal(from[1 * 25 + 24], 0, "anchor 24's own from-cell 0");
|
||||
// Corner->corner: 8 perimeter hops (row 0 then column 4, 33 quanta each;
|
||||
// interior hops cost 66 — strictly worse) = 264 exactly.
|
||||
assert.equal(from[0 * 25 + 24], 264, "0->24 = 8 x 33 perimeter quanta, exact");
|
||||
assert.equal(to[1 * 25 + 0], 264, "the to-row mirrors on the grid");
|
||||
assert.equal(from[1 * 25 + 18], 0, "anchor 18's own from-cell 0 (row = position 1)");
|
||||
assert.equal(from[0 * 25 + 18], 297, "0->18 = 3 perimeter + 3 interior hops = 99+198, exact");
|
||||
assert.equal(to[1 * 25 + 0], 297, "the to-row mirrors on the grid");
|
||||
const coverage = landmarkCoverage(to, anchors, 25);
|
||||
assert.deepEqual(coverage, [1, 1], "the grid is strongly connected — full coverage");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user