cube-state-engine 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,8 @@
8
8
  "Bash(node -e ' *)",
9
9
  "Bash(node *)",
10
10
  "Bash(cd /tmp)",
11
- "Bash(sed -i 's/console.log\\(`\\\\\\\\nsideA.*cmll\\)}`\\);/console.log\\(\"\\\\\\\\nsideA=\"+sideA+\" up=\"+up+\": aFirst=\"+firstTrue\\(aDone\\)+\" aLast=\"+aDone[n-1]+\" bFirst=\"+firstTrue\\(bDone\\)+\" bLast=\"+bDone[n-1]+\" cmllFirst=\"+firstTrue\\(cmll\\)\\);/' trace.mjs)"
11
+ "Bash(sed -i 's/console.log\\(`\\\\\\\\nsideA.*cmll\\)}`\\);/console.log\\(\"\\\\\\\\nsideA=\"+sideA+\" up=\"+up+\": aFirst=\"+firstTrue\\(aDone\\)+\" aLast=\"+aDone[n-1]+\" bFirst=\"+firstTrue\\(bDone\\)+\" bLast=\"+bDone[n-1]+\" cmllFirst=\"+firstTrue\\(cmll\\)\\);/' trace.mjs)",
12
+ "Bash(npx jest *)"
12
13
  ]
13
14
  }
14
15
  }
package/dist/index.d.mts CHANGED
@@ -327,28 +327,22 @@ function rouxBlockPieces(geo, sideFace, upFace) {
327
327
  return { edges, corners };
328
328
  }
329
329
 
330
- // A Roux block is done when its three edges and two corners are all placed.
331
- // As with CMLL, the block stays intact while the slice between the two blocks is
332
- // unaligned, but the block's down/front/back facelets are compared against the
333
- // floating centers -- which the slice displaces. So we accept the block if ANY
334
- // of the four slice rotations makes its pieces match the centers: the slice
335
- // moves neither the blocks nor their corners/edges (none sit on the slice), it
336
- // only re-aligns the floating centers. Without this, a perfectly built block
337
- // reads as broken for most of a Roux solve, because the slice is scrambled until
338
- // LSE. `slicePerm` is the slice perpendicular to the block axis.
339
- function rouxBlockDone(st, geo, sideFace, upFace, slicePerm) {
330
+ // A Roux block is done when its three edges and two corners all match the
331
+ // centers. The block can be built while the slice between the two blocks is
332
+ // rotated away from the centers; that offset is handled by the caller, which
333
+ // evaluates each orientation against snapshots pre-rotated by a fixed slice
334
+ // amount (see the candidate enumeration). Keeping the offset FIXED per
335
+ // orientation -- rather than accepting any of the four rotations move-by-move --
336
+ // is what makes detection match how the solve was actually framed instead of
337
+ // firing on coincidental alignments.
338
+ function rouxBlockDone(st, geo, sideFace, upFace) {
340
339
  const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
341
340
  if (edges.length !== 3 || corners.length !== 2) return false;
342
- let cur = st;
343
- for (let m = 0; m < 4; m++) {
344
- if (m > 0) cur = applyPerm(cur, slicePerm);
345
- const centers = centersOf(cur, geo);
346
- const ok =
347
- edges.every((e) => slotCorrect(cur, centers, e.indices, geo.per)) &&
348
- corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per));
349
- if (ok) return true;
350
- }
351
- return false;
341
+ const centers = centersOf(st, geo);
342
+ return (
343
+ edges.every((e) => slotCorrect(st, centers, e.indices, geo.per)) &&
344
+ corners.every((c) => slotCorrect(st, centers, c.indices, geo.per))
345
+ );
352
346
  }
353
347
 
354
348
  // Applies a permutation (out[i] = st[perm[i]]) to a flat sticker array.
@@ -358,23 +352,14 @@ function applyPerm(st, perm) {
358
352
  return out;
359
353
  }
360
354
 
361
- // CMLL is done when the four corners touching the up face are solved, allowing
362
- // the slice between the two blocks to be unaligned: we accept the state if ANY
363
- // of the four slice rotations makes those corners correct. The slice moves
364
- // neither the corners nor the two blocks, only the four floating centers (and
365
- // slice edges) around the block axis, so this captures exactly "last-layer
366
- // corners solved, slice not necessarily aligned yet". `slicePerm` must be the
367
- // slice perpendicular to the block axis (M for L/R blocks, E for U/D, S for F/B).
368
- function cmllDone(st, geo, upFace, slicePerm) {
369
- let cur = st;
370
- const corners = geo.cornersByFace(upFace);
371
- for (let m = 0; m < 4; m++) {
372
- if (m > 0) cur = applyPerm(cur, slicePerm);
373
- const centers = centersOf(cur, geo);
374
- if (corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per)))
375
- return true;
376
- }
377
- return false;
355
+ // CMLL corners are done when the four corners touching the up face match the
356
+ // centers. As with the blocks, any fixed slice offset is applied by the caller
357
+ // via pre-rotated snapshots, so this is a plain match against the centers.
358
+ function cmllCornersDone(st, geo, upFace) {
359
+ const centers = centersOf(st, geo);
360
+ return geo
361
+ .cornersByFace(upFace)
362
+ .every((c) => slotCorrect(st, centers, c.indices, geo.per));
378
363
  }
379
364
 
380
365
  // Index of the move that COMPLETES a stage: the first move after which the
@@ -389,6 +374,19 @@ function completionIndex(bools) {
389
374
  return null;
390
375
  }
391
376
 
377
+ // Index of the FIRST move strictly after `after` at which the condition holds.
378
+ // Used for a stage whose completion is "the first time X happens once the
379
+ // previous stage is done" -- e.g. the last-layer corners getting solved after
380
+ // the second block is built, even if the blocks are momentarily disturbed at
381
+ // that instant. Returns null if `after` is null or the condition never holds.
382
+ function firstIndexAfter(bools, after) {
383
+ if (after == null) return null;
384
+ for (let i = after + 1; i < bools.length; i++) {
385
+ if (bools[i]) return i;
386
+ }
387
+ return null;
388
+ }
389
+
392
390
  // Builds the milestone indices for one assumed cross color. Detection is
393
391
  // cumulative: an F2L pair only counts while the cross is solved, and OLL only
394
392
  // counts once the full F2L is solved. This rejects transient false positives.
@@ -444,57 +442,65 @@ function isCFOP(build) {
444
442
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
445
443
  }
446
444
 
447
- // Builds the Roux milestone indices for one (sideFace, upFace) orientation.
448
- // Detection is cumulative, mirroring buildForCross: the second block only counts
449
- // while the first block holds, CMLL only once both blocks hold, and LSE is the
450
- // fully solved cube. The first block is whichever of the two opposite side faces
451
- // finishes its block earliest; the other side is the second block.
452
- function buildForRoux(snapshots, geo, sideA, upFace, slicePerm) {
453
- snapshots.length;
445
+ // Builds the Roux milestone indices for one (sideFace, upFace) orientation,
446
+ // using snapshots already pre-rotated by this orientation's fixed slice offset.
447
+ //
448
+ // Milestones are taken at FIRST achievement, not while a condition holds
449
+ // continuously: once a stage is reached the solver moves on, and on face-move
450
+ // Roux later stages routinely break and rebuild earlier ones (a CMLL alg breaks
451
+ // a block; a face-move LSE breaks everything). So:
452
+ // 1st block = first instant either side's block is built;
453
+ // 2nd block = first instant the OTHER side's block is built afterwards -- NOT
454
+ // gated on the first block still being intact, which it often is
455
+ // not while the second is being inserted;
456
+ // CMLL = first instant the last-layer corners are solved afterwards;
457
+ // LSE = the fully solved cube (`lseIdx`, computed once by the caller and
458
+ // passed in, since "solved" is the same in every orientation).
459
+ function buildForRoux(snapshots, geo, sideA, upFace, lseIdx) {
454
460
  const sideB = geo.opposite[sideA];
455
461
 
456
- const aDone = snapshots.map((st) =>
457
- rouxBlockDone(st, geo, sideA, upFace, slicePerm)
458
- );
459
- const bDone = snapshots.map((st) =>
460
- rouxBlockDone(st, geo, sideB, upFace, slicePerm)
461
- );
462
- const aIdx = completionIndex(aDone);
463
- const bIdx = completionIndex(bDone);
464
-
465
- // First block = the side that completes earliest; second block = the other.
466
- let firstSide = sideA;
467
- let secondSide = sideB;
468
- let fbBools = aDone;
469
- let sbBools = bDone;
470
- if ((bIdx ?? Infinity) < (aIdx ?? Infinity)) {
471
- firstSide = sideB;
472
- secondSide = sideA;
473
- fbBools = bDone;
474
- sbBools = aDone;
475
- }
462
+ const aDone = snapshots.map((st) => rouxBlockDone(st, geo, sideA, upFace));
463
+ const bDone = snapshots.map((st) => rouxBlockDone(st, geo, sideB, upFace));
464
+ const aIdx = firstIndexAfter(aDone, -1);
465
+ const bIdx = firstIndexAfter(bDone, -1);
466
+ if (aIdx == null || bIdx == null) return null;
476
467
 
477
- const fbIdx = completionIndex(fbBools);
478
- // Second block gated by the first block holding at the same instant.
479
- const secondBlockBools = snapshots.map((_, i) => fbBools[i] && sbBools[i]);
480
- const sbIdx = completionIndex(secondBlockBools);
468
+ // First block = the side built earliest; the other side is the second block.
469
+ const aFirst = aIdx <= bIdx;
470
+ const firstSide = aFirst ? sideA : sideB;
471
+ const fbIdx = aFirst ? aIdx : bIdx;
472
+ const sbIdx = firstIndexAfter(aFirst ? bDone : aDone, fbIdx);
473
+ if (sbIdx == null) return null;
481
474
 
482
- const cmllIdx = completionIndex(
483
- snapshots.map(
484
- (st, i) => secondBlockBools[i] && cmllDone(st, geo, upFace, slicePerm)
485
- )
486
- );
487
- const lseIdx = completionIndex(snapshots.map((st) => isSolvedFlat(st, geo.per)));
475
+ const cornerBools = snapshots.map((st) => cmllCornersDone(st, geo, upFace));
476
+ const cmllIdx = firstIndexAfter(cornerBools, sbIdx);
477
+ if (cmllIdx == null) return null;
488
478
 
489
- return { firstSide, secondSide, upFace, fbIdx, sbIdx, cmllIdx, lseIdx };
479
+ return {
480
+ firstSide,
481
+ secondSide: aFirst ? sideB : sideA,
482
+ upFace,
483
+ fbIdx,
484
+ sbIdx,
485
+ cmllIdx,
486
+ lseIdx,
487
+ };
490
488
  }
491
489
 
492
490
  // Does this breakdown follow the Roux order: 1st block -> 2nd block -> CMLL -> LSE?
491
+ // Shortest plausible length (in moves) of a genuine Roux stage. The block and
492
+ // corner conditions flicker true at scattered transient instants throughout a
493
+ // solve; a "stage" only a move or two long is one of those coincidences, not a
494
+ // real second block or CMLL. Requiring a minimum span rejects them so a spurious
495
+ // orientation cannot masquerade as a clean Roux staging.
496
+ const ROUX_MIN_STAGE = 5;
497
+
493
498
  function isRoux(build) {
494
499
  const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
495
500
  if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
496
501
  return false;
497
- return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
502
+ if (!(fbIdx < sbIdx && sbIdx < cmllIdx && cmllIdx < lseIdx)) return false;
503
+ return sbIdx - fbIdx >= ROUX_MIN_STAGE && cmllIdx - sbIdx >= ROUX_MIN_STAGE;
498
504
  }
499
505
 
500
506
  /**
@@ -623,27 +629,60 @@ function analyzeSolution(moves, options = {}) {
623
629
  const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
624
630
 
625
631
  // Stage the solve as Roux (1st block -> 2nd block -> CMLL -> LSE) on every
626
- // orientation. Each candidate fixes a side face and a perpendicular up face;
627
- // buildForRoux assigns first/second block by which side finishes earliest.
628
- // The "floating" slice that block/CMLL detection lets drift is the one
629
- // perpendicular to the block axis: M for L/R blocks, E for U/D, S for F/B.
630
- // Pick the valid candidate whose first block completes earliest.
632
+ // orientation. A candidate fixes a side face, a perpendicular up face, and a
633
+ // slice offset `d`: the blocks can be assembled with the slice between them
634
+ // turned away from the centers, so each orientation is tested against
635
+ // snapshots pre-rotated by d quarter-turns of its axis slice (M for L/R blocks,
636
+ // E for U/D, S for F/B). Holding that offset fixed for the whole solve (rather
637
+ // than re-matching it move-by-move) is what stops a block reading as "built"
638
+ // on a coincidental alignment.
639
+ //
640
+ // Many orientations still yield a technically ordered staging. The one that
641
+ // reflects the real solve is the one whose three build milestones land
642
+ // EARLIEST in aggregate (smallest fb + sb + cmll): a false orientation only
643
+ // assembles its blocks and corners by coincidence, which does not happen
644
+ // earlier than the genuine build. (isRoux discards orientations whose stages
645
+ // are too short to be anything but a coincidence.) "Solved" is orientation-
646
+ // independent, so the LSE index is computed once and shared.
631
647
  const perms = getMovePermutations(size);
632
- const axisSlicePerm = (face) => {
648
+ const sliceCwOf = (face) => {
633
649
  if (face === 1 || face === 3) return perms["M"].cw; // L/R axis
634
650
  if (face === 0 || face === 5) return perms["E"].cw; // U/D axis
635
651
  return perms["S"].cw; // F/B axis
636
652
  };
653
+ const lseIdx = completionIndex(
654
+ snapshots.map((st) => isSolvedFlat(st, geo.per))
655
+ );
656
+ // Snapshots rotated by d quarter-turns of a given slice, built incrementally
657
+ // and cached so each (axis, d) is computed once and reused across its up faces.
658
+ const rotCache = new Map();
659
+ const rotatedSnaps = (sliceCw, d) => {
660
+ const key = `${sliceCw}:${d}`;
661
+ if (rotCache.has(key)) return rotCache.get(key);
662
+ const snaps =
663
+ d === 0
664
+ ? snapshots
665
+ : rotatedSnaps(sliceCw, d - 1).map((st) => applyPerm(st, sliceCw));
666
+ rotCache.set(key, snaps);
667
+ return snaps;
668
+ };
637
669
  const rouxCandidates = [];
638
670
  for (let s = 0; s < 6; s++) {
671
+ const sliceCw = sliceCwOf(s);
639
672
  for (const u of geo.neighbors[s]) {
640
- rouxCandidates.push(buildForRoux(snapshots, geo, s, u, axisSlicePerm(s)));
673
+ for (let d = 0; d < 4; d++) {
674
+ const build = buildForRoux(rotatedSnaps(sliceCw, d), geo, s, u, lseIdx);
675
+ if (build) rouxCandidates.push(build);
676
+ }
641
677
  }
642
678
  }
643
679
  const rouxBuild =
644
680
  rouxCandidates
645
681
  .filter((b) => isRoux(b))
646
- .sort((a, b) => a.fbIdx - b.fbIdx)[0] ?? null;
682
+ .sort(
683
+ (a, b) =>
684
+ a.fbIdx + a.sbIdx + a.cmllIdx - (b.fbIdx + b.sbIdx + b.cmllIdx)
685
+ )[0] ?? null;
647
686
 
648
687
  // A solved cube satisfies many orderings, so both stagings can be technically
649
688
  // valid. Disambiguate by which method's FIRST milestone is genuinely reached
package/dist/index.d.ts CHANGED
@@ -327,28 +327,22 @@ function rouxBlockPieces(geo, sideFace, upFace) {
327
327
  return { edges, corners };
328
328
  }
329
329
 
330
- // A Roux block is done when its three edges and two corners are all placed.
331
- // As with CMLL, the block stays intact while the slice between the two blocks is
332
- // unaligned, but the block's down/front/back facelets are compared against the
333
- // floating centers -- which the slice displaces. So we accept the block if ANY
334
- // of the four slice rotations makes its pieces match the centers: the slice
335
- // moves neither the blocks nor their corners/edges (none sit on the slice), it
336
- // only re-aligns the floating centers. Without this, a perfectly built block
337
- // reads as broken for most of a Roux solve, because the slice is scrambled until
338
- // LSE. `slicePerm` is the slice perpendicular to the block axis.
339
- function rouxBlockDone(st, geo, sideFace, upFace, slicePerm) {
330
+ // A Roux block is done when its three edges and two corners all match the
331
+ // centers. The block can be built while the slice between the two blocks is
332
+ // rotated away from the centers; that offset is handled by the caller, which
333
+ // evaluates each orientation against snapshots pre-rotated by a fixed slice
334
+ // amount (see the candidate enumeration). Keeping the offset FIXED per
335
+ // orientation -- rather than accepting any of the four rotations move-by-move --
336
+ // is what makes detection match how the solve was actually framed instead of
337
+ // firing on coincidental alignments.
338
+ function rouxBlockDone(st, geo, sideFace, upFace) {
340
339
  const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
341
340
  if (edges.length !== 3 || corners.length !== 2) return false;
342
- let cur = st;
343
- for (let m = 0; m < 4; m++) {
344
- if (m > 0) cur = applyPerm(cur, slicePerm);
345
- const centers = centersOf(cur, geo);
346
- const ok =
347
- edges.every((e) => slotCorrect(cur, centers, e.indices, geo.per)) &&
348
- corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per));
349
- if (ok) return true;
350
- }
351
- return false;
341
+ const centers = centersOf(st, geo);
342
+ return (
343
+ edges.every((e) => slotCorrect(st, centers, e.indices, geo.per)) &&
344
+ corners.every((c) => slotCorrect(st, centers, c.indices, geo.per))
345
+ );
352
346
  }
353
347
 
354
348
  // Applies a permutation (out[i] = st[perm[i]]) to a flat sticker array.
@@ -358,23 +352,14 @@ function applyPerm(st, perm) {
358
352
  return out;
359
353
  }
360
354
 
361
- // CMLL is done when the four corners touching the up face are solved, allowing
362
- // the slice between the two blocks to be unaligned: we accept the state if ANY
363
- // of the four slice rotations makes those corners correct. The slice moves
364
- // neither the corners nor the two blocks, only the four floating centers (and
365
- // slice edges) around the block axis, so this captures exactly "last-layer
366
- // corners solved, slice not necessarily aligned yet". `slicePerm` must be the
367
- // slice perpendicular to the block axis (M for L/R blocks, E for U/D, S for F/B).
368
- function cmllDone(st, geo, upFace, slicePerm) {
369
- let cur = st;
370
- const corners = geo.cornersByFace(upFace);
371
- for (let m = 0; m < 4; m++) {
372
- if (m > 0) cur = applyPerm(cur, slicePerm);
373
- const centers = centersOf(cur, geo);
374
- if (corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per)))
375
- return true;
376
- }
377
- return false;
355
+ // CMLL corners are done when the four corners touching the up face match the
356
+ // centers. As with the blocks, any fixed slice offset is applied by the caller
357
+ // via pre-rotated snapshots, so this is a plain match against the centers.
358
+ function cmllCornersDone(st, geo, upFace) {
359
+ const centers = centersOf(st, geo);
360
+ return geo
361
+ .cornersByFace(upFace)
362
+ .every((c) => slotCorrect(st, centers, c.indices, geo.per));
378
363
  }
379
364
 
380
365
  // Index of the move that COMPLETES a stage: the first move after which the
@@ -389,6 +374,19 @@ function completionIndex(bools) {
389
374
  return null;
390
375
  }
391
376
 
377
+ // Index of the FIRST move strictly after `after` at which the condition holds.
378
+ // Used for a stage whose completion is "the first time X happens once the
379
+ // previous stage is done" -- e.g. the last-layer corners getting solved after
380
+ // the second block is built, even if the blocks are momentarily disturbed at
381
+ // that instant. Returns null if `after` is null or the condition never holds.
382
+ function firstIndexAfter(bools, after) {
383
+ if (after == null) return null;
384
+ for (let i = after + 1; i < bools.length; i++) {
385
+ if (bools[i]) return i;
386
+ }
387
+ return null;
388
+ }
389
+
392
390
  // Builds the milestone indices for one assumed cross color. Detection is
393
391
  // cumulative: an F2L pair only counts while the cross is solved, and OLL only
394
392
  // counts once the full F2L is solved. This rejects transient false positives.
@@ -444,57 +442,65 @@ function isCFOP(build) {
444
442
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
445
443
  }
446
444
 
447
- // Builds the Roux milestone indices for one (sideFace, upFace) orientation.
448
- // Detection is cumulative, mirroring buildForCross: the second block only counts
449
- // while the first block holds, CMLL only once both blocks hold, and LSE is the
450
- // fully solved cube. The first block is whichever of the two opposite side faces
451
- // finishes its block earliest; the other side is the second block.
452
- function buildForRoux(snapshots, geo, sideA, upFace, slicePerm) {
453
- snapshots.length;
445
+ // Builds the Roux milestone indices for one (sideFace, upFace) orientation,
446
+ // using snapshots already pre-rotated by this orientation's fixed slice offset.
447
+ //
448
+ // Milestones are taken at FIRST achievement, not while a condition holds
449
+ // continuously: once a stage is reached the solver moves on, and on face-move
450
+ // Roux later stages routinely break and rebuild earlier ones (a CMLL alg breaks
451
+ // a block; a face-move LSE breaks everything). So:
452
+ // 1st block = first instant either side's block is built;
453
+ // 2nd block = first instant the OTHER side's block is built afterwards -- NOT
454
+ // gated on the first block still being intact, which it often is
455
+ // not while the second is being inserted;
456
+ // CMLL = first instant the last-layer corners are solved afterwards;
457
+ // LSE = the fully solved cube (`lseIdx`, computed once by the caller and
458
+ // passed in, since "solved" is the same in every orientation).
459
+ function buildForRoux(snapshots, geo, sideA, upFace, lseIdx) {
454
460
  const sideB = geo.opposite[sideA];
455
461
 
456
- const aDone = snapshots.map((st) =>
457
- rouxBlockDone(st, geo, sideA, upFace, slicePerm)
458
- );
459
- const bDone = snapshots.map((st) =>
460
- rouxBlockDone(st, geo, sideB, upFace, slicePerm)
461
- );
462
- const aIdx = completionIndex(aDone);
463
- const bIdx = completionIndex(bDone);
464
-
465
- // First block = the side that completes earliest; second block = the other.
466
- let firstSide = sideA;
467
- let secondSide = sideB;
468
- let fbBools = aDone;
469
- let sbBools = bDone;
470
- if ((bIdx ?? Infinity) < (aIdx ?? Infinity)) {
471
- firstSide = sideB;
472
- secondSide = sideA;
473
- fbBools = bDone;
474
- sbBools = aDone;
475
- }
462
+ const aDone = snapshots.map((st) => rouxBlockDone(st, geo, sideA, upFace));
463
+ const bDone = snapshots.map((st) => rouxBlockDone(st, geo, sideB, upFace));
464
+ const aIdx = firstIndexAfter(aDone, -1);
465
+ const bIdx = firstIndexAfter(bDone, -1);
466
+ if (aIdx == null || bIdx == null) return null;
476
467
 
477
- const fbIdx = completionIndex(fbBools);
478
- // Second block gated by the first block holding at the same instant.
479
- const secondBlockBools = snapshots.map((_, i) => fbBools[i] && sbBools[i]);
480
- const sbIdx = completionIndex(secondBlockBools);
468
+ // First block = the side built earliest; the other side is the second block.
469
+ const aFirst = aIdx <= bIdx;
470
+ const firstSide = aFirst ? sideA : sideB;
471
+ const fbIdx = aFirst ? aIdx : bIdx;
472
+ const sbIdx = firstIndexAfter(aFirst ? bDone : aDone, fbIdx);
473
+ if (sbIdx == null) return null;
481
474
 
482
- const cmllIdx = completionIndex(
483
- snapshots.map(
484
- (st, i) => secondBlockBools[i] && cmllDone(st, geo, upFace, slicePerm)
485
- )
486
- );
487
- const lseIdx = completionIndex(snapshots.map((st) => isSolvedFlat(st, geo.per)));
475
+ const cornerBools = snapshots.map((st) => cmllCornersDone(st, geo, upFace));
476
+ const cmllIdx = firstIndexAfter(cornerBools, sbIdx);
477
+ if (cmllIdx == null) return null;
488
478
 
489
- return { firstSide, secondSide, upFace, fbIdx, sbIdx, cmllIdx, lseIdx };
479
+ return {
480
+ firstSide,
481
+ secondSide: aFirst ? sideB : sideA,
482
+ upFace,
483
+ fbIdx,
484
+ sbIdx,
485
+ cmllIdx,
486
+ lseIdx,
487
+ };
490
488
  }
491
489
 
492
490
  // Does this breakdown follow the Roux order: 1st block -> 2nd block -> CMLL -> LSE?
491
+ // Shortest plausible length (in moves) of a genuine Roux stage. The block and
492
+ // corner conditions flicker true at scattered transient instants throughout a
493
+ // solve; a "stage" only a move or two long is one of those coincidences, not a
494
+ // real second block or CMLL. Requiring a minimum span rejects them so a spurious
495
+ // orientation cannot masquerade as a clean Roux staging.
496
+ const ROUX_MIN_STAGE = 5;
497
+
493
498
  function isRoux(build) {
494
499
  const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
495
500
  if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
496
501
  return false;
497
- return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
502
+ if (!(fbIdx < sbIdx && sbIdx < cmllIdx && cmllIdx < lseIdx)) return false;
503
+ return sbIdx - fbIdx >= ROUX_MIN_STAGE && cmllIdx - sbIdx >= ROUX_MIN_STAGE;
498
504
  }
499
505
 
500
506
  /**
@@ -623,27 +629,60 @@ function analyzeSolution(moves, options = {}) {
623
629
  const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
624
630
 
625
631
  // Stage the solve as Roux (1st block -> 2nd block -> CMLL -> LSE) on every
626
- // orientation. Each candidate fixes a side face and a perpendicular up face;
627
- // buildForRoux assigns first/second block by which side finishes earliest.
628
- // The "floating" slice that block/CMLL detection lets drift is the one
629
- // perpendicular to the block axis: M for L/R blocks, E for U/D, S for F/B.
630
- // Pick the valid candidate whose first block completes earliest.
632
+ // orientation. A candidate fixes a side face, a perpendicular up face, and a
633
+ // slice offset `d`: the blocks can be assembled with the slice between them
634
+ // turned away from the centers, so each orientation is tested against
635
+ // snapshots pre-rotated by d quarter-turns of its axis slice (M for L/R blocks,
636
+ // E for U/D, S for F/B). Holding that offset fixed for the whole solve (rather
637
+ // than re-matching it move-by-move) is what stops a block reading as "built"
638
+ // on a coincidental alignment.
639
+ //
640
+ // Many orientations still yield a technically ordered staging. The one that
641
+ // reflects the real solve is the one whose three build milestones land
642
+ // EARLIEST in aggregate (smallest fb + sb + cmll): a false orientation only
643
+ // assembles its blocks and corners by coincidence, which does not happen
644
+ // earlier than the genuine build. (isRoux discards orientations whose stages
645
+ // are too short to be anything but a coincidence.) "Solved" is orientation-
646
+ // independent, so the LSE index is computed once and shared.
631
647
  const perms = getMovePermutations(size);
632
- const axisSlicePerm = (face) => {
648
+ const sliceCwOf = (face) => {
633
649
  if (face === 1 || face === 3) return perms["M"].cw; // L/R axis
634
650
  if (face === 0 || face === 5) return perms["E"].cw; // U/D axis
635
651
  return perms["S"].cw; // F/B axis
636
652
  };
653
+ const lseIdx = completionIndex(
654
+ snapshots.map((st) => isSolvedFlat(st, geo.per))
655
+ );
656
+ // Snapshots rotated by d quarter-turns of a given slice, built incrementally
657
+ // and cached so each (axis, d) is computed once and reused across its up faces.
658
+ const rotCache = new Map();
659
+ const rotatedSnaps = (sliceCw, d) => {
660
+ const key = `${sliceCw}:${d}`;
661
+ if (rotCache.has(key)) return rotCache.get(key);
662
+ const snaps =
663
+ d === 0
664
+ ? snapshots
665
+ : rotatedSnaps(sliceCw, d - 1).map((st) => applyPerm(st, sliceCw));
666
+ rotCache.set(key, snaps);
667
+ return snaps;
668
+ };
637
669
  const rouxCandidates = [];
638
670
  for (let s = 0; s < 6; s++) {
671
+ const sliceCw = sliceCwOf(s);
639
672
  for (const u of geo.neighbors[s]) {
640
- rouxCandidates.push(buildForRoux(snapshots, geo, s, u, axisSlicePerm(s)));
673
+ for (let d = 0; d < 4; d++) {
674
+ const build = buildForRoux(rotatedSnaps(sliceCw, d), geo, s, u, lseIdx);
675
+ if (build) rouxCandidates.push(build);
676
+ }
641
677
  }
642
678
  }
643
679
  const rouxBuild =
644
680
  rouxCandidates
645
681
  .filter((b) => isRoux(b))
646
- .sort((a, b) => a.fbIdx - b.fbIdx)[0] ?? null;
682
+ .sort(
683
+ (a, b) =>
684
+ a.fbIdx + a.sbIdx + a.cmllIdx - (b.fbIdx + b.sbIdx + b.cmllIdx)
685
+ )[0] ?? null;
647
686
 
648
687
  // A solved cube satisfies many orderings, so both stagings can be technically
649
688
  // valid. Disambiguate by which method's FIRST milestone is genuinely reached
package/dist/index.js CHANGED
@@ -276,33 +276,20 @@ function rouxBlockPieces(geo, sideFace, upFace) {
276
276
  const corners = geo.cornersByFace(sideFace).filter((c) => c.faces.includes(downFace));
277
277
  return { edges, corners };
278
278
  }
279
- function rouxBlockDone(st, geo, sideFace, upFace, slicePerm) {
279
+ function rouxBlockDone(st, geo, sideFace, upFace) {
280
280
  const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
281
281
  if (edges.length !== 3 || corners.length !== 2) return false;
282
- let cur = st;
283
- for (let m = 0; m < 4; m++) {
284
- if (m > 0) cur = applyPerm(cur, slicePerm);
285
- const centers = centersOf(cur, geo);
286
- const ok = edges.every((e) => slotCorrect(cur, centers, e.indices, geo.per)) && corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per));
287
- if (ok) return true;
288
- }
289
- return false;
282
+ const centers = centersOf(st, geo);
283
+ return edges.every((e) => slotCorrect(st, centers, e.indices, geo.per)) && corners.every((c) => slotCorrect(st, centers, c.indices, geo.per));
290
284
  }
291
285
  function applyPerm(st, perm) {
292
286
  const out = new Array(st.length);
293
287
  for (let i = 0; i < st.length; i++) out[i] = st[perm[i]];
294
288
  return out;
295
289
  }
296
- function cmllDone(st, geo, upFace, slicePerm) {
297
- let cur = st;
298
- const corners = geo.cornersByFace(upFace);
299
- for (let m = 0; m < 4; m++) {
300
- if (m > 0) cur = applyPerm(cur, slicePerm);
301
- const centers = centersOf(cur, geo);
302
- if (corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per)))
303
- return true;
304
- }
305
- return false;
290
+ function cmllCornersDone(st, geo, upFace) {
291
+ const centers = centersOf(st, geo);
292
+ return geo.cornersByFace(upFace).every((c) => slotCorrect(st, centers, c.indices, geo.per));
306
293
  }
307
294
  function completionIndex(bools) {
308
295
  const n = bools.length;
@@ -310,6 +297,13 @@ function completionIndex(bools) {
310
297
  for (let i = 0; i < n; i++) if (bools[i]) return i;
311
298
  return null;
312
299
  }
300
+ function firstIndexAfter(bools, after) {
301
+ if (after == null) return null;
302
+ for (let i = after + 1; i < bools.length; i++) {
303
+ if (bools[i]) return i;
304
+ }
305
+ return null;
306
+ }
313
307
  function buildForCross(snapshots, geo, color) {
314
308
  const n = snapshots.length;
315
309
  const crossBools = snapshots.map((st) => crossDone(st, geo, color));
@@ -349,43 +343,38 @@ function isCFOP(build) {
349
343
  const lastF2L = f2lSlots[3].idx;
350
344
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
351
345
  }
352
- function buildForRoux(snapshots, geo, sideA, upFace, slicePerm) {
353
- const n = snapshots.length;
346
+ function buildForRoux(snapshots, geo, sideA, upFace, lseIdx) {
354
347
  const sideB = geo.opposite[sideA];
355
- const aDone = snapshots.map(
356
- (st) => rouxBlockDone(st, geo, sideA, upFace, slicePerm)
357
- );
358
- const bDone = snapshots.map(
359
- (st) => rouxBlockDone(st, geo, sideB, upFace, slicePerm)
360
- );
361
- const aIdx = completionIndex(aDone);
362
- const bIdx = completionIndex(bDone);
363
- let firstSide = sideA;
364
- let secondSide = sideB;
365
- let fbBools = aDone;
366
- let sbBools = bDone;
367
- if ((bIdx != null ? bIdx : Infinity) < (aIdx != null ? aIdx : Infinity)) {
368
- firstSide = sideB;
369
- secondSide = sideA;
370
- fbBools = bDone;
371
- sbBools = aDone;
372
- }
373
- const fbIdx = completionIndex(fbBools);
374
- const secondBlockBools = snapshots.map((_, i) => fbBools[i] && sbBools[i]);
375
- const sbIdx = completionIndex(secondBlockBools);
376
- const cmllIdx = completionIndex(
377
- snapshots.map(
378
- (st, i) => secondBlockBools[i] && cmllDone(st, geo, upFace, slicePerm)
379
- )
380
- );
381
- const lseIdx = completionIndex(snapshots.map((st) => isSolvedFlat(st, geo.per)));
382
- return { firstSide, secondSide, upFace, fbIdx, sbIdx, cmllIdx, lseIdx };
348
+ const aDone = snapshots.map((st) => rouxBlockDone(st, geo, sideA, upFace));
349
+ const bDone = snapshots.map((st) => rouxBlockDone(st, geo, sideB, upFace));
350
+ const aIdx = firstIndexAfter(aDone, -1);
351
+ const bIdx = firstIndexAfter(bDone, -1);
352
+ if (aIdx == null || bIdx == null) return null;
353
+ const aFirst = aIdx <= bIdx;
354
+ const firstSide = aFirst ? sideA : sideB;
355
+ const fbIdx = aFirst ? aIdx : bIdx;
356
+ const sbIdx = firstIndexAfter(aFirst ? bDone : aDone, fbIdx);
357
+ if (sbIdx == null) return null;
358
+ const cornerBools = snapshots.map((st) => cmllCornersDone(st, geo, upFace));
359
+ const cmllIdx = firstIndexAfter(cornerBools, sbIdx);
360
+ if (cmllIdx == null) return null;
361
+ return {
362
+ firstSide,
363
+ secondSide: aFirst ? sideB : sideA,
364
+ upFace,
365
+ fbIdx,
366
+ sbIdx,
367
+ cmllIdx,
368
+ lseIdx
369
+ };
383
370
  }
371
+ var ROUX_MIN_STAGE = 5;
384
372
  function isRoux(build) {
385
373
  const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
386
374
  if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
387
375
  return false;
388
- return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
376
+ if (!(fbIdx < sbIdx && sbIdx < cmllIdx && cmllIdx < lseIdx)) return false;
377
+ return sbIdx - fbIdx >= ROUX_MIN_STAGE && cmllIdx - sbIdx >= ROUX_MIN_STAGE;
389
378
  }
390
379
  function analyzeSolution(moves, options = {}) {
391
380
  var _a, _b;
@@ -472,18 +461,35 @@ function analyzeSolution(moves, options = {}) {
472
461
  const cfopChosen = (_a = ordered.find((c) => isCFOP(c.build))) != null ? _a : ordered[0];
473
462
  const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
474
463
  const perms = getMovePermutations(size);
475
- const axisSlicePerm = (face) => {
464
+ const sliceCwOf = (face) => {
476
465
  if (face === 1 || face === 3) return perms["M"].cw;
477
466
  if (face === 0 || face === 5) return perms["E"].cw;
478
467
  return perms["S"].cw;
479
468
  };
469
+ const lseIdx = completionIndex(
470
+ snapshots.map((st) => isSolvedFlat(st, geo.per))
471
+ );
472
+ const rotCache = /* @__PURE__ */ new Map();
473
+ const rotatedSnaps = (sliceCw, d) => {
474
+ const key = `${sliceCw}:${d}`;
475
+ if (rotCache.has(key)) return rotCache.get(key);
476
+ const snaps = d === 0 ? snapshots : rotatedSnaps(sliceCw, d - 1).map((st) => applyPerm(st, sliceCw));
477
+ rotCache.set(key, snaps);
478
+ return snaps;
479
+ };
480
480
  const rouxCandidates = [];
481
481
  for (let s = 0; s < 6; s++) {
482
+ const sliceCw = sliceCwOf(s);
482
483
  for (const u of geo.neighbors[s]) {
483
- rouxCandidates.push(buildForRoux(snapshots, geo, s, u, axisSlicePerm(s)));
484
+ for (let d = 0; d < 4; d++) {
485
+ const build2 = buildForRoux(rotatedSnaps(sliceCw, d), geo, s, u, lseIdx);
486
+ if (build2) rouxCandidates.push(build2);
487
+ }
484
488
  }
485
489
  }
486
- const rouxBuild = (_b = rouxCandidates.filter((b) => isRoux(b)).sort((a, b) => a.fbIdx - b.fbIdx)[0]) != null ? _b : null;
490
+ const rouxBuild = (_b = rouxCandidates.filter((b) => isRoux(b)).sort(
491
+ (a, b) => a.fbIdx + a.sbIdx + a.cmllIdx - (b.fbIdx + b.sbIdx + b.cmllIdx)
492
+ )[0]) != null ? _b : null;
487
493
  let method = "unknown";
488
494
  if (cfopValid && rouxBuild) {
489
495
  method = cfopChosen.build.crossIdx <= rouxBuild.fbIdx ? "CFOP" : "Roux";
package/dist/index.mjs CHANGED
@@ -248,33 +248,20 @@ function rouxBlockPieces(geo, sideFace, upFace) {
248
248
  const corners = geo.cornersByFace(sideFace).filter((c) => c.faces.includes(downFace));
249
249
  return { edges, corners };
250
250
  }
251
- function rouxBlockDone(st, geo, sideFace, upFace, slicePerm) {
251
+ function rouxBlockDone(st, geo, sideFace, upFace) {
252
252
  const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
253
253
  if (edges.length !== 3 || corners.length !== 2) return false;
254
- let cur = st;
255
- for (let m = 0; m < 4; m++) {
256
- if (m > 0) cur = applyPerm(cur, slicePerm);
257
- const centers = centersOf(cur, geo);
258
- const ok = edges.every((e) => slotCorrect(cur, centers, e.indices, geo.per)) && corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per));
259
- if (ok) return true;
260
- }
261
- return false;
254
+ const centers = centersOf(st, geo);
255
+ return edges.every((e) => slotCorrect(st, centers, e.indices, geo.per)) && corners.every((c) => slotCorrect(st, centers, c.indices, geo.per));
262
256
  }
263
257
  function applyPerm(st, perm) {
264
258
  const out = new Array(st.length);
265
259
  for (let i = 0; i < st.length; i++) out[i] = st[perm[i]];
266
260
  return out;
267
261
  }
268
- function cmllDone(st, geo, upFace, slicePerm) {
269
- let cur = st;
270
- const corners = geo.cornersByFace(upFace);
271
- for (let m = 0; m < 4; m++) {
272
- if (m > 0) cur = applyPerm(cur, slicePerm);
273
- const centers = centersOf(cur, geo);
274
- if (corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per)))
275
- return true;
276
- }
277
- return false;
262
+ function cmllCornersDone(st, geo, upFace) {
263
+ const centers = centersOf(st, geo);
264
+ return geo.cornersByFace(upFace).every((c) => slotCorrect(st, centers, c.indices, geo.per));
278
265
  }
279
266
  function completionIndex(bools) {
280
267
  const n = bools.length;
@@ -282,6 +269,13 @@ function completionIndex(bools) {
282
269
  for (let i = 0; i < n; i++) if (bools[i]) return i;
283
270
  return null;
284
271
  }
272
+ function firstIndexAfter(bools, after) {
273
+ if (after == null) return null;
274
+ for (let i = after + 1; i < bools.length; i++) {
275
+ if (bools[i]) return i;
276
+ }
277
+ return null;
278
+ }
285
279
  function buildForCross(snapshots, geo, color) {
286
280
  const n = snapshots.length;
287
281
  const crossBools = snapshots.map((st) => crossDone(st, geo, color));
@@ -321,43 +315,38 @@ function isCFOP(build) {
321
315
  const lastF2L = f2lSlots[3].idx;
322
316
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
323
317
  }
324
- function buildForRoux(snapshots, geo, sideA, upFace, slicePerm) {
325
- const n = snapshots.length;
318
+ function buildForRoux(snapshots, geo, sideA, upFace, lseIdx) {
326
319
  const sideB = geo.opposite[sideA];
327
- const aDone = snapshots.map(
328
- (st) => rouxBlockDone(st, geo, sideA, upFace, slicePerm)
329
- );
330
- const bDone = snapshots.map(
331
- (st) => rouxBlockDone(st, geo, sideB, upFace, slicePerm)
332
- );
333
- const aIdx = completionIndex(aDone);
334
- const bIdx = completionIndex(bDone);
335
- let firstSide = sideA;
336
- let secondSide = sideB;
337
- let fbBools = aDone;
338
- let sbBools = bDone;
339
- if ((bIdx != null ? bIdx : Infinity) < (aIdx != null ? aIdx : Infinity)) {
340
- firstSide = sideB;
341
- secondSide = sideA;
342
- fbBools = bDone;
343
- sbBools = aDone;
344
- }
345
- const fbIdx = completionIndex(fbBools);
346
- const secondBlockBools = snapshots.map((_, i) => fbBools[i] && sbBools[i]);
347
- const sbIdx = completionIndex(secondBlockBools);
348
- const cmllIdx = completionIndex(
349
- snapshots.map(
350
- (st, i) => secondBlockBools[i] && cmllDone(st, geo, upFace, slicePerm)
351
- )
352
- );
353
- const lseIdx = completionIndex(snapshots.map((st) => isSolvedFlat(st, geo.per)));
354
- return { firstSide, secondSide, upFace, fbIdx, sbIdx, cmllIdx, lseIdx };
320
+ const aDone = snapshots.map((st) => rouxBlockDone(st, geo, sideA, upFace));
321
+ const bDone = snapshots.map((st) => rouxBlockDone(st, geo, sideB, upFace));
322
+ const aIdx = firstIndexAfter(aDone, -1);
323
+ const bIdx = firstIndexAfter(bDone, -1);
324
+ if (aIdx == null || bIdx == null) return null;
325
+ const aFirst = aIdx <= bIdx;
326
+ const firstSide = aFirst ? sideA : sideB;
327
+ const fbIdx = aFirst ? aIdx : bIdx;
328
+ const sbIdx = firstIndexAfter(aFirst ? bDone : aDone, fbIdx);
329
+ if (sbIdx == null) return null;
330
+ const cornerBools = snapshots.map((st) => cmllCornersDone(st, geo, upFace));
331
+ const cmllIdx = firstIndexAfter(cornerBools, sbIdx);
332
+ if (cmllIdx == null) return null;
333
+ return {
334
+ firstSide,
335
+ secondSide: aFirst ? sideB : sideA,
336
+ upFace,
337
+ fbIdx,
338
+ sbIdx,
339
+ cmllIdx,
340
+ lseIdx
341
+ };
355
342
  }
343
+ var ROUX_MIN_STAGE = 5;
356
344
  function isRoux(build) {
357
345
  const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
358
346
  if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
359
347
  return false;
360
- return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
348
+ if (!(fbIdx < sbIdx && sbIdx < cmllIdx && cmllIdx < lseIdx)) return false;
349
+ return sbIdx - fbIdx >= ROUX_MIN_STAGE && cmllIdx - sbIdx >= ROUX_MIN_STAGE;
361
350
  }
362
351
  function analyzeSolution(moves, options = {}) {
363
352
  var _a, _b;
@@ -444,18 +433,35 @@ function analyzeSolution(moves, options = {}) {
444
433
  const cfopChosen = (_a = ordered.find((c) => isCFOP(c.build))) != null ? _a : ordered[0];
445
434
  const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
446
435
  const perms = getMovePermutations(size);
447
- const axisSlicePerm = (face) => {
436
+ const sliceCwOf = (face) => {
448
437
  if (face === 1 || face === 3) return perms["M"].cw;
449
438
  if (face === 0 || face === 5) return perms["E"].cw;
450
439
  return perms["S"].cw;
451
440
  };
441
+ const lseIdx = completionIndex(
442
+ snapshots.map((st) => isSolvedFlat(st, geo.per))
443
+ );
444
+ const rotCache = /* @__PURE__ */ new Map();
445
+ const rotatedSnaps = (sliceCw, d) => {
446
+ const key = `${sliceCw}:${d}`;
447
+ if (rotCache.has(key)) return rotCache.get(key);
448
+ const snaps = d === 0 ? snapshots : rotatedSnaps(sliceCw, d - 1).map((st) => applyPerm(st, sliceCw));
449
+ rotCache.set(key, snaps);
450
+ return snaps;
451
+ };
452
452
  const rouxCandidates = [];
453
453
  for (let s = 0; s < 6; s++) {
454
+ const sliceCw = sliceCwOf(s);
454
455
  for (const u of geo.neighbors[s]) {
455
- rouxCandidates.push(buildForRoux(snapshots, geo, s, u, axisSlicePerm(s)));
456
+ for (let d = 0; d < 4; d++) {
457
+ const build2 = buildForRoux(rotatedSnaps(sliceCw, d), geo, s, u, lseIdx);
458
+ if (build2) rouxCandidates.push(build2);
459
+ }
456
460
  }
457
461
  }
458
- const rouxBuild = (_b = rouxCandidates.filter((b) => isRoux(b)).sort((a, b) => a.fbIdx - b.fbIdx)[0]) != null ? _b : null;
462
+ const rouxBuild = (_b = rouxCandidates.filter((b) => isRoux(b)).sort(
463
+ (a, b) => a.fbIdx + a.sbIdx + a.cmllIdx - (b.fbIdx + b.sbIdx + b.cmllIdx)
464
+ )[0]) != null ? _b : null;
459
465
  let method = "unknown";
460
466
  if (cfopValid && rouxBuild) {
461
467
  method = cfopChosen.build.crossIdx <= rouxBuild.fbIdx ? "CFOP" : "Roux";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cube-state-engine",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "An efficient representation in memory for tracking the Rubik's cube state on each movement.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",