UNPKG

@alexaegis/advent-of-code-lib

Version:
1,585 lines 105 kB
"use strict"; const set = require("./set.cjs"); const common = require("@alexaegis/common"); const regex = require("./regex.cjs"); const jsSdsl = require("js-sdsl"); require("./index-ZPyfgNVR.cjs"); require("./map.cjs"); require("node:perf_hooks"); require("node:fs/promises"); require("node:path"); require("node:fs"); require("kolorist"); const arktype = require("arktype"); const arrayContains = (array, item) => { return hasEquals(item) ? array.some((i) => item.equals(i)) : array.includes(item); }; const hasEquals = (o) => { return typeof o.equals === "function"; }; const arrayDiff = (first, second) => { const onlyInFirst = []; const onlyInSecond = []; const bothHas = []; for (const itemFromFirst of first) { if (arrayContains(second, itemFromFirst)) { bothHas.push(itemFromFirst); } else { onlyInFirst.push(itemFromFirst); } } for (const itemFromSecond of second) { if (!arrayContains(first, itemFromSecond)) { onlyInSecond.push(itemFromSecond); } } return { onlyInFirst, onlyInSecond, bothHas }; }; const isNumber = (n) => typeof n === "number"; const isNumberArray = (n) => n.every(isNumber); const isBigint = (n) => typeof n === "bigint"; const isNumeric = (n) => isNumber(n) || isBigint(n); const max$1 = (a, b) => a < b ? b : a; const min$1 = (a, b) => a > b ? b : a; const sum = (a, b) => a + b; const mult = (a, b) => a * b; const dup = (a) => a * 2; const add = (a, b) => a + b; const sub = (a, b) => a - b; const descending = (a, b) => b - a; const ascending = sub; const findEndOfPair = (t, [begin, end], from = 0) => { if (t[from] !== begin) { throw new Error("First element must be the opening part of the pair"); } let pc = 1; let i = from + 1; for (; i < t.length; i++) { if (t[i] === begin) pc++; else if (t[i] === end) pc--; if (pc === 0) break; } if (pc !== 0) { return void 0; } return i; }; const cutSubSegment = (t, pair, from = 0, leaveOneBehind = false) => { const j = findEndOfPair(t, pair, from); if (j === void 0) { return void 0; } else { const inner = t.splice(from + 1, j - from - 1); if (leaveOneBehind) { t.splice(from + 1, 1); } else { t.splice(from, 2); } return inner; } }; const nonNullish = (t) => t !== void 0 && t !== null; const filterMap = (array, mapFn) => { const result = []; let i = 0; for (const item of array) { const value = mapFn(item, i); if (nonNullish(value)) { result.push(value); } i++; } return result; }; const findLast = (array, predicate, skipCount = 0) => { for (let i = array.length - 1 - skipCount; i >= 0; i--) { if (predicate(array[i], i)) { return array[i]; } } return void 0; }; function* matrixFlipFlop(matrix) { let state = matrix; yield state = state.rotateMatrix("r"); yield state = state.rotateMatrix("r"); yield state = state.rotateMatrix("r"); yield state = state.rotateMatrix("r"); yield state = state.flipMatrix("x"); yield state = state.rotateMatrix("r"); yield state = state.rotateMatrix("r"); yield state = state.rotateMatrix("r"); yield state = state.rotateMatrix("r"); } const flipMatrix = (matrix, axis = "x") => { if (matrix.length === 0) { return []; } else if (!Array.isArray(matrix[0]) && axis !== "y") { throw new Error("Input is not a matrix, cannot be flipped"); } matrix = axis === "x" ? [...matrix].reverse() : matrix.map((row) => [...row].reverse()); return matrix; }; const getSizedGroups = (array, groupSize) => array.reduce( (groups, next) => { let lastGroup = groups.at(-1); if (lastGroup) { if (lastGroup.length >= groupSize) { lastGroup = []; groups.push(lastGroup); } lastGroup.push(next); } return groups; }, [[]] ); const groupByDelimiter = (values, isDelimiter = (t) => !t) => { const result = [[]]; for (const value of values) { if (isDelimiter(value)) { result.push([]); } else { result.at(-1)?.push(value); } } return result; }; const pairwise = (array, callback) => { let last = void 0; for (const element of array) { if (last !== void 0) { callback(last, element); } last = element; } }; const slideWindow = (array, windowSize = 2, stepSize = 1) => { const result = []; const window = []; for (const element of array) { window.push(element); if (window.length === windowSize) { result.push([...window]); window.splice(0, stepSize); } } return result; }; const mapFirst = (array, map) => { for (const item of array) { const value = map(item); if (value) { return value; } } return void 0; }; const mapLast = (array, map) => { for (let i = array.length - 1; i >= 0; i--) { const value = map(array[i]); if (value) { return value; } } return void 0; }; const maxOf = (array, count = 1) => { return count === 1 ? array.reduce(max$1, Number.NEGATIVE_INFINITY) : [...array].sort(descending).splice(0, count); }; const addWithinRange = (base, add2, fromOrTo, optionalTo) => { if (nonNullish(fromOrTo)) { const [from, to] = nonNullish(optionalTo) ? [fromOrTo, optionalTo] : [0, fromOrTo]; const range = to - from + 1; return (base + add2 - from) % range + from; } else { return base + add2; } }; const posMod = (n, m) => { const mod = n % m; return Math.abs(mod >= 0 ? mod : (mod + m) % m); }; const posModBigInt = (n, m) => { const mod = n % m; return mod >= 0 ? mod : (mod + m) % m; }; const egcd = (a, b) => { if (a === 0) { return [b, 0, 1]; } else { const [gcd2, y, x] = egcd(posMod(b, a), a); return [gcd2, x - b / a * y, y]; } }; const egcdBigInt = (a, b) => { if (a === 0n) { return [b, 0n, 1n]; } else { const [g, y, x] = egcdBigInt(posModBigInt(b, a), a); return [g, x - b / a * y, y]; } }; const gcdOverTwo = (x = 0, y = 0) => { x = Math.abs(x); y = Math.abs(y); while (y) { const t = y; y = x % y; x = t; } return x; }; const gcd = (x, y) => Array.isArray(x) ? x.reduce((a, n) => gcdOverTwo(a, n), 1) : gcdOverTwo(x, y); var DirectionArrowSymbol = /* @__PURE__ */ ((DirectionArrowSymbol2) => { DirectionArrowSymbol2["NORTH"] = "^"; DirectionArrowSymbol2["EAST"] = ">"; DirectionArrowSymbol2["SOUTH"] = "v"; DirectionArrowSymbol2["WEST"] = "<"; return DirectionArrowSymbol2; })(DirectionArrowSymbol || {}); var DirectionNames = /* @__PURE__ */ ((DirectionNames2) => { DirectionNames2["NORTH"] = "NORTH"; DirectionNames2["EAST"] = "EAST"; DirectionNames2["SOUTH"] = "SOUTH"; DirectionNames2["WEST"] = "WEST"; return DirectionNames2; })(DirectionNames || {}); const isHorizontalDirectionArrowSymbol = (symbol) => symbol === ">" || symbol === "<"; const isVericalDirectionArrowSymbol = (symbol) => symbol === "^" || symbol === "v"; const isDirectionArrowSymbol = (symbol) => isHorizontalDirectionArrowSymbol(symbol) || isVericalDirectionArrowSymbol(symbol); var DirectionCardinalGeographicLetter = /* @__PURE__ */ ((DirectionCardinalGeographicLetter2) => { DirectionCardinalGeographicLetter2["NORTH"] = "N"; DirectionCardinalGeographicLetter2["EAST"] = "E"; DirectionCardinalGeographicLetter2["SOUTH"] = "S"; DirectionCardinalGeographicLetter2["WEST"] = "W"; return DirectionCardinalGeographicLetter2; })(DirectionCardinalGeographicLetter || {}); const isHorizontalDirectionCardinalGeographicLetter = (symbol) => symbol === "E" || symbol === "W"; const isVericalDirectionCardinalGeographicLetter = (symbol) => symbol === "N" || symbol === "S"; const isDirectionCardinalGeographicLetter = (marker) => isHorizontalDirectionCardinalGeographicLetter(marker) || isVericalDirectionCardinalGeographicLetter(marker); var DirectionCardinalLiteralLetter = /* @__PURE__ */ ((DirectionCardinalLiteralLetter2) => { DirectionCardinalLiteralLetter2["NORTH"] = "U"; DirectionCardinalLiteralLetter2["EAST"] = "R"; DirectionCardinalLiteralLetter2["SOUTH"] = "D"; DirectionCardinalLiteralLetter2["WEST"] = "L"; return DirectionCardinalLiteralLetter2; })(DirectionCardinalLiteralLetter || {}); const isHorizontalDirectionCardinalLiteralLetter = (symbol) => symbol === "R" || symbol === "L"; const isVericalDirectionCardinalLiteralLetter = (symbol) => symbol === "U" || symbol === "D"; const isDirectionCardinalLiteralLetter = (marker) => isHorizontalDirectionCardinalLiteralLetter(marker) || isVericalDirectionCardinalLiteralLetter(marker); const isDirectionMarker = (marker) => isDirectionArrowSymbol(marker) || isDirectionCardinalGeographicLetter(marker) || isDirectionCardinalLiteralLetter(marker); class Vec2 { static ORIGIN = Object.freeze(new Vec2(0, 0)); static INIFINITY_NE = Object.freeze( new Vec2(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY) ); static INIFINITY_NW = Object.freeze( new Vec2(Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY) ); static INIFINITY_SE = Object.freeze( new Vec2(Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY) ); static INIFINITY_SW = Object.freeze( new Vec2(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY) ); x; y; constructor(x, y) { if (typeof x === "object") { this.x = x.x; this.y = x.y; } else if (typeof x === "number" && typeof y === "number") { this.x = x; this.y = y; } else if (typeof x === "string") { if (isDirectionMarker(x)) { switch (x) { case DirectionArrowSymbol.NORTH: case DirectionCardinalLiteralLetter.NORTH: case DirectionCardinalGeographicLetter.NORTH: { this.x = 0; this.y = 1; break; } case DirectionArrowSymbol.EAST: case DirectionCardinalLiteralLetter.EAST: case DirectionCardinalGeographicLetter.EAST: { this.x = 1; this.y = 0; break; } case DirectionArrowSymbol.SOUTH: case DirectionCardinalLiteralLetter.SOUTH: case DirectionCardinalGeographicLetter.SOUTH: { this.x = 0; this.y = -1; break; } case DirectionArrowSymbol.WEST: case DirectionCardinalLiteralLetter.WEST: case DirectionCardinalGeographicLetter.WEST: { this.x = -1; this.y = 0; break; } } } else { [this.x, this.y] = (x.match(regex.NUM) ?? []).map((s) => Number.parseInt(s, 10)); } } } static compareColumnFirst(a, b) { return a.x === b.x ? a.y - b.y : a.x - b.x; } /** * Negative if b is before */ static compareRowFirst(a, b) { return a.y === b.y ? a.x - b.x : a.y - b.y; } compareColumnFirst(o) { return Vec2.compareColumnFirst(this, o); } compareRowFirst(o) { return Vec2.compareRowFirst(this, o); } static comparator(a, b) { return a.y === b.y ? a.x - b.x : a.y - b.y; } static sort(a) { return a.sort(Vec2.comparator); } static isWithin(v, area) { return area.horizontal.contains(v.x) && area.vertical.contains(v.y); } isWithin(area) { return Vec2.isWithin(this, area); } static isFinite(v, partial = false) { if (typeof partial === "string") { const x = partial === "x" ? Number.isFinite(v.x) : true; const y = partial === "y" ? Number.isFinite(v.y) : true; return x && y; } else if (partial) { return Number.isFinite(v.x) || Number.isFinite(v.y); } else { return Number.isFinite(v.x) && Number.isFinite(v.y); } } isFinite(partial = false) { return Vec2.isFinite(this, partial); } clamp(area) { const xMax = Math.max(area.topLeft.x, area.bottomRight.x); const yMax = Math.max(area.topLeft.y, area.bottomRight.y); const xMin = Math.min(area.topLeft.x, area.bottomRight.x); const yMin = Math.min(area.topLeft.y, area.bottomRight.y); if (this.x > xMax) { this.x = xMax; } else if (this.x < xMin) { this.x = xMin; } if (this.y > yMax) { this.y = yMax; } else if (this.y < yMin) { this.y = yMin; } return this; } add(coord, options) { return this.clone().addMut(coord, options); } *generateVectorsAroundInManhattanRadius(radius) { if (radius === 0) { yield this; return; } const leftEdge = this.x - radius; const rightEdge = this.x + radius; yield new Vec2(leftEdge, this.y); yield new Vec2(rightEdge, this.y); yield new Vec2(this.x, this.y + radius); yield new Vec2(this.x, this.y - radius); for (let i = leftEdge + 1; i < this.x; i++) { const yDiff = i - leftEdge; yield new Vec2(leftEdge + i, this.y + yDiff); yield new Vec2(leftEdge + i, this.y - yDiff); yield new Vec2(rightEdge - i, this.y + yDiff); yield new Vec2(rightEdge - i, this.y - yDiff); } } addMut(v, options) { const originalX = this.x; const originalY = this.y; const diffX = v.x * (options?.times ?? 1); const diffY = v.y * (options?.times ?? 1); this.x += options?.flipX ? -diffX : diffX; this.y += options?.flipY ? -diffY : diffY; if (options?.limit && (typeof options.limit === "function" ? options.limit(this) : !Vec2.isWithin(this, options.limit))) { this.x = originalX; this.y = originalY; } return this; } sub(o, times = 1) { let ox = o.x; if (ox === Number.NEGATIVE_INFINITY) { ox = 0; } let oy = o.y; if (oy === Number.NEGATIVE_INFINITY) { oy = 0; } return new Vec2(this.x - ox * times, this.y - oy * times); } subMut(o, times = 1) { this.x -= o.x * times; this.y -= o.y * times; return this; } manhattan(x, y) { if (typeof x === "number" && typeof y === "number") { return Math.abs(x - this.x) + Math.abs(y - this.y); } else if (typeof x === "object") { return this.manhattan(x.x, x.y); } else { return 0; } } dist(o) { return Math.sqrt(Math.pow(o.x - this.x, 2) + Math.pow(o.y - this.y, 2)); } stepVec(to) { const dx = to.x - this.x; const dy = to.y - this.y; let g = gcd(dx, dy); const step = new Vec2(dx / g, dy / g); while (g !== 1) { g = gcd(step.x, step.y); step.x /= g; step.y /= g; } return step; } isInt() { return Math.floor(this.x) === this.x && Math.floor(this.y) === this.y; } floor() { this.x = Math.floor(this.x); this.y = Math.floor(this.y); return this; } ceil() { this.x = Math.ceil(this.x); this.y = Math.ceil(this.y); return this; } /** * TODO: remove duplicate method * @param o * @returns */ subtract(o) { const dx = o.x - this.x; const dy = o.y - this.y; return new Vec2(dx, dy); } subtractMut(o) { this.x = o.x - this.x; this.y = o.y - this.y; return this; } negateMut() { this.x = -this.x; this.y = -this.y; return this; } negate() { return new Vec2(-this.x, -this.y); } *reach(o, yieldStart = false, yieldEnd = false) { const stepVec = this.stepVec(o); const current = this.add(stepVec); if (yieldStart) { yield this.clone(); } while (!current.equals(o)) { yield current.clone(); current.addMut(stepVec); } if (yieldEnd) { yield current.clone(); } } los(f) { return f.filter((fo) => !fo.equals(this)).map( (o) => [...this.reach(o, false, true)].filter((l) => f.find((fi) => fi.equals(l))).sort((a, b) => this.dist(a) - this.dist(b)).shift() ).filter((a) => !!a).reduce((acc, n) => { if (!acc.some((a) => a.equals(n))) { acc.push(n); } return acc; }, []); } equals(o) { return this.x === o.x && this.y === o.y; } angle(o) { return Math.atan2(o.y - this.y, o.x - this.x) * 180 / Math.PI; } toString() { return Vec2.toString(this); } static toString(v) { return typeof v === "string" ? v : `${v.x},${v.y}`; } clone() { return new Vec2(this); } rotateLeft(times = 1, around = Vec2.ORIGIN) { this.subMut(around); for (let i = 0; i < times; i++) { const x = this.x; this.x = -this.y; this.y = x; this.addMut(around); } return this; } rotateRight(times = 1, around = Vec2.ORIGIN) { this.subMut(around); for (let i = 0; i < times; i++) { const y = this.y; this.y = -this.x; this.x = y; this.addMut(around); } return this; } isNeighbour(o) { return Math.abs(this.x - o.x) <= 1 && Math.abs(this.y - o.y) <= 1; } normalizeMut() { this.x = Math.max(Math.min(this.x, 1), -1); this.y = Math.max(Math.min(this.y, 1), -1); return this; } set(o) { this.x = o.x; this.y = o.y; return this; } applyChange(fn) { this.x = fn(this.x); this.y = fn(this.y); return this; } middle(o) { const result = this.add(o); result.x = result.x / 2; result.y = result.y / 2; return result; } } const INTERVAL_ENDPOINT_OPEN_QUALIFIER = "open"; const INTERVAL_ENDPOINT_CLOSED_QUALIFIER = "closed"; const INTERVAL_CLOSED = { lowQualifier: INTERVAL_ENDPOINT_CLOSED_QUALIFIER, highQualifier: INTERVAL_ENDPOINT_CLOSED_QUALIFIER }; const INTERVAL_CLOSED_OPEN = { lowQualifier: INTERVAL_ENDPOINT_CLOSED_QUALIFIER, highQualifier: INTERVAL_ENDPOINT_OPEN_QUALIFIER }; const INTERVAL_OPEN = { lowQualifier: INTERVAL_ENDPOINT_OPEN_QUALIFIER, highQualifier: INTERVAL_ENDPOINT_OPEN_QUALIFIER }; const INTERVAL_OPEN_CLOSED = { lowQualifier: INTERVAL_ENDPOINT_OPEN_QUALIFIER, highQualifier: INTERVAL_ENDPOINT_CLOSED_QUALIFIER }; class Interval { low; high; /** * @default 'open' */ lowQualifier; /** * @default 'closed' */ highQualifier; /** * Contains only one element, 0 */ static ZERO = new Interval(0, 0, INTERVAL_CLOSED); /** * Contains no elements */ static EMPTY = new Interval(0, 0, INTERVAL_OPEN); static INFINITE = new Interval( Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, INTERVAL_OPEN ); constructor(low, high, options = INTERVAL_CLOSED_OPEN) { this.low = Math.min(low, high); this.high = Math.max(low, high); this.lowQualifier = options.lowQualifier ?? INTERVAL_ENDPOINT_CLOSED_QUALIFIER; this.highQualifier = options.highQualifier ?? INTERVAL_ENDPOINT_OPEN_QUALIFIER; } static invertQualifier(qualifier) { return qualifier === "open" ? "closed" : "open"; } static invertDesignation(designation) { return designation === "low" ? "high" : "low"; } static closed(low, high) { return new Interval(low, high, INTERVAL_CLOSED); } static open(low, high) { return new Interval(low, high, INTERVAL_OPEN); } static closedOpen(low, high) { return new Interval(low, high, INTERVAL_CLOSED_OPEN); } static openClosed(low, high) { return new Interval(low, high, INTERVAL_OPEN_CLOSED); } static parse(span) { const asVec = new Vec2(span); return new Interval(asVec.x, asVec.y); } /** * Shrinks or extends the interval on both ends * @param n */ pad(n) { const newLow = this.low - n; const newHigh = this.high + n; this.low = Math.min(newLow, newHigh); this.high = Math.max(newLow, newHigh); } clampInto(n) { if (this.isTooLow(n)) { return this.lowest(); } else if (this.isTooHigh(n)) { return this.highest(); } else { return n; } } isClosedInterval() { return this.lowQualifier === INTERVAL_ENDPOINT_CLOSED_QUALIFIER && this.highQualifier === INTERVAL_ENDPOINT_CLOSED_QUALIFIER; } isOpenInterval() { return this.lowQualifier === INTERVAL_ENDPOINT_OPEN_QUALIFIER && this.highQualifier === INTERVAL_ENDPOINT_OPEN_QUALIFIER; } isClosedOpenInterval() { return this.lowQualifier === INTERVAL_ENDPOINT_CLOSED_QUALIFIER && this.highQualifier === INTERVAL_ENDPOINT_OPEN_QUALIFIER; } isOpenClosedInterval() { return this.lowQualifier === INTERVAL_ENDPOINT_OPEN_QUALIFIER && this.highQualifier === INTERVAL_ENDPOINT_CLOSED_QUALIFIER; } get length() { const bothClosedOffset = this.isClosedInterval() ? 1 : 0; const bothOpenOffset = this.isOpenInterval() ? -1 : 0; return Math.max(bothClosedOffset + bothOpenOffset + this.high - this.low, 0); } moveBy(by) { if (Number.isFinite(this.low)) { this.low += by; } if (Number.isFinite(this.high)) { this.high += by; } return this; } moveLowTo(low) { if (Number.isFinite(this.low)) { const diff = this.low - low; this.high = this.high - diff; } else if (this.low === Number.NEGATIVE_INFINITY && low !== Number.NEGATIVE_INFINITY) { this.high = Number.POSITIVE_INFINITY; } this.low = low; return this; } moveHighTo(high) { if (Number.isFinite(this.high)) { const diff = this.high - high; this.low = this.low - diff; } else if (this.high === Number.POSITIVE_INFINITY && high !== Number.POSITIVE_INFINITY) { this.low = Number.NEGATIVE_INFINITY; } this.high = high; return this; } map(mapper) { return this.reduce((acc, next) => { acc.push(mapper(next)); return acc; }, []); } reduce(reducer, initialValue) { let accumulator = initialValue; for (const item of this.iter()) { accumulator = reducer(accumulator, item); } return accumulator; } merge(...others) { return Interval.merge([this, ...others]); } mergeOne(other) { const [lowestLow] = [this, other].sort(Interval.compareByLow); const [, highestHigh] = [this, other].sort(Interval.compareByHigh); return this.intersects(other) ? new Interval(lowestLow.low, highestHigh.high, { lowQualifier: lowestLow.lowQualifier, highQualifier: highestHigh.highQualifier }) : void 0; } static complement(intervals, within) { const points = Interval.collectAllSignificantPoints(intervals).map( Interval.invertQualifiedNumber ); if (within) { points.push(...Interval.collectAllSignificantPoints(within)); } return Interval.mergeQualifiedNumbers(points, true).filter((m) => !m.isEmpty()); } /** * An interval is empty if both its high and low values are the same, and * it's not a closed interval */ static isEmpty(interval) { return interval.low === interval.high && (interval.lowQualifier === INTERVAL_ENDPOINT_OPEN_QUALIFIER || interval.highQualifier === INTERVAL_ENDPOINT_OPEN_QUALIFIER); } isEmpty() { return Interval.isEmpty(this); } /** * Returns this interval trimmed into another, using the lowest high value * and the highest low value. * * If the interval is completely enveloped, it is simply returned. * * If there is no intersection, undefined is returned. */ static trim(interval, within) { if (interval.isAfterOf(within) || interval.isBeforeOf(within)) { return void 0; } else if (within.envelops(interval)) { return interval; } else { let low = interval.low; let lowQualifier = interval.lowQualifier; if (interval.low < within.low) { low = within.low; lowQualifier = within.lowQualifier; } let high = interval.high; let highQualifier = interval.highQualifier; if (interval.high > within.high) { high = within.high; highQualifier = within.highQualifier; } return new Interval(low, high, { lowQualifier, highQualifier }); } } /** * Returns this interval trimmed into another, using the lowest high value * and the highest low value. * * If the interval is completely enveloped, it is simply returned. * * If there is no intersection, undefined is returned. */ trim(within) { return Interval.trim(this, within); } static invertQualifiedNumber(qualifiedNumber) { return { value: qualifiedNumber.value, originalDesignation: Interval.invertDesignation(qualifiedNumber.originalDesignation), highQualifier: qualifiedNumber.highQualifier, lowQualifier: qualifiedNumber.lowQualifier }; } static collectAllSignificantPoints(intervals) { const result = []; for (const interval of intervals) { if (!interval.isEmpty()) { result.push( { originalDesignation: "low", value: interval.low, lowQualifier: interval.lowQualifier, highQualifier: Interval.invertQualifier(interval.lowQualifier) }, { originalDesignation: "high", value: interval.high, lowQualifier: Interval.invertQualifier(interval.highQualifier), highQualifier: interval.highQualifier } ); } } return result.sort(Interval.compareQualifiedNumber); } static mergeQualifiedNumbers(qualifiedNumbers, useSort = true) { const result = []; if (useSort) { qualifiedNumbers.sort(Interval.compareQualifiedNumber); } const intervalStartStack = []; if (qualifiedNumbers[0]?.originalDesignation === "high") { intervalStartStack.push({ value: Number.NEGATIVE_INFINITY, lowQualifier: "open", highQualifier: "closed", originalDesignation: "low" }); } for (const qualifiedNumber of qualifiedNumbers) { if (qualifiedNumber.originalDesignation === "low") { intervalStartStack.push(qualifiedNumber); } else if (qualifiedNumber.originalDesignation === "high" && intervalStartStack.length > 0) { const matchingLow = intervalStartStack.shift(); const next = new Interval(matchingLow.value, qualifiedNumber.value, { lowQualifier: matchingLow.lowQualifier, highQualifier: qualifiedNumber.highQualifier }); if (!next.isEmpty()) { result.push(next); } } } const last = qualifiedNumbers.at(-1); if (last?.originalDesignation === "low") { result.push( new Interval(last.value, Number.POSITIVE_INFINITY, { lowQualifier: last.lowQualifier, highQualifier: "open" }) ); } return Interval.merge(result); } static merge(intervals) { const [first, ...remaining] = intervals.sort(Interval.compareByLow); if (first) { const result = [first]; for (const span of remaining) { const mergeBase = result.pop(); const mergeResult = mergeBase.mergeOne(span); if (mergeResult) { result.push(mergeResult); } else { result.push(mergeBase, span); } } return result; } else { return []; } } /** * Lowest possible integer number. This takes openness into account. low or low + 1 when open */ lowest() { return this.lowQualifier === INTERVAL_ENDPOINT_CLOSED_QUALIFIER ? this.low : this.low + 1; } /** * Highest possible integer number, this takes openness into account, high, or high - 1 when open */ highest() { return this.highQualifier === INTERVAL_ENDPOINT_CLOSED_QUALIFIER ? this.high : this.high - 1; } /** * Checks if a single value is above the high value */ static isTooHigh(interval, n) { return interval.highQualifier === INTERVAL_ENDPOINT_CLOSED_QUALIFIER ? interval.high < n : interval.high <= n; } /** * Checks if a single value is not above the high value */ static isBelowHigh(interval, n) { return !Interval.isTooHigh(interval, n); } /** * Checks if a single value is below the low value of the interval */ static isTooLow(interval, n) { return interval.lowQualifier === INTERVAL_ENDPOINT_OPEN_QUALIFIER ? n <= interval.low : n < interval.low; } /** * Checks if a single value is not below the low value of the interval */ static isAboveLow(interval, n) { return !Interval.isTooLow(interval, n); } /** * Checks if a single value is above the high value */ isTooHigh(n) { return Interval.isTooHigh(this, n); } /** * Checks if a single value is not above the high value */ isBelowHigh(n) { return Interval.isBelowHigh(this, n); } /** * Checks if a single value is below the low value of the interval */ isTooLow(n) { return Interval.isTooLow(this, n); } /** * Checks if a single value is not below the low value of the interval */ isAboveLow(n) { return Interval.isAboveLow(this, n); } *iter() { for (let i = this.lowest(); this.contains(i); i++) { yield i; } } collectValues() { return [...this.iter()]; } /** * Checks if the first parameter is completely enveloped by the second */ static envelops(a, b) { return Interval.isAboveLow(b, a.low) && Interval.isAboveLow(b, a.high) && Interval.isBelowHigh(b, a.low) && Interval.isBelowHigh(b, a.high); } /** * Checks if this interal is completely enveloped by the one passed in */ envelops(other) { return Interval.envelops(this, other); } /** * Returns if the first parameter is completely above of the second parameter */ static isAfterOf(a, b) { return Interval.isTooHigh(b, a.low) && Interval.isTooHigh(b, a.high); } /** * Returns if the first parameter is completely above of the second parameter */ static isBeforeOf(a, b) { return Interval.isTooLow(b, a.low) && Interval.isTooLow(b, a.high); } /** * Checks if an interval is completely below another */ isBeforeOf(other) { return Interval.isBeforeOf(this, other); } /** * Checks if an interval is completely above another */ isAfterOf(other) { return Interval.isAfterOf(this, other); } clone() { return new Interval(this.low, this.high, { lowQualifier: this.lowQualifier, highQualifier: this.highQualifier }); } contains(n) { return Interval.contains(this, n); } isFinite() { return Number.isFinite(this.low) && Number.isFinite(this.high); } intersection(other) { return Interval.intersection(this, other); } static intersection(a, b) { if (!Interval.intersects(a, b)) { return void 0; } else if (b.isFinite() && Interval.contains(a, b.low) && Interval.contains(a, b.high)) { return b; } else if (a.isFinite() && Interval.contains(b, a.low) && Interval.contains(b, a.high)) { return a; } else { const [, highestLow] = [a, b].sort(Interval.compareByLow); const [lowestHigh] = [a, b].sort(Interval.compareByLow); return new Interval(highestLow.low, lowestHigh.high, { lowQualifier: highestLow.lowQualifier, highQualifier: lowestHigh.highQualifier }); } } static intersect(intersections) { let result = intersections[0]; if (result === void 0) { return void 0; } for (let i = 1; i <= intersections.length; i++) { result = result?.intersection(intersections[i]); } return result; } /** * Comparator, comparing only the low end of an interval. When they are * equal, use the qualifier. * * For the low end, closed comes earlier */ static compareByLow(a, b) { return a.low === b.low ? a.lowQualifier === INTERVAL_ENDPOINT_OPEN_QUALIFIER ? 1 : -1 : a.low - b.low; } /** * a low designation comes before the high designation */ static compareEndpointDesignation(a, b) { return a === b ? 0 : a === "low" ? -1 : 1; } /** * Comparator, comparing qualified numbers * * For the low end, closed comes earlier */ static compareQualifiedNumber(a, b) { return a.value === b.value ? Interval.compareEndpointDesignation(a.originalDesignation, b.originalDesignation) : a.value - b.value; } /** * Comparator, comparing only the high end of an interval. When they are * equal, use the qualifier. * * For the high end, open comes earlier */ static compareByHigh(a, b) { return a.high === b.high ? a.highQualifier === INTERVAL_ENDPOINT_CLOSED_QUALIFIER ? 1 : -1 : a.high - b.high; } intersects(other) { return Interval.intersects(this, other); } static intersects(a, b) { return Interval.contains(a, b.low) || Interval.contains(a, b.high) || Interval.contains(b, a.low) || Interval.contains(b, a.high); } static contains(interval, n) { return Interval.isAboveLow(interval, n) && Interval.isBelowHigh(interval, n); } static equals(a, b) { return (a && b && a.low === b.low && a.high === b.high && (a.lowQualifier ?? "open") === (b.lowQualifier ?? "open") && (a.highQualifier ?? "closed") === (b.highQualifier ?? "closed")) ?? false; } equals(other) { return Interval.equals(this, other); } /** * * @returns a copy of this interval with both endpoint qualifiers being closed */ asClosed() { return new Interval(this.low, this.high, { lowQualifier: INTERVAL_ENDPOINT_CLOSED_QUALIFIER, highQualifier: INTERVAL_ENDPOINT_CLOSED_QUALIFIER }); } /** * * @returns a copy of this interval with the low endpoint qualifier being closed and the high open */ asClosedOpen() { return new Interval(this.low, this.high, { lowQualifier: INTERVAL_ENDPOINT_CLOSED_QUALIFIER, highQualifier: INTERVAL_ENDPOINT_OPEN_QUALIFIER }); } /** * * @returns a copy of this interval with both endpoint qualifiers being open */ asOpen() { return new Interval(this.low, this.high, { lowQualifier: INTERVAL_ENDPOINT_OPEN_QUALIFIER, highQualifier: INTERVAL_ENDPOINT_OPEN_QUALIFIER }); } /** * * @returns a copy of this interval with the low endpoint qualifier being open and the high closed */ asOpenClosed() { return new Interval(this.low, this.high, { lowQualifier: INTERVAL_ENDPOINT_OPEN_QUALIFIER, highQualifier: INTERVAL_ENDPOINT_CLOSED_QUALIFIER }); } closestEndTo(to) { const ld = Math.abs(to - this.low); const hd = Math.abs(this.high - to); return ld < hd ? this.low : this.high; } toString() { return `${this.lowQualifier === INTERVAL_ENDPOINT_OPEN_QUALIFIER ? "(" : "["}${this.low},${this.high}${this.highQualifier === INTERVAL_ENDPOINT_OPEN_QUALIFIER ? ")" : "]"}`; } } const lerp1D = (from, to, options) => { const lower = Math.min(from, to); const higher = Math.max(from, to); const start = options?.excludeStart ? lower + 1 : lower; const end = options?.excludeEnd ? higher - 1 : higher; if (start > end) { return []; } const result = []; for (let i = start; i <= end; i++) { result.push(i); } return result; }; const invMod = (a, m) => { return Number(invModBigInt(BigInt(a), BigInt(m))); }; const invModBigInt = (a, m) => { const b0 = m; let x0 = 0n; let x1 = 1n; let q; let tmp; if (m == 1n) { return 1n; } while (a > 1n) { if (m === 0n) { throw new Error("Multiplicative inverse does not exist, tried to divide by 0"); } q = a / m; tmp = a; a = m; m = tmp % m; tmp = x0; x0 = x1 - q * x0; x1 = tmp; } if (x1 < 0n) { x1 = x1 + b0; } return x1; }; const invModEgdc = (a, m) => { return Number(invModEgdcBigInt(BigInt(a), BigInt(m))); }; const invModEgdcBigInt = (a, m) => { const [g, x] = egcdBigInt(a, m); if (g === 1n) { return posModBigInt(x, m); } else { throw new Error(`Modular inverse of ${a} modulo ${m} does not exist`); } }; const modExp = (a, b, n) => Number(modExpBigInt(BigInt(a), BigInt(b), BigInt(n))); const modExpBigInt = (a, b, n) => { a = a % n; let result = 1n; let x = a; while (b > 0) { const leastSignificantBit = b % 2n; b = b >> 1n; if (leastSignificantBit == 1n) { result = result * x % n; } x = x * x % n; } return result; }; const hasToString = (t) => typeof t.toString === "function"; var DirectionArrowUnicodeSymbol = /* @__PURE__ */ ((DirectionArrowUnicodeSymbol2) => { DirectionArrowUnicodeSymbol2["NORTH"] = "↑"; DirectionArrowUnicodeSymbol2["EAST"] = "→"; DirectionArrowUnicodeSymbol2["SOUTH"] = "↓"; DirectionArrowUnicodeSymbol2["WEST"] = "←"; return DirectionArrowUnicodeSymbol2; })(DirectionArrowUnicodeSymbol || {}); const isHorizontalDirectionArrowUnicodeSymbol = (symbol) => symbol === "→" || symbol === "←"; const isVericalDirectionArrowUnicodeSymbol = (symbol) => symbol === "↑" || symbol === "↓"; const isDirectionArrowUnicodeSymbol = (symbol) => isHorizontalDirectionArrowUnicodeSymbol(symbol) || isVericalDirectionArrowUnicodeSymbol(symbol); var DirectionCardinalNumericClockwiseIndex = /* @__PURE__ */ ((DirectionCardinalNumericClockwiseIndex2) => { DirectionCardinalNumericClockwiseIndex2[DirectionCardinalNumericClockwiseIndex2["NORTH"] = 0] = "NORTH"; DirectionCardinalNumericClockwiseIndex2[DirectionCardinalNumericClockwiseIndex2["EAST"] = 1] = "EAST"; DirectionCardinalNumericClockwiseIndex2[DirectionCardinalNumericClockwiseIndex2["SOUTH"] = 2] = "SOUTH"; DirectionCardinalNumericClockwiseIndex2[DirectionCardinalNumericClockwiseIndex2["WEST"] = 3] = "WEST"; return DirectionCardinalNumericClockwiseIndex2; })(DirectionCardinalNumericClockwiseIndex || {}); const isHorizontalDirectionCardinalNumericClockwiseIndex = (index) => index === 1 || index === 3; const isVericalDirectionCardinalNumericClockwiseIndex = (index) => index === 0 || index === 2; const isDirectionCardinalNumericClockwiseIndex = (index) => isHorizontalDirectionCardinalNumericClockwiseIndex(index) || isVericalDirectionCardinalNumericClockwiseIndex(index); class Direction extends Vec2 { static SYSTEM = "Y-DOWN"; constructor(x, y) { super(x, y); } /** * Returns the cardinal index of this direction clockwise * NORTH: 0 * EAST: 1 * SOUTH: 2 * WEST: 3 */ get cardinalValue() { if (this.equals(Direction.NORTH)) { return DirectionCardinalNumericClockwiseIndex.NORTH; } else if (this.equals(Direction.EAST)) { return DirectionCardinalNumericClockwiseIndex.EAST; } else if (this.equals(Direction.SOUTH)) { return DirectionCardinalNumericClockwiseIndex.SOUTH; } else if (this.equals(Direction.WEST)) { return DirectionCardinalNumericClockwiseIndex.WEST; } else { return void 0; } } get reverseValue() { return Direction.reverseValue(this.cardinalValue); } static ZERO = Object.freeze(new Direction(0, 0)); static EAST = Object.freeze(new Direction(1, 0)); static NORTHEAST = Object.freeze(new Direction(1, -1)); static NORTH = Object.freeze(new Direction(0, -1)); static NORTHWEST = Object.freeze(new Direction(-1, -1)); static WEST = Object.freeze(new Direction(-1, 0)); static SOUTHWEST = Object.freeze(new Direction(-1, 1)); static SOUTH = Object.freeze(new Direction(0, 1)); static SOUTHEAST = Object.freeze(new Direction(1, 1)); /** * Main directions * N, W, S, E * * Counter-Clockwise from east */ static cardinalDirections = Object.freeze([ Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST ]); /** * Diagonal directions (Intercardinal) * NE, SE, SW, NW * * Clockwise from north */ static ordinalDirections = Object.freeze([ Direction.NORTHEAST, Direction.SOUTHEAST, Direction.SOUTHWEST, Direction.NORTHWEST ]); /** * All 8 directions, cardinal and ordinal combined * E, NE, N, NW, W, SW, S, SE * * Counter-Clockwise from east */ static allDirections = Object.freeze([ Direction.EAST, Direction.NORTHEAST, Direction.NORTH, Direction.NORTHWEST, Direction.WEST, Direction.SOUTHWEST, Direction.SOUTH, Direction.SOUTHEAST ]); static isHorizonal(marker) { return marker === DirectionArrowSymbol.EAST || marker === DirectionArrowSymbol.WEST || marker === DirectionCardinalLiteralLetter.EAST || marker === DirectionCardinalLiteralLetter.WEST || marker === DirectionCardinalGeographicLetter.EAST || marker === DirectionCardinalGeographicLetter.WEST; } static isVertical(marker) { return marker === DirectionArrowSymbol.NORTH || marker === DirectionArrowSymbol.SOUTH || marker === DirectionCardinalLiteralLetter.NORTH || marker === DirectionCardinalLiteralLetter.SOUTH || marker === DirectionCardinalGeographicLetter.NORTH || marker === DirectionCardinalGeographicLetter.SOUTH; } sameAxis(other) { return this.isHorizonal() ? other.isHorizonal() : other.isVertical(); } static fromMarker(marker) { return directionMarkerAssociationMap[marker]; } static reverseValue(v) { switch (v) { case DirectionCardinalNumericClockwiseIndex.NORTH: { return DirectionCardinalNumericClockwiseIndex.SOUTH; } case DirectionCardinalNumericClockwiseIndex.EAST: { return DirectionCardinalNumericClockwiseIndex.WEST; } case DirectionCardinalNumericClockwiseIndex.SOUTH: { return DirectionCardinalNumericClockwiseIndex.NORTH; } case DirectionCardinalNumericClockwiseIndex.WEST: { return DirectionCardinalNumericClockwiseIndex.EAST; } default: { return void 0; } } } isHorizonal() { return this.equals(Direction.WEST) || this.equals(Direction.EAST); } isVertical() { return this.equals(Direction.NORTH) || this.equals(Direction.SOUTH); } /** * @param angle must be a multiple of 45, turns clockwise */ turn(angle) { const step = (this.angularValue + angle % 360) / 45; const direction = Direction.allDirections[(step + Direction.allDirections.length) % Direction.allDirections.length]; if (direction) { return direction; } else { throw new Error(`Not a valid angle: ${angle}, it must be a multiple of 45!`); } } /** * Returns angles from [0, 1] (east) * * Counter-Clockwise */ get angularValue() { if (this.equals(Direction.EAST)) return 0; else if (this.equals(Direction.NORTHEAST)) return 45; else if (this.equals(Direction.NORTH)) return 90; else if (this.equals(Direction.NORTHWEST)) return 135; else if (this.equals(Direction.WEST)) return 180; else if (this.equals(Direction.SOUTHWEST)) return 225; else if (this.equals(Direction.SOUTH)) return 270; else if (this.equals(Direction.SOUTHEAST)) return 315; else return 0; } /** * @param angle must be a multiple of 45 */ right(angle = 90) { return this.turn(-angle); } /** * @param angle must be a multiple of 45 */ left(angle = 90) { return this.turn(angle); } reverse(axis) { if ((!axis || axis === "v") && this.equals(Direction.NORTH)) return Direction.SOUTH; else if ((!axis || axis === "h") && this.equals(Direction.WEST)) return Direction.EAST; else if ((!axis || axis === "v") && this.equals(Direction.SOUTH)) return Direction.NORTH; else if ((!axis || axis === "h") && this.equals(Direction.EAST)) return Direction.WEST; else return this; } equals(that) { return this.x === that.x && this.y === that.y; } clone() { return new Direction(this); } get marker() { if (this.equals(Direction.EAST)) return DirectionArrowSymbol.EAST; else if (this.equals(Direction.NORTH)) return DirectionArrowSymbol.NORTH; else if (this.equals(Direction.WEST)) return DirectionArrowSymbol.WEST; else if (this.equals(Direction.SOUTH)) return DirectionArrowSymbol.SOUTH; else return " "; } static getNameOf(direction) { return Object.entries(directionNameMap).find(([, d]) => direction.equals(d))?.[0] ?? ""; } } const directionNameMap = { [DirectionNames.NORTH]: Direction.NORTH, [DirectionNames.EAST]: Direction.EAST, [DirectionNames.SOUTH]: Direction.SOUTH, [DirectionNames.WEST]: Direction.WEST }; const directionMarkerAssociationMap = { [DirectionArrowSymbol.NORTH]: Direction.NORTH, [DirectionCardinalGeographicLetter.NORTH]: Direction.NORTH, [DirectionCardinalLiteralLetter.NORTH]: Direction.NORTH, [DirectionArrowSymbol.EAST]: Direction.EAST, [DirectionCardinalGeographicLetter.EAST]: Direction.EAST, [DirectionCardinalLiteralLetter.EAST]: Direction.EAST, [DirectionArrowSymbol.SOUTH]: Direction.SOUTH, [DirectionCardinalGeographicLetter.SOUTH]: Direction.SOUTH, [DirectionCardinalLiteralLetter.SOUTH]: Direction.SOUTH, [DirectionArrowSymbol.WEST]: Direction.WEST, [DirectionCardinalGeographicLetter.WEST]: Direction.WEST, [DirectionCardinalLiteralLetter.WEST]: Direction.WEST, "": Direction.ZERO }; const directionMarkerInvertMap = { [DirectionArrowSymbol.NORTH]: DirectionArrowSymbol.SOUTH, [DirectionCardinalGeographicLetter.NORTH]: DirectionCardinalGeographicLetter.SOUTH, [DirectionCardinalLiteralLetter.NORTH]: DirectionCardinalLiteralLetter.SOUTH, [DirectionArrowSymbol.EAST]: DirectionArrowSymbol.WEST, [DirectionCardinalGeographicLetter.EAST]: DirectionCardinalGeographicLetter.WEST, [DirectionCardinalLiteralLetter.EAST]: DirectionCardinalLiteralLetter.WEST, [DirectionArrowSymbol.SOUTH]: DirectionArrowSymbol.NORTH, [DirectionCardinalGeographicLetter.SOUTH]: DirectionCardinalGeographicLetter.NORTH, [DirectionCardinalLiteralLetter.SOUTH]: DirectionCardinalLiteralLetter.NORTH, [DirectionArrowSymbol.WEST]: DirectionArrowSymbol.EAST, [DirectionCardinalGeographicLetter.WEST]: DirectionCardinalGeographicLetter.EAST, [DirectionCardinalLiteralLetter.WEST]: DirectionCardinalLiteralLetter.EAST, "": "" }; const constructPath = (start, end, prevMap) => { const path = []; if (end) { let u = end; if (start === u || prevMap.get(u)) { while (u) { path.unshift(u); u = prevMap.get(u); } } } return path; }; const calculateCostOfTraversal = (edge, currentPathWeighter, pathConstructor) => { let cost = 1; if (common.isNotNullish(currentPathWeighter) && common.isNotNullish(pathConstructor)) { cost = currentPathWeighter(edge.from, edge.to, edge.direction, pathConstructor(edge.to)); } else if (common.isNotNullish(edge.weight)) { cost = edge.weight; } return cost; }; const collectEdges = (node, options) => { const edgeGenerator = options?.edgeGenerator; let edges = edgeGenerator ? edgeGenerator(options.allNodes, node, options.pathConstructor(node)) : [...node.neighbours.values()]; const edgeFilter = options?.edgeFilter; if (edgeFilter) { edges = edges.filter((edge) => edgeFilter(edge, options.pathConstructor(node))); } return edges; }; const dijkstra = (options) => { const dist = /* @__PURE__ */ new Map(); const pathLengthMap = /* @__PURE__ */ new Map(); const prev = /* @__PURE__ */ new Map(); const pq = new jsSdsl.PriorityQueue(options.allNodes, (a, b) => { const aDist = dist.get(a) ?? Number.POSITIVE_INFINITY; const bDist = dist.get(b) ?? Number.POSITIVE_INFINITY; return aDist - bDist; }); const pathConstructor = (to) => constructPath(options.start, to, prev); const isFinished = common.isNotNullish(options.end) ? (n) => typeof options.end === "function" ? options.end(n, pathConstructor(n)) : n === options.end : (_n) => false; dist.set(options.start, 0); pathLengthMap.set(options.start, 0); pq.updateItem(options.start); let target; while (!pq.empty()) { const u = pq.pop(); if (isFinished(u)) { target = u; break; } const uDist = dist.get(u) ?? Number.POSITIVE_INFINITY; for (const neighbour of collectEdges(u, { pathConstructor, allNodes: options.allNodes, edgeFilter: options.edgeFilter, edgeGenerator: options.edgeGenerator })) { const weight = calculateCostOfTraversal( neighbour, options.currentPathWeighter, pathConstructor ); const tentativegScore = uDist + weight; const currentCost = dist.get(neighbour.to) ?? Number.POSITIVE_INFINITY; if (tentativegScore < currentCost) { dist.set(neighbour.to, tentativegScore); prev.set(neighbour.to, u); pathLengthMap.set(neighbour.to, (pathLengthMap.get(u) ?? 0) + 1); pq.updateItem(neighbour.to); } } } return { distances: pathLengthMap, path: target ? constructPath(options.start, target, prev) : [] }; }; const aStar = (options) => { if (!options.start) { return { path: [], distances: /* @__PURE__ */ new Map() }; } const h = options?.heuristic ?? (() => 1); const prev = /* @__PURE__ */ new Map(); const gScore = /* @__PURE__ */ new Map(); const fScore = /* @__PURE__ */ new Map(); const pathLengthMap = /* @__PURE__ */ new Map(); gScore.set(options.start, 0); fScore.set(options.start, options.end ? h(options.start, []) : 1); pathLengthMap.set(options.start, 0); const orderOfDiscovery = [options.start]; const pq = new jsSdsl.PriorityQueue([options.start], (a, b) => { const aScore = fScore.get(a) ?? Number.POSITIVE_INFINITY; const bScore = fScore.get(b) ?? Number.POSITIVE_INFINITY; if (aScore === bScore) { let aPathLength = orderOfDiscovery.indexOf(a); let bPathLength = orderOfDiscovery.indexOf(b); if (aPathLength < 0) { aPathLength = Number.POSITIVE_INFINITY; } if (bPathLength < 0) { bPathLength = Number.POSITIVE_INFINITY; } return aPathLength - bPathLength; } else { return aScore - bScore; } }); const pathConstructor = (to) => constructPath(options.start, to, prev); const isFinished = common.isNotNullish(options.end) ? (n) => typeof options.end === "function" ? options.end(n, pathConstructor(n)) : n === options.end : (_n) => false; let goal; while (pq.length > 0) { const current = pq.pop(); orderOfDiscovery.removeItem(current); if (isFinished(current)) { goal = current; break; } const uDist = gScore.get(current) ?? Number.POSITIVE_INFINITY; for (const neighbour of collectEdges(current, { pathConstructor, allNodes: options.allNodes, edgeFilter: options.edgeFilter, edgeGenerator: options.