UNPKG

@vscubing/cubing

Version:

A collection of JavaScript cubing libraries.

1,678 lines (1,657 loc) 54.5 kB
// src/cubing/alg/common.ts var writeAlgDebugField = false; var Comparable = class { is(c) { return this instanceof c; } as(c) { return this instanceof c ? this : null; } }; var AlgCommon = class extends Comparable { constructor() { super(); if (writeAlgDebugField) { Object.defineProperty(this, "_debugStr", { get: () => { return this.toString(); } }); } } get log() { return console.log.bind(console, this, this.toString()); } }; // src/cubing/alg/iteration.ts function toggleDirection(iterationDirection, flip = true) { if (!flip) { return iterationDirection; } switch (iterationDirection) { case 1 /* Forwards */: return -1 /* Backwards */; case -1 /* Backwards */: return 1 /* Forwards */; } } function direct(g, iterDir) { return iterDir === -1 /* Backwards */ ? Array.from(g).reverse() : g; } function reverse(g) { return Array.from(g).reverse(); } function* directedGenerator(g, direction) { direction === -1 /* Backwards */ ? yield* reverseGenerator(g) : yield* g; } function* reverseGenerator(g) { for (const t of Array.from(g).reverse()) { yield t; } } // src/cubing/alg/limits.ts var MAX_INT = 2147483647; var MAX_INT_DESCRIPTION = "2^31 - 1"; var MIN_INT = -2147483648; // src/cubing/alg/alg-nodes/containers/Commutator.ts var Commutator = class _Commutator extends AlgCommon { #A; #B; constructor(aSource, bSource) { super(); this.#A = experimentalEnsureAlg(aSource); this.#B = experimentalEnsureAlg(bSource); } get A() { return this.#A; } get B() { return this.#B; } isIdentical(other) { const otherAsCommutator = other.as(_Commutator); return !!(otherAsCommutator?.A.isIdentical(this.A) && otherAsCommutator?.B.isIdentical(this.B)); } invert() { return new _Commutator(this.#B, this.#A); } *experimentalExpand(iterDir = 1 /* Forwards */, depth) { depth ??= Infinity; if (depth === 0) { yield iterDir === 1 /* Forwards */ ? this : this.invert(); } else { if (iterDir === 1 /* Forwards */) { yield* this.A.experimentalExpand( 1 /* Forwards */, depth - 1 ); yield* this.B.experimentalExpand( 1 /* Forwards */, depth - 1 ); yield* this.A.experimentalExpand( -1 /* Backwards */, depth - 1 ); yield* this.B.experimentalExpand( -1 /* Backwards */, depth - 1 ); } else { yield* this.B.experimentalExpand( 1 /* Forwards */, depth - 1 ); yield* this.A.experimentalExpand( 1 /* Forwards */, depth - 1 ); yield* this.B.experimentalExpand( -1 /* Backwards */, depth - 1 ); yield* this.A.experimentalExpand( -1 /* Backwards */, depth - 1 ); } } } toString(experimentalSerializationOptions) { return `[${this.#A.toString(experimentalSerializationOptions)}, ${this.#B.toString(experimentalSerializationOptions)}]`; } }; // src/cubing/alg/alg-nodes/containers/Conjugate.ts var Conjugate = class _Conjugate extends AlgCommon { #A; #B; constructor(aSource, bSource) { super(); this.#A = experimentalEnsureAlg(aSource); this.#B = experimentalEnsureAlg(bSource); } get A() { return this.#A; } get B() { return this.#B; } isIdentical(other) { const otherAsConjugate = other.as(_Conjugate); return !!(otherAsConjugate?.A.isIdentical(this.A) && otherAsConjugate?.B.isIdentical(this.B)); } invert() { return new _Conjugate(this.#A, this.#B.invert()); } *experimentalExpand(iterDir, depth) { depth ??= Infinity; if (depth === 0) { yield iterDir === 1 /* Forwards */ ? this : this.invert(); } else { yield* this.A.experimentalExpand(1 /* Forwards */, depth - 1); yield* this.B.experimentalExpand(iterDir, depth - 1); yield* this.A.experimentalExpand(-1 /* Backwards */, depth - 1); } } toString(experimentalSerializationOptions) { return `[${this.A.toString(experimentalSerializationOptions)}: ${this.B.toString(experimentalSerializationOptions)}]`; } }; // src/cubing/alg/alg-nodes/leaves/LineComment.ts var LineComment = class _LineComment extends AlgCommon { #text; constructor(commentText) { super(); if (commentText.includes("\n") || commentText.includes("\r")) { throw new Error("LineComment cannot contain newline"); } this.#text = commentText; } get text() { return this.#text; } isIdentical(other) { const otherAsLineComment = other; return other.is(_LineComment) && this.#text === otherAsLineComment.#text; } invert() { return this; } *experimentalExpand(_iterDir = 1 /* Forwards */, _depth = Infinity) { yield this; } toString(experimentalSerializationOptions) { return `//${this.#text}`; } // toJSON(): LineCommentJSON { // return { // type: "comment", // text: this.#text, // }; // } }; // src/cubing/alg/alg-nodes/leaves/Newline.ts var Newline = class _Newline extends AlgCommon { toString(experimentalSerializationOptions) { return "\n"; } isIdentical(other) { return other.is(_Newline); } invert() { return this; } *experimentalExpand(_iterDir = 1 /* Forwards */, _depth = Infinity) { yield this; } }; // src/cubing/alg/alg-nodes/leaves/Pause.ts var Pause = class _Pause extends AlgCommon { experimentalNISSGrouping; // TODO: tie this to the alg toString(experimentalSerializationOptions) { return "."; } isIdentical(other) { return other.is(_Pause); } invert() { return this; } *experimentalExpand(_iterDir = 1 /* Forwards */, _depth = Infinity) { yield this; } }; // src/cubing/alg/AlgBuilder.ts var AlgBuilder = class { #algNodes = []; push(u) { this.#algNodes.push(u); } // TODO: Allow FlexibleAlgSource? /** @deprecated */ experimentalPushAlg(alg) { for (const u of alg.childAlgNodes()) { this.push(u); } } // TODO: can we guarantee this to be fast in the permanent API? experimentalNumAlgNodes() { return this.#algNodes.length; } // can be called multiple times, even if you push alg nodes inbetween. toAlg() { return new Alg(this.#algNodes); } reset() { this.#algNodes = []; } }; // src/cubing/alg/debug.ts var algDebugGlobals = { caretNISSNotationEnabled: true }; function setAlgDebug(options) { if ("caretNISSNotationEnabled" in options) { algDebugGlobals.caretNISSNotationEnabled = !!options.caretNISSNotationEnabled; } } // src/cubing/alg/parseAlg.ts function parseIntWithEmptyFallback(n, emptyFallback) { return n ? parseInt(n) : emptyFallback; } var AMOUNT_REGEX = /^(\d+)?('?)/; var MOVE_START_REGEX = /^[_\dA-Za-z]/; var QUANTUM_MOVE_REGEX = /^((([1-9]\d*)-)?([1-9]\d*))?([_A-Za-z]+)/; var COMMENT_TEXT_REGEX = /^[^\n]*/; var SQUARE1_PAIR_START_REGEX = /^(-?\d+), ?/; var SQUARE1_PAIR_END_REGEX = /^(-?\d+)\)/; function parseAlg(s) { return new AlgParser().parseAlg(s); } function parseMove(s) { return new AlgParser().parseMove(s); } function parseQuantumMove(s) { return new AlgParser().parseQuantumMove(s); } var startCharIndexKey = Symbol("startCharIndex"); var endCharIndexKey = Symbol("endCharIndex"); function addCharIndices(t, startCharIndex, endCharIndex) { const parsedT = t; parsedT[startCharIndexKey] = startCharIndex; parsedT[endCharIndexKey] = endCharIndex; return parsedT; } function transferCharIndex(from, to) { if (startCharIndexKey in from) { to[startCharIndexKey] = from[startCharIndexKey]; } if (endCharIndexKey in from) { to[endCharIndexKey] = from[endCharIndexKey]; } return to; } var AlgParser = class { #input = ""; #idx = 0; #nissQueue = []; parseAlg(input) { this.#input = input; this.#idx = 0; const alg = this.parseAlgWithStopping([]); this.mustBeAtEndOfInput(); const algNodes = Array.from(alg.childAlgNodes()); if (this.#nissQueue.length > 0) { for (const nissGrouping of this.#nissQueue.reverse()) { algNodes.push(nissGrouping); } } const newAlg = new Alg(algNodes); const { [startCharIndexKey]: startCharIndex, [endCharIndexKey]: endCharIndex } = alg; addCharIndices(newAlg, startCharIndex, endCharIndex); return newAlg; } parseMove(input) { this.#input = input; this.#idx = 0; const move = this.parseMoveImpl(); this.mustBeAtEndOfInput(); return move; } parseQuantumMove(input) { this.#input = input; this.#idx = 0; const quantumMove = this.parseQuantumMoveImpl(); this.mustBeAtEndOfInput(); return quantumMove; } mustBeAtEndOfInput() { if (this.#idx !== this.#input.length) { throw new Error("parsing unexpectedly ended early"); } } parseAlgWithStopping(stopBefore) { let algStartIdx = this.#idx; let algEndIdx = this.#idx; const algBuilder = new AlgBuilder(); let crowded = false; const mustNotBeCrowded = (idx) => { if (crowded) { throw new Error( `Unexpected character at index ${idx}. Are you missing a space?` ); } }; mainLoop: while (this.#idx < this.#input.length) { const savedCharIndex = this.#idx; if (stopBefore.includes(this.#input[this.#idx])) { return addCharIndices(algBuilder.toAlg(), algStartIdx, algEndIdx); } if (this.tryConsumeNext(" ")) { crowded = false; if (algBuilder.experimentalNumAlgNodes() === 0) { algStartIdx = this.#idx; } continue mainLoop; } else if (MOVE_START_REGEX.test(this.#input[this.#idx])) { mustNotBeCrowded(savedCharIndex); const move = this.parseMoveImpl(); algBuilder.push(move); crowded = true; algEndIdx = this.#idx; continue mainLoop; } else if (this.tryConsumeNext("(")) { mustNotBeCrowded(savedCharIndex); const sq1PairStartMatch = this.tryRegex(SQUARE1_PAIR_START_REGEX); if (sq1PairStartMatch) { const topAmountString = sq1PairStartMatch[1]; const savedCharIndexD = this.#idx; const sq1PairEndMatch = this.parseRegex(SQUARE1_PAIR_END_REGEX); const uMove = addCharIndices( new Move(new QuantumMove("U_SQ_"), parseInt(topAmountString)), savedCharIndex + 1, savedCharIndex + 1 + topAmountString.length ); const dMove = addCharIndices( new Move(new QuantumMove("D_SQ_"), parseInt(sq1PairEndMatch[1])), savedCharIndexD, this.#idx - 1 ); const alg = addCharIndices( new Alg([uMove, dMove]), savedCharIndex + 1, this.#idx - 1 ); algBuilder.push( addCharIndices(new Grouping(alg), savedCharIndex, this.#idx) ); crowded = true; algEndIdx = this.#idx; continue mainLoop; } else { const alg = this.parseAlgWithStopping([")"]); this.mustConsumeNext(")"); const amount = this.parseAmount(); algBuilder.push( addCharIndices( new Grouping(alg, amount), savedCharIndex, this.#idx ) ); crowded = true; algEndIdx = this.#idx; continue mainLoop; } } else if (this.tryConsumeNext("^")) { if (!algDebugGlobals.caretNISSNotationEnabled) { throw new Error( "Alg contained a caret but caret NISS notation is not enabled." ); } this.mustConsumeNext("("); const alg = this.parseAlgWithStopping([")"]); this.popNext(); const grouping = new Grouping(alg, -1); const placeholder = new Pause(); grouping.experimentalNISSPlaceholder = placeholder; placeholder.experimentalNISSGrouping = grouping; this.#nissQueue.push(grouping); algBuilder.push(placeholder); } else if (this.tryConsumeNext("[")) { mustNotBeCrowded(savedCharIndex); const A = this.parseAlgWithStopping([",", ":"]); const separator = this.popNext(); const B = this.parseAlgWithStopping(["]"]); this.mustConsumeNext("]"); let unrepeated; switch (separator) { case ":": { unrepeated = addCharIndices( new Conjugate(A, B), savedCharIndex, this.#idx ); crowded = true; algEndIdx = this.#idx; break; } case ",": { unrepeated = addCharIndices( new Commutator(A, B), savedCharIndex, this.#idx ); crowded = true; algEndIdx = this.#idx; break; } default: throw new Error("unexpected parsing error"); } const afterClosingBracketIdx = this.#idx; const amount = this.parseAmount(); if (amount === 1) { algBuilder.push(unrepeated); } else { const unrepeatedAlg = addCharIndices( new Alg([unrepeated]), savedCharIndex, afterClosingBracketIdx ); const grouping = addCharIndices( new Grouping(unrepeatedAlg, amount), savedCharIndex, this.#idx ); algBuilder.push(grouping); } crowded = true; algEndIdx = this.#idx; continue mainLoop; } else if (this.tryConsumeNext("\n")) { algBuilder.push( addCharIndices(new Newline(), savedCharIndex, this.#idx) ); crowded = false; algEndIdx = this.#idx; continue mainLoop; } else if (this.tryConsumeNext("/")) { if (this.tryConsumeNext("/")) { mustNotBeCrowded(savedCharIndex); const [text] = this.parseRegex(COMMENT_TEXT_REGEX); algBuilder.push( addCharIndices(new LineComment(text), savedCharIndex, this.#idx) ); crowded = false; algEndIdx = this.#idx; continue mainLoop; } else { algBuilder.push( addCharIndices(new Move("_SLASH_"), savedCharIndex, this.#idx) ); crowded = true; algEndIdx = this.#idx; continue mainLoop; } } else if (this.tryConsumeNext(".")) { mustNotBeCrowded(savedCharIndex); algBuilder.push(addCharIndices(new Pause(), savedCharIndex, this.#idx)); crowded = true; algEndIdx = this.#idx; continue mainLoop; } else { throw new Error(`Unexpected character: ${this.popNext()}`); } } if (this.#idx !== this.#input.length) { throw new Error("did not finish parsing?"); } if (stopBefore.length > 0) { throw new Error("expected stopping"); } return addCharIndices(algBuilder.toAlg(), algStartIdx, algEndIdx); } parseQuantumMoveImpl() { const [, , , outerLayerStr, innerLayerStr, family] = this.parseRegex(QUANTUM_MOVE_REGEX); return new QuantumMove( family, parseIntWithEmptyFallback(innerLayerStr, void 0), parseIntWithEmptyFallback(outerLayerStr, void 0) ); } parseMoveImpl() { const savedCharIndex = this.#idx; if (this.tryConsumeNext("/")) { return addCharIndices(new Move("_SLASH_"), savedCharIndex, this.#idx); } let quantumMove = this.parseQuantumMoveImpl(); let [amount, hadEmptyAbsAmount] = this.parseAmountAndTrackEmptyAbsAmount(); const suffix = this.parseMoveSuffix(); if (suffix) { if (amount < 0) { throw new Error("uh-oh"); } if ((suffix === "++" || suffix === "--") && amount !== 1) { throw new Error( "Pochmann ++ or -- moves cannot have an amount other than 1." ); } if ((suffix === "++" || suffix === "--") && !hadEmptyAbsAmount) { throw new Error( "Pochmann ++ or -- moves cannot have an amount written as a number." ); } if ((suffix === "+" || suffix === "-") && hadEmptyAbsAmount) { throw new Error( "Clock dial moves must have an amount written as a natural number followed by + or -." ); } if (suffix.startsWith("+")) { quantumMove = quantumMove.modified({ family: `${quantumMove.family}_${suffix === "+" ? "PLUS" : "PLUSPLUS"}_` // TODO }); } if (suffix.startsWith("-")) { quantumMove = quantumMove.modified({ family: `${quantumMove.family}_${suffix === "-" ? "PLUS" : "PLUSPLUS"}_` // TODO }); amount *= -1; } } const move = addCharIndices( new Move(quantumMove, amount), savedCharIndex, this.#idx ); return move; } parseMoveSuffix() { if (this.tryConsumeNext("+")) { if (this.tryConsumeNext("+")) { return "++"; } return "+"; } if (this.tryConsumeNext("-")) { if (this.tryConsumeNext("-")) { return "--"; } return "-"; } return null; } parseAmountAndTrackEmptyAbsAmount() { const savedIdx = this.#idx; const [, absAmountStr, primeStr] = this.parseRegex(AMOUNT_REGEX); if (absAmountStr?.startsWith("0") && absAmountStr !== "0") { throw new Error( `Error at char index ${savedIdx}: An amount can only start with 0 if it's exactly the digit 0.` ); } return [ parseIntWithEmptyFallback(absAmountStr, 1) * (primeStr === "'" ? -1 : 1), !absAmountStr ]; } parseAmount() { const savedIdx = this.#idx; const [, absAmountStr, primeStr] = this.parseRegex(AMOUNT_REGEX); if (absAmountStr?.startsWith("0") && absAmountStr !== "0") { throw new Error( `Error at char index ${savedIdx}: An amount number can only start with 0 if it's exactly the digit 0.` ); } return parseIntWithEmptyFallback(absAmountStr, 1) * (primeStr === "'" ? -1 : 1); } parseRegex(regex) { const arr = regex.exec(this.remaining()); if (arr === null) { throw new Error("internal parsing error"); } this.#idx += arr[0].length; return arr; } // TOD: can we avoid this? tryRegex(regex) { const arr = regex.exec(this.remaining()); if (arr === null) { return null; } this.#idx += arr[0].length; return arr; } remaining() { return this.#input.slice(this.#idx); } popNext() { const next = this.#input[this.#idx]; this.#idx++; return next; } tryConsumeNext(expected) { if (this.#input[this.#idx] === expected) { this.#idx++; return true; } return false; } mustConsumeNext(expected) { const next = this.popNext(); if (next !== expected) { throw new Error( `expected \`${expected}\` while parsing, encountered ${next}` ); } return next; } }; // src/cubing/alg/warnOnce.ts var warned = /* @__PURE__ */ new Set(); function warnOnce(s) { if (!warned.has(s)) { console.warn(s); warned.add(s); } } // src/cubing/alg/alg-nodes/QuantumWithAmount.ts var QuantumWithAmount = class { quantum; amount; constructor(quantum, amount = 1) { this.quantum = quantum; this.amount = amount; if (!Number.isInteger(this.amount) || this.amount < MIN_INT || this.amount > MAX_INT) { throw new Error( `AlgNode amount absolute value must be a non-negative integer below ${MAX_INT_DESCRIPTION}.` ); } } suffix() { let s = ""; const absAmount = Math.abs(this.amount); if (absAmount !== 1) { s += absAmount; } if (this.amount < 0) { s += "'"; } return s; } isIdentical(other) { return this.quantum.isIdentical(other.quantum) && this.amount === other.amount; } // TODO: `Conjugate` and `Commutator` decrement `depth` inside the quantum, `Grouping` has to do it outside the quantum. *experimentalExpand(iterDir, depth) { const absAmount = Math.abs(this.amount); const newIterDir = toggleDirection(iterDir, this.amount < 0); for (let i = 0; i < absAmount; i++) { yield* this.quantum.experimentalExpand(newIterDir, depth); } } }; // src/cubing/alg/alg-nodes/leaves/Move.ts var QuantumMove = class _QuantumMove extends Comparable { #family; #innerLayer; #outerLayer; constructor(family, innerLayer, outerLayer) { super(); this.#family = family; this.#innerLayer = innerLayer ?? null; this.#outerLayer = outerLayer ?? null; Object.freeze(this); if (this.#innerLayer !== null && (!Number.isInteger(this.#innerLayer) || this.#innerLayer < 1 || this.#innerLayer > MAX_INT)) { throw new Error( `QuantumMove inner layer must be a positive integer below ${MAX_INT_DESCRIPTION}.` ); } if (this.#outerLayer !== null && (!Number.isInteger(this.#outerLayer) || this.#outerLayer < 1 || this.#outerLayer > MAX_INT)) { throw new Error( `QuantumMove outer layer must be a positive integer below ${MAX_INT_DESCRIPTION}.` ); } if (this.#outerLayer !== null && this.#innerLayer !== null && this.#innerLayer <= this.#outerLayer) { throw new Error( "QuantumMove outer layer must be smaller than inner layer." ); } if (this.#outerLayer !== null && this.#innerLayer === null) { throw new Error( "QuantumMove with an outer layer must have an inner layer" ); } } static fromString(s) { return parseQuantumMove(s); } // TODO: `modify`? modified(modifications) { return new _QuantumMove( modifications.family ?? this.#family, modifications.innerLayer ?? this.#innerLayer, modifications.outerLayer ?? this.#outerLayer ); } isIdentical(other) { const otherAsQuantumMove = other; return other.is(_QuantumMove) && this.#family === otherAsQuantumMove.#family && this.#innerLayer === otherAsQuantumMove.#innerLayer && this.#outerLayer === otherAsQuantumMove.#outerLayer; } // TODO: provide something more useful on average. /** @deprecated */ get family() { return this.#family; } // TODO: provide something more useful on average. /** @deprecated */ get outerLayer() { return this.#outerLayer; } // TODO: provide something more useful on average. /** @deprecated */ get innerLayer() { return this.#innerLayer; } experimentalExpand() { throw new Error( "experimentalExpand() cannot be called on a `QuantumMove` directly." ); } toString(experimentalSerializationOptions) { let s = this.#family; if (this.#innerLayer !== null) { s = String(this.#innerLayer) + s; if (this.#outerLayer !== null) { s = `${String(this.#outerLayer)}-${s}`; } } return s; } }; var Move = class _Move extends AlgCommon { #quantumWithAmount; constructor(...args) { super(); if (typeof args[0] === "string") { if (args[1] ?? null) { this.#quantumWithAmount = new QuantumWithAmount( QuantumMove.fromString(args[0]), args[1] ); return; } else { return _Move.fromString(args[0]); } } this.#quantumWithAmount = new QuantumWithAmount( args[0], args[1] ); } isIdentical(other) { const otherAsMove = other.as(_Move); return !!otherAsMove && this.#quantumWithAmount.isIdentical(otherAsMove.#quantumWithAmount); } invert() { return transferCharIndex( this, new _Move( this.#quantumWithAmount.quantum, this.#isSlash() ? this.amount : -this.amount ) ); } *experimentalExpand(iterDir = 1 /* Forwards */) { if (iterDir === 1 /* Forwards */) { yield this; } else { yield this.modified({ amount: -this.amount }); } } get quantum() { return this.#quantumWithAmount.quantum; } // TODO: `modify`? modified(modifications) { return new _Move( this.#quantumWithAmount.quantum.modified(modifications), modifications.amount ?? this.amount ); } static fromString(s) { return parseMove(s); } get amount() { return this.#quantumWithAmount.amount; } /** @deprecated */ get type() { warnOnce("deprecated: type"); return "blockMove"; } /** @deprecated */ get family() { return this.#quantumWithAmount.quantum.family ?? void 0; } /** @deprecated */ get outerLayer() { return this.#quantumWithAmount.quantum.outerLayer ?? void 0; } /** @deprecated */ get innerLayer() { return this.#quantumWithAmount.quantum.innerLayer ?? void 0; } #cachedSlashMove; #isSlash() { return this.isIdentical(this.#cachedSlashMove ??= new _Move("_SLASH_")); } toString(experimentalSerializationOptions) { if (experimentalSerializationOptions?.notation !== "LGN") { if (this.#isSlash()) { return "/"; } if (this.family.endsWith("_PLUS_")) { return this.#quantumWithAmount.quantum.toString().slice(0, -6) + Math.abs(this.amount) + (this.amount < 0 ? "-" : "+"); } if (this.family.endsWith("_PLUSPLUS_")) { const absAmount = Math.abs(this.amount); return this.#quantumWithAmount.quantum.toString().slice(0, -10) + (absAmount === 1 ? "" : absAmount) + (this.amount < 0 ? "--" : "++"); } } return this.#quantumWithAmount.quantum.toString( experimentalSerializationOptions ) + this.#quantumWithAmount.suffix(); } // // TODO: Serialize as a string? // toJSON(): MoveJSON { // return { // type: "move", // family: this.family, // innerLayer: this.innerLayer, // outerLayer: this.outerLayer, // }; // } }; // src/cubing/alg/alg-nodes/containers/Grouping.ts var Square1TupleFormatter = class { quantumU_SQ_ = null; quantumD_SQ_ = null; format(grouping, experimentalSerializationOptions) { if (experimentalSerializationOptions?.notation === "LGN") { return null; } if (grouping.amount !== 1) { return null; } const amounts = this.tuple(grouping); if (!amounts) { return null; } return `(${amounts.map((move) => move.amount).join(", ")})`; } tuple(grouping) { if (grouping.amount !== 1) { return null; } this.quantumU_SQ_ ||= new QuantumMove("U_SQ_"); this.quantumD_SQ_ ||= new QuantumMove("D_SQ_"); const quantumAlg = grouping.alg; if (quantumAlg.experimentalNumChildAlgNodes() === 2) { const [U, D] = quantumAlg.childAlgNodes(); if (U.as(Move)?.quantum.isIdentical(this.quantumU_SQ_) && D.as(Move)?.quantum.isIdentical(this.quantumD_SQ_)) { return [U, D]; } } return null; } }; var square1TupleFormatterInstance = new Square1TupleFormatter(); var Grouping = class _Grouping extends AlgCommon { #quantumWithAmount; experimentalNISSPlaceholder; // TODO: tie this to the alg constructor(algSource, amount) { super(); const alg = experimentalEnsureAlg(algSource); this.#quantumWithAmount = new QuantumWithAmount(alg, amount); } isIdentical(other) { const otherAsGrouping = other; return other.is(_Grouping) && this.#quantumWithAmount.isIdentical(otherAsGrouping.#quantumWithAmount); } get alg() { return this.#quantumWithAmount.quantum; } get amount() { return this.#quantumWithAmount.amount; } /** @deprecated */ get experimentalRepetitionSuffix() { return this.#quantumWithAmount.suffix(); } invert() { const amounts = square1TupleFormatterInstance.tuple(this); if (amounts) { const [moveU, moveD] = amounts; return new _Grouping(new Alg([moveU.invert(), moveD.invert()])); } return new _Grouping( this.#quantumWithAmount.quantum, -this.#quantumWithAmount.amount ); } *experimentalExpand(iterDir = 1 /* Forwards */, depth) { depth ??= Infinity; if (depth === 0) { yield iterDir === 1 /* Forwards */ ? this : this.invert(); } else { yield* this.#quantumWithAmount.experimentalExpand(iterDir, depth - 1); } } static fromString() { throw new Error("unimplemented"); } #unrepeatedString(experimentalSerializationOptions) { const insideString = this.#quantumWithAmount.quantum.toString( experimentalSerializationOptions ); const iter = this.alg.childAlgNodes(); const { value } = iter.next(); if (iter.next().done && (value?.is(Commutator) || value?.is(Conjugate))) { return insideString; } return `(${insideString})`; } toString(experimentalSerializationOptions) { return square1TupleFormatterInstance.format( this, experimentalSerializationOptions ) ?? `${this.#unrepeatedString(experimentalSerializationOptions)}${this.#quantumWithAmount.suffix()}`; } experimentalAsSquare1Tuple() { return square1TupleFormatterInstance.tuple(this); } // toJSON(): GroupingJSON { // return { // type: "grouping", // alg: this.#quanta.quantum.toJSON(), // }; // } }; // src/cubing/alg/is.ts function experimentalIs(v, c) { return v instanceof c; } function experimentalIsAlgNode(v) { return experimentalIs(v, Grouping) || experimentalIs(v, LineComment) || experimentalIs(v, Commutator) || experimentalIs(v, Conjugate) || experimentalIs(v, Move) || experimentalIs(v, Newline) || experimentalIs(v, Pause); } // src/cubing/alg/traversal.ts function dispatch(t, algNode, dataDown) { if (algNode.is(Grouping)) { return t.traverseGrouping(algNode, dataDown); } if (algNode.is(Move)) { return t.traverseMove(algNode, dataDown); } if (algNode.is(Commutator)) { return t.traverseCommutator(algNode, dataDown); } if (algNode.is(Conjugate)) { return t.traverseConjugate(algNode, dataDown); } if (algNode.is(Pause)) { return t.traversePause(algNode, dataDown); } if (algNode.is(Newline)) { return t.traverseNewline(algNode, dataDown); } if (algNode.is(LineComment)) { return t.traverseLineComment(algNode, dataDown); } throw new Error("unknown AlgNode"); } function mustBeAlgNode(t) { if (t.is(Grouping) || t.is(Move) || t.is(Commutator) || t.is(Conjugate) || t.is(Pause) || t.is(Newline) || t.is(LineComment)) { return t; } throw new Error("internal error: expected AlgNode"); } var TraversalDownUp = class { // Immediate subclasses should overwrite this. traverseAlgNode(algNode, dataDown) { return dispatch(this, algNode, dataDown); } traverseIntoAlgNode(algNode, dataDown) { return mustBeAlgNode(this.traverseAlgNode(algNode, dataDown)); } }; var TraversalUp = class extends TraversalDownUp { traverseAlgNode(algNode) { return dispatch( this, algNode, void 0 ); } traverseIntoAlgNode(algNode) { return mustBeAlgNode(this.traverseAlgNode(algNode)); } }; function functionFromTraversal(traversalConstructor, constructorArgs) { const instance = new traversalConstructor( ...constructorArgs ?? [] ); return instance.traverseAlg.bind(instance); } // src/cubing/alg/simplify/options.ts var DEFAULT_DIRECTIONAL = "any-direction"; var AppendOptionsHelper = class { constructor(config = {}) { this.config = config; } cancelQuantum() { const { cancel } = this.config; if (cancel === true) { return DEFAULT_DIRECTIONAL; } if (cancel === false) { return "none"; } return cancel?.directional ?? "none"; } cancelAny() { return this.config.cancel && this.cancelQuantum() !== "none"; } cancelPuzzleSpecificModWrap() { const { cancel } = this.config; if (cancel === true || cancel === false) { return "canonical-centered"; } if (cancel?.puzzleSpecificModWrap) { return cancel?.puzzleSpecificModWrap; } return cancel?.directional === "same-direction" ? "preserve-sign" : "canonical-centered"; } puzzleSpecificSimplifyOptions() { return this.config.puzzleLoader?.puzzleSpecificSimplifyOptions ?? this.config.puzzleSpecificSimplifyOptions; } }; // src/cubing/alg/simplify/append.ts function areSameDirection(direction, move2) { return direction * Math.sign(move2.amount) >= 0; } function offsetMod(x, positiveMod, offset = 0) { return ((x - offset) % positiveMod + positiveMod) % positiveMod + offset; } function experimentalAppendMove(alg, addedMove, options) { const optionsHelper = new AppendOptionsHelper(options); const outputPrefix = Array.from(alg.childAlgNodes()); let outputSuffix = [addedMove]; function output() { return new Alg([...outputPrefix, ...outputSuffix]); } function modMove(move) { if (optionsHelper.cancelPuzzleSpecificModWrap() === "none") { return move; } const quantumMoveOrder = optionsHelper.puzzleSpecificSimplifyOptions()?.quantumMoveOrder; if (!quantumMoveOrder) { return move; } const mod = quantumMoveOrder(addedMove.quantum); let offset; switch (optionsHelper.cancelPuzzleSpecificModWrap()) { case "gravity": { offset = -Math.floor((mod - (move.amount < 0 ? 0 : 1)) / 2); break; } case "canonical-centered": { offset = -Math.floor((mod - 1) / 2); break; } case "canonical-positive": { offset = 0; break; } case "preserve-sign": { offset = move.amount < 0 ? 1 - mod : 0; break; } default: { throw new Error("Unknown mod wrap"); } } const offsetAmount = offsetMod(move.amount, mod, offset); return move.modified({ amount: offsetAmount }); } if (optionsHelper.cancelAny()) { let canCancelMoveBasedOnQuantum; const axis = optionsHelper.puzzleSpecificSimplifyOptions()?.axis; if (axis) { canCancelMoveBasedOnQuantum = (move) => axis.areQuantumMovesSameAxis(addedMove.quantum, move.quantum); } else { const newMoveQuantumString = addedMove.quantum.toString(); canCancelMoveBasedOnQuantum = (move) => move.quantum.toString() === newMoveQuantumString; } const sameDirectionOnly = optionsHelper.cancelQuantum() === "same-direction"; const quantumDirections = /* @__PURE__ */ new Map(); quantumDirections.set( addedMove.quantum.toString(), Math.sign(addedMove.amount) ); let i; for (i = outputPrefix.length - 1; i >= 0; i--) { const move = outputPrefix[i].as(Move); if (!move) { break; } if (!canCancelMoveBasedOnQuantum(move)) { break; } const quantumKey = move.quantum.toString(); if (sameDirectionOnly) { const existingQuantumDirectionOnAxis = quantumDirections.get(quantumKey); if (existingQuantumDirectionOnAxis && // Short-circuits, but that's actually okay here. !areSameDirection(existingQuantumDirectionOnAxis, move)) { break; } quantumDirections.set(quantumKey, Math.sign(move.amount)); } } const suffix = [...outputPrefix.splice(i + 1), addedMove]; if (axis) { outputSuffix = axis.simplifySameAxisMoves( suffix, optionsHelper.cancelPuzzleSpecificModWrap() !== "none" ); } else { const amount = suffix.reduce( (sum, move) => sum + move.amount, 0 ); if (quantumDirections.size !== 1) { throw new Error( "Internal error: multiple quantums when one was expected" ); } outputSuffix = [new Move(addedMove.quantum, amount)]; } } outputSuffix = outputSuffix.map((m) => modMove(m)).filter((move) => move.amount !== 0); return output(); } function experimentalAppendNode(alg, leaf, options) { const maybeMove = leaf.as(Move); if (maybeMove) { return experimentalAppendMove(alg, maybeMove, options); } else { return new Alg([...alg.childAlgNodes(), leaf]); } } // src/cubing/alg/simplify/simplify.ts var Simplify = class extends TraversalDownUp { #newPlaceholderAssociationsMap; #newPlaceholderAssociations() { return this.#newPlaceholderAssociationsMap ??= /* @__PURE__ */ new Map(); } // TODO: avoid allocations? #descendOptions(options) { return { ...options, depth: options.depth ? options.depth - 1 : null }; } // TODO: Handle *traverseAlg(alg, options) { if (options.depth === 0) { yield* alg.childAlgNodes(); return; } let output = []; const newOptions = this.#descendOptions(options); for (const algNode of alg.childAlgNodes()) { for (const traversedNode of this.traverseAlgNode(algNode, newOptions)) { output = Array.from( experimentalAppendNode( new Alg(output), traversedNode, newOptions ).childAlgNodes() ); } } for (const newAlgNode of output) { yield newAlgNode; } } *traverseGrouping(grouping, options) { if (options.depth === 0) { yield grouping; return; } if (grouping.amount === 0) { return; } const newGrouping = new Grouping( this.traverseAlg(grouping.alg, this.#descendOptions(options)), grouping.amount ); if (newGrouping.alg.experimentalIsEmpty()) { return; } const newPlaceholder = this.#newPlaceholderAssociations().get(grouping); if (newPlaceholder) { newGrouping.experimentalNISSPlaceholder = newPlaceholder; newPlaceholder.experimentalNISSGrouping = newGrouping; } yield newGrouping; } *traverseMove(move, _options) { yield move; } #doChildrenCommute(A, B, options) { if (A.experimentalNumChildAlgNodes() === 1 && B.experimentalNumChildAlgNodes() === 1) { const aMove = Array.from(A.childAlgNodes())[0]?.as(Move); const bMove = Array.from(B.childAlgNodes())[0]?.as(Move); if (!(aMove && bMove)) { return false; } if (bMove.quantum.isIdentical(aMove.quantum)) { return true; } const appendOptionsHelper = new AppendOptionsHelper(options); if (appendOptionsHelper.puzzleSpecificSimplifyOptions()?.axis?.areQuantumMovesSameAxis(aMove.quantum, bMove.quantum)) { return true; } } return false; } *traverseCommutator(commutator, options) { if (options.depth === 0) { yield commutator; return; } const newOptions = this.#descendOptions(options); const newCommutator = new Commutator( this.traverseAlg(commutator.A, newOptions), this.traverseAlg(commutator.B, newOptions) ); if (newCommutator.A.experimentalIsEmpty() || newCommutator.B.experimentalIsEmpty() || newCommutator.A.isIdentical(newCommutator.B) || newCommutator.A.isIdentical(newCommutator.B.invert()) || this.#doChildrenCommute(newCommutator.A, newCommutator.B, options)) { return; } yield newCommutator; } *traverseConjugate(conjugate, options) { if (options.depth === 0) { yield conjugate; return; } const newOptions = this.#descendOptions(options); const newConjugate = new Conjugate( this.traverseAlg(conjugate.A, newOptions), this.traverseAlg(conjugate.B, newOptions) ); if (newConjugate.B.experimentalIsEmpty()) { return; } if (newConjugate.A.experimentalIsEmpty() || newConjugate.A.isIdentical(newConjugate.B) || newConjugate.A.isIdentical(newConjugate.B.invert()) || this.#doChildrenCommute(newConjugate.A, newConjugate.B, options)) { yield* conjugate.B.childAlgNodes(); return; } yield newConjugate; } *traversePause(pause, _options) { if (pause.experimentalNISSGrouping) { const newPause = new Pause(); this.#newPlaceholderAssociations().set( pause.experimentalNISSGrouping, newPause ); yield newPause; } else { yield pause; } } *traverseNewline(newline, _options) { yield newline; } *traverseLineComment(comment, _options) { yield comment; } }; var simplify = functionFromTraversal(Simplify); // src/cubing/alg/Alg.ts function toIterable(input) { if (!input) { return []; } if (experimentalIs(input, Alg)) { return input.childAlgNodes(); } if (typeof input === "string") { return parseAlg(input).childAlgNodes(); } const iter = input; if (typeof iter[Symbol.iterator] === "function") { return iter; } throw new Error("Invalid AlgNode"); } function experimentalEnsureAlg(alg) { if (experimentalIs(alg, Alg)) { return alg; } return new Alg(alg); } var Alg = class _Alg extends AlgCommon { // #debugString: string; #algNodes; // TODO: freeze? constructor(alg) { super(); this.#algNodes = Array.from(toIterable(alg)); for (const algNode of this.#algNodes) { if (!experimentalIsAlgNode(algNode)) { throw new Error("An alg can only contain alg nodes."); } } } /** * Checks whether this Alg is structurally identical to another Alg. This * essentially means that they are written identically apart from whitespace. * * const alg1 = new Alg("R U L'"); * const alg2 = new Alg("L U' R'").invert(); * // true * alg1.isIdentical(alg2); * * // false * new Alg("[R, U]").isIdentical(new Alg("R U R' U'")); * // true * new Alg("[R, U]").expand().isIdentical(new Alg("R U R' U'")); * * Note that .isIdentical() efficiently compares algorithms, but mainly exists * to help optimize code when the structure of an algorithm hasn't changed. * There are many ways to write the "same" alg on most puzzles, but is * *highly* recommended to avoid expanding two Alg instances to compare them, * since that can easily slow your program to a crawl if someone inputs an alg * containing a large repetition. In general, you should use `cubing/kpuzzle` * to compare if two algs have the same effect on a puzzle. * * Also note that parser annotations are not taken into account while comparing * algs: * * const alg = new Alg([new Move("R"), new Move("U2")]); * // true, even though one of the algs has parser annotations * alg.isIdentical(new Alg("R U2")) * */ isIdentical(other) { const otherAsAlg = other; if (!other.is(_Alg)) { return false; } const l1 = Array.from(this.#algNodes); const l2 = Array.from(otherAsAlg.#algNodes); if (l1.length !== l2.length) { return false; } for (let i = 0; i < l1.length; i++) { if (!l1[i].isIdentical(l2[i])) { return false; } } return true; } /** * Returns the inverse of the given alg. * * Note that that this does not make any assumptions about what puzzle the alg * is for. For example, U2 is its own inverse on a cube, but U2' has the same * effect U3 (and not U2) on Megaminx: * * // Outputs: R U2' L' * new Alg("L U2 R'").invert().log(); */ invert() { return new _Alg(reverse(Array.from(this.#algNodes).map((u) => u.invert()))); } /** @deprecated Use {@link Alg.expand} instead. */ *experimentalExpand(iterDir = 1 /* Forwards */, depth) { depth ??= Infinity; for (const algNode of direct(this.#algNodes, iterDir)) { yield* algNode.experimentalExpand(iterDir, depth); } } /** * Expands all Grouping, Commutator, and Conjugate parts nested inside the * alg. * * // F R U R' U' F' * new Alg("[F: [R, U]]").expand().log(); * * // F [R, U] F' * new Alg("[F: [R, U]]").expand(({ depth: 1 }).log(); * * Avoid calling this on a user-provided alg unless the user explicitly asks * to see the expanded alg. Otherwise, it's easy to make your program freeze * when someone passes in an alg like: (R U)10000000 * * Generally, if you want to perform an operation on an entire alg, you'll * want to use something based on the `Traversal` mechanism, like countMoves() * from `cubing/notation`. */ expand(options) { return new _Alg( this.experimentalExpand( 1 /* Forwards */, options?.depth ?? Infinity ) ); } /** @deprecated */ *experimentalLeafMoves() { for (const leaf of this.experimentalExpand()) { if (leaf.is(Move)) { yield leaf; } } } concat(input) { return new _Alg( Array.from(this.#algNodes).concat(Array.from(toIterable(input))) ); } /** @deprecated */ experimentalIsEmpty() { for (const _ of this.#algNodes) { return false; } return true; } static fromString(s) { return parseAlg(s); } /** @deprecated */ units() { return this.childAlgNodes(); } *childAlgNodes() { for (const algNode of this.#algNodes) { yield algNode; } } /** @deprecated */ experimentalNumUnits() { return this.experimentalNumChildAlgNodes(); } experimentalNumChildAlgNodes() { return Array.from(this.#algNodes).length; } /** @deprecated */ get type() { warnOnce("deprecated: type"); return "sequence"; } /** * Converts the Alg to a string: * * const alg = new Alg([new Move("R"), new Move("U2"), new Move("L")]) * // R U2 L * console.log(alg.toString()) */ toString(experimentalSerializationOptions) { let output = ""; let previousVisibleAlgNode = null; for (const algNode of this.#algNodes) { if (previousVisibleAlgNode) { output += spaceBetween(previousVisibleAlgNode, algNode); } const nissGrouping = algNode.as(Pause)?.experimentalNISSGrouping; if (nissGrouping) { if (nissGrouping.amount !== -1) { throw new Error("Invalid NISS Grouping amount!"); } output += `^(${nissGrouping.alg.toString(experimentalSerializationOptions)})`; } else if (algNode.as(Grouping)?.experimentalNISSPlaceholder) { } else { output += algNode.toString(experimentalSerializationOptions); } previousVisibleAlgNode = algNode; } return output; } /** * `experimentalSimplify` can perform several mostly-syntactic simplifications on an alg: * * // Logs: R' U3 * import { Alg } from "@vscubing/cubing/alg"; * new Alg("R R2' U U2").experimentalSimplify({ cancel: true }).log() * * You can pass in a `PuzzleLoader` (currently only for 3x3x3) for puzzle-specific simplifications: * * // Logs: R' U' * import { Alg } from "@vscubing/cubing/alg"; * import { cube3x3x3 } from "@vscubing/cubing/puzzles"; * new Alg("R R2' U U2").experimentalSimplify({ cancel: true, puzzleLoader: cube3x3x3 }).log() * * You can also cancel only moves that are in the same direction: * * // Logs: R R2' U' * import { Alg } from "@vscubing/cubing/alg"; * import { cube3x3x3 } from "@vscubing/cubing/puzzles"; * new Alg("R R2' U U2").experimentalSimplify({ * cancel: { directional: "same-direction" }, * puzzleLoader: cube3x3x3 * }).log() * * Additionally, you can specify how moves are "wrapped": * * import { Alg } from "@vscubing/cubing/alg"; * import { cube3x3x3 } from "@vscubing/cubing/puzzles"; * * function example(puzzleSpecificModWrap) { * alg.experimentalSimplify({ * cancel: { puzzleSpecificModWrap }, * puzzleLoader: cube3x3x3 * }).log() * } * * const alg = new Alg("R7' . R6' . R5' . R6") * example("none") // R7' . R6' . R5' . R6 * example("gravity") // R . R2' . R' . R2 * example("canonical-centered") // R . R2 . R' . R2 * example("canonical-positive") // R . R2 . R3 . R2 * example("preserve-sign") // R3' . R2' . R' . R2 * * Same-axis and simultaneous move canonicalization is not implemented yet: * * // Logs: R L R * import { Alg } from "@vscubing/cubing/alg"; * import { cube3x3x3 } from "@vscubing/cubing/puzzles"; * new Alg("R L R").experimentalSimplify({ cancel: true, puzzleLoader: cube3x3x3 }).log() */ experimentalSimplify(options) { return new _Alg(simplify(this, options ?? {})); } /** @deprecated See {@link experimentalSimplify} */ simplify(options) { return this.experimentalSimplify(options); } }; function spaceBetween(u1, u2) { if (u1.is(Newline) || u2.is(Newline)) { return ""; } if (u2.as(Grouping)?.experimentalNISSPlaceholder) { return ""; } if (u1.is(LineComment) && !u2.is(Newline)) { return "\n"; } return " "; } // src/cubing/alg/example.ts var Example = { Sune: new Alg([ new Move("R", 1), new Move("U", 1), new Move("R", -1), new Move("U", 1), new Move("R", 1), new Move("U", -2), new Move("R", -1) ]), AntiSune: new Alg([ new Move("R", 1), new Move("U", 2), new Move("R", -1), new Move("U", -1), new Move("R", 1), new Move("U", -1), new Move("R", -1) ]), SuneCommutator: new Alg([ new Commutator( new Alg([new Move("R", 1), new Move("U", 1), new Move("R", -2)]), new Alg([ new Conjugate(new Alg([new Move("R", 1)]), new Alg([new Move("U", 1)])) ]) ) ]), Niklas: new Alg([ new Move("R", 1), new Move("U", -1), new Move("L", -1), new Move("U", 1), new Move("R", -1), new Move("U", -1), new Move("L", 1), new Move("U", 1) ]), EPerm: new Alg([ new Move("x", -1), new Commutator( new Alg([ new Conjugate( new Alg([new Move("R", 1)]), new Alg([new Move("U", -1)]) ) ]), new Alg([new Move("D", 1)]) ), new Commutator( new Alg([ new Conjugate(new Alg([new Move("R", 1)]), new Alg([new Move("U", 1)])) ]), new Alg([new Move("D", 1)]) ), new Move("x"