cube-state-engine 1.5.1 → 1.7.0

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.
@@ -3,7 +3,12 @@
3
3
  "allow": [
4
4
  "Bash(npm run *)",
5
5
  "Bash(node -e \"const m=require\\('./dist/index.js'\\); console.log\\(Object.keys\\(m\\).filter\\(k=>/analyze|invert|MovePerm/.test\\(k\\)\\)\\)\")",
6
- "Bash(npm test *)"
6
+ "Bash(npm test *)",
7
+ "Read(//mnt/d/workspace/**)",
8
+ "Bash(node -e ' *)",
9
+ "Bash(node *)",
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)"
7
12
  ]
8
13
  }
9
14
  }
package/dist/index.d.mts CHANGED
@@ -309,6 +309,74 @@ function ollDone(st, geo, color) {
309
309
  return true;
310
310
  }
311
311
 
312
+ // --- Roux geometry --------------------------------------------------------
313
+
314
+ // The pieces of a Roux 1x2x3 block, parameterized by the block's side face and
315
+ // the up face (which fixes down = opposite[up]). The block holds the side
316
+ // center, the side's three edges that do NOT touch the up face, and the side's
317
+ // two corners that DO touch the down face. The center always matches itself, so
318
+ // only edges/corners need checking.
319
+ function rouxBlockPieces(geo, sideFace, upFace) {
320
+ const downFace = geo.opposite[upFace];
321
+ const edges = geo
322
+ .edgesByFace(sideFace)
323
+ .filter((e) => !e.faces.includes(upFace));
324
+ const corners = geo
325
+ .cornersByFace(sideFace)
326
+ .filter((c) => c.faces.includes(downFace));
327
+ return { edges, corners };
328
+ }
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) {
340
+ const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
341
+ 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;
352
+ }
353
+
354
+ // Applies a permutation (out[i] = st[perm[i]]) to a flat sticker array.
355
+ function applyPerm(st, perm) {
356
+ const out = new Array(st.length);
357
+ for (let i = 0; i < st.length; i++) out[i] = st[perm[i]];
358
+ return out;
359
+ }
360
+
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;
378
+ }
379
+
312
380
  // Index of the move that COMPLETES a stage: the first move after which the
313
381
  // condition holds, provided the stage is genuinely achieved by the end of the
314
382
  // solve. Later moves are allowed to break it transiently (a turn mid-algorithm
@@ -376,15 +444,71 @@ function isCFOP(build) {
376
444
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
377
445
  }
378
446
 
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;
454
+ const sideB = geo.opposite[sideA];
455
+
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
+ }
476
+
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);
481
+
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)));
488
+
489
+ return { firstSide, secondSide, upFace, fbIdx, sbIdx, cmllIdx, lseIdx };
490
+ }
491
+
492
+ // Does this breakdown follow the Roux order: 1st block -> 2nd block -> CMLL -> LSE?
493
+ function isRoux(build) {
494
+ const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
495
+ if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
496
+ return false;
497
+ return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
498
+ }
499
+
379
500
  /**
380
501
  * Analyzes a solution and returns the timing of each method milestone.
381
502
  *
382
503
  * @param {Array<{m: string, t: number}>} moves - Solution moves with cumulative
383
504
  * timestamps. `m` is a move token; `t` is elapsed ms up to that move.
384
- * @param {{size?: number}} [options] - Cube size (defaults to 3). CFOP staging
505
+ * @param {{size?: number}} [options] - Cube size (defaults to 3). Method staging
385
506
  * is only computed for 3x3; other sizes report the solved (PLL) time only.
386
- * @returns {object} Breakdown with `method`, `total`, `cross`, `f2l[]`, `oll`,
387
- * `pll` and `allCrosses` (cross time per face color).
507
+ * @returns {object} Breakdown with `method` ("CFOP", "Roux" or "unknown"),
508
+ * `total`, `tps` and `allCrosses` (cross time per face color). For CFOP it
509
+ * carries `cross`, `f2l[]`, `oll`, `pll`; for Roux it carries `firstBlock`,
510
+ * `secondBlock`, `cmll`, `lse` (the other method's fields are null). Each
511
+ * block record also includes the `side` center color it was built on.
388
512
  */
389
513
  function analyzeSolution(moves, options = {}) {
390
514
  const size = options.size === 2 ? 2 : 3;
@@ -420,6 +544,10 @@ function analyzeSolution(moves, options = {}) {
420
544
  f2l: [],
421
545
  oll: null,
422
546
  pll: null,
547
+ firstBlock: null,
548
+ secondBlock: null,
549
+ cmll: null,
550
+ lse: null,
423
551
  allCrosses: {},
424
552
  unsupported,
425
553
  };
@@ -491,11 +619,87 @@ function analyzeSolution(moves, options = {}) {
491
619
  return ai - bi;
492
620
  });
493
621
 
494
- let chosen = ordered.find((c) => isCFOP(c.build)) ?? ordered[0];
495
- const method = chosen && isCFOP(chosen.build) ? "CFOP" : "unknown";
496
- const { color: crossColor, build } = chosen;
622
+ const cfopChosen = ordered.find((c) => isCFOP(c.build)) ?? ordered[0];
623
+ const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
624
+
625
+ // 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.
631
+ const perms = getMovePermutations(size);
632
+ const axisSlicePerm = (face) => {
633
+ if (face === 1 || face === 3) return perms["M"].cw; // L/R axis
634
+ if (face === 0 || face === 5) return perms["E"].cw; // U/D axis
635
+ return perms["S"].cw; // F/B axis
636
+ };
637
+ const rouxCandidates = [];
638
+ for (let s = 0; s < 6; s++) {
639
+ for (const u of geo.neighbors[s]) {
640
+ rouxCandidates.push(buildForRoux(snapshots, geo, s, u, axisSlicePerm(s)));
641
+ }
642
+ }
643
+ const rouxBuild =
644
+ rouxCandidates
645
+ .filter((b) => isRoux(b))
646
+ .sort((a, b) => a.fbIdx - b.fbIdx)[0] ?? null;
647
+
648
+ // A solved cube satisfies many orderings, so both stagings can be technically
649
+ // valid. Disambiguate by which method's FIRST milestone is genuinely reached
650
+ // early: a real CFOP cross is built up front, whereas on a Roux solve no cross
651
+ // completes until LSE; conversely a full 1x2x3 block only forms mid-CFOP. The
652
+ // structure that actually happened owns the earlier first milestone; ties go
653
+ // to CFOP.
654
+ let method = "unknown";
655
+ if (cfopValid && rouxBuild) {
656
+ method = cfopChosen.build.crossIdx <= rouxBuild.fbIdx ? "CFOP" : "Roux";
657
+ } else if (cfopValid) {
658
+ method = "CFOP";
659
+ } else if (rouxBuild) {
660
+ method = "Roux";
661
+ }
662
+ const total = seq[n - 1].t;
663
+ const base = {
664
+ size,
665
+ method,
666
+ solved,
667
+ total,
668
+ tps: total > 0 ? simplifiedCount / (total / 1000) : 0,
669
+ moves: simplifiedMoves,
670
+ cross: null,
671
+ f2l: [],
672
+ oll: null,
673
+ pll: null,
674
+ firstBlock: null,
675
+ secondBlock: null,
676
+ cmll: null,
677
+ lse: null,
678
+ allCrosses,
679
+ unsupported,
680
+ };
681
+
682
+ // Roux: report 1st block / 2nd block / CMLL / LSE, chaining durations.
683
+ if (method === "Roux") {
684
+ const fbM = milestone(rouxBuild.fbIdx, 0);
685
+ const sbM = milestone(rouxBuild.sbIdx, fbM.at);
686
+ const cmllM = milestone(rouxBuild.cmllIdx, sbM.at);
687
+ const lseM = milestone(rouxBuild.lseIdx, cmllM.at);
688
+ return {
689
+ ...base,
690
+ firstBlock: fbM.record
691
+ ? { side: finalCenters[rouxBuild.firstSide], ...fbM.record }
692
+ : null,
693
+ secondBlock: sbM.record
694
+ ? { side: finalCenters[rouxBuild.secondSide], ...sbM.record }
695
+ : null,
696
+ cmll: cmllM.record,
697
+ lse: lseM.record,
698
+ };
699
+ }
497
700
 
498
- // Assemble timed records in solve order so durations chain correctly.
701
+ // CFOP (or unknown): report cross / F2L / OLL / PLL from the earliest cross.
702
+ const { color: crossColor, build } = cfopChosen;
499
703
  const crossM = milestone(build.crossIdx, 0);
500
704
  const cross = crossM.record
501
705
  ? { color: crossColor, ...crossM.record }
@@ -512,26 +716,15 @@ function analyzeSolution(moves, options = {}) {
512
716
  }
513
717
 
514
718
  const ollM = milestone(build.ollIdx, prevAt);
515
- const oll = ollM.record;
516
719
  prevAt = ollM.at;
517
-
518
720
  const pllM = milestone(build.pllIdx, prevAt);
519
- const pll = pllM.record;
520
721
 
521
- const total = seq[n - 1].t;
522
722
  return {
523
- size,
524
- method,
525
- solved,
526
- total,
527
- tps: total > 0 ? simplifiedCount / (total / 1000) : 0,
528
- moves: simplifiedMoves,
723
+ ...base,
529
724
  cross,
530
725
  f2l,
531
- oll,
532
- pll,
533
- allCrosses,
534
- unsupported,
726
+ oll: ollM.record,
727
+ pll: pllM.record,
535
728
  };
536
729
  }
537
730
 
package/dist/index.d.ts CHANGED
@@ -309,6 +309,74 @@ function ollDone(st, geo, color) {
309
309
  return true;
310
310
  }
311
311
 
312
+ // --- Roux geometry --------------------------------------------------------
313
+
314
+ // The pieces of a Roux 1x2x3 block, parameterized by the block's side face and
315
+ // the up face (which fixes down = opposite[up]). The block holds the side
316
+ // center, the side's three edges that do NOT touch the up face, and the side's
317
+ // two corners that DO touch the down face. The center always matches itself, so
318
+ // only edges/corners need checking.
319
+ function rouxBlockPieces(geo, sideFace, upFace) {
320
+ const downFace = geo.opposite[upFace];
321
+ const edges = geo
322
+ .edgesByFace(sideFace)
323
+ .filter((e) => !e.faces.includes(upFace));
324
+ const corners = geo
325
+ .cornersByFace(sideFace)
326
+ .filter((c) => c.faces.includes(downFace));
327
+ return { edges, corners };
328
+ }
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) {
340
+ const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
341
+ 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;
352
+ }
353
+
354
+ // Applies a permutation (out[i] = st[perm[i]]) to a flat sticker array.
355
+ function applyPerm(st, perm) {
356
+ const out = new Array(st.length);
357
+ for (let i = 0; i < st.length; i++) out[i] = st[perm[i]];
358
+ return out;
359
+ }
360
+
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;
378
+ }
379
+
312
380
  // Index of the move that COMPLETES a stage: the first move after which the
313
381
  // condition holds, provided the stage is genuinely achieved by the end of the
314
382
  // solve. Later moves are allowed to break it transiently (a turn mid-algorithm
@@ -376,15 +444,71 @@ function isCFOP(build) {
376
444
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
377
445
  }
378
446
 
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;
454
+ const sideB = geo.opposite[sideA];
455
+
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
+ }
476
+
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);
481
+
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)));
488
+
489
+ return { firstSide, secondSide, upFace, fbIdx, sbIdx, cmllIdx, lseIdx };
490
+ }
491
+
492
+ // Does this breakdown follow the Roux order: 1st block -> 2nd block -> CMLL -> LSE?
493
+ function isRoux(build) {
494
+ const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
495
+ if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
496
+ return false;
497
+ return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
498
+ }
499
+
379
500
  /**
380
501
  * Analyzes a solution and returns the timing of each method milestone.
381
502
  *
382
503
  * @param {Array<{m: string, t: number}>} moves - Solution moves with cumulative
383
504
  * timestamps. `m` is a move token; `t` is elapsed ms up to that move.
384
- * @param {{size?: number}} [options] - Cube size (defaults to 3). CFOP staging
505
+ * @param {{size?: number}} [options] - Cube size (defaults to 3). Method staging
385
506
  * is only computed for 3x3; other sizes report the solved (PLL) time only.
386
- * @returns {object} Breakdown with `method`, `total`, `cross`, `f2l[]`, `oll`,
387
- * `pll` and `allCrosses` (cross time per face color).
507
+ * @returns {object} Breakdown with `method` ("CFOP", "Roux" or "unknown"),
508
+ * `total`, `tps` and `allCrosses` (cross time per face color). For CFOP it
509
+ * carries `cross`, `f2l[]`, `oll`, `pll`; for Roux it carries `firstBlock`,
510
+ * `secondBlock`, `cmll`, `lse` (the other method's fields are null). Each
511
+ * block record also includes the `side` center color it was built on.
388
512
  */
389
513
  function analyzeSolution(moves, options = {}) {
390
514
  const size = options.size === 2 ? 2 : 3;
@@ -420,6 +544,10 @@ function analyzeSolution(moves, options = {}) {
420
544
  f2l: [],
421
545
  oll: null,
422
546
  pll: null,
547
+ firstBlock: null,
548
+ secondBlock: null,
549
+ cmll: null,
550
+ lse: null,
423
551
  allCrosses: {},
424
552
  unsupported,
425
553
  };
@@ -491,11 +619,87 @@ function analyzeSolution(moves, options = {}) {
491
619
  return ai - bi;
492
620
  });
493
621
 
494
- let chosen = ordered.find((c) => isCFOP(c.build)) ?? ordered[0];
495
- const method = chosen && isCFOP(chosen.build) ? "CFOP" : "unknown";
496
- const { color: crossColor, build } = chosen;
622
+ const cfopChosen = ordered.find((c) => isCFOP(c.build)) ?? ordered[0];
623
+ const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
624
+
625
+ // 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.
631
+ const perms = getMovePermutations(size);
632
+ const axisSlicePerm = (face) => {
633
+ if (face === 1 || face === 3) return perms["M"].cw; // L/R axis
634
+ if (face === 0 || face === 5) return perms["E"].cw; // U/D axis
635
+ return perms["S"].cw; // F/B axis
636
+ };
637
+ const rouxCandidates = [];
638
+ for (let s = 0; s < 6; s++) {
639
+ for (const u of geo.neighbors[s]) {
640
+ rouxCandidates.push(buildForRoux(snapshots, geo, s, u, axisSlicePerm(s)));
641
+ }
642
+ }
643
+ const rouxBuild =
644
+ rouxCandidates
645
+ .filter((b) => isRoux(b))
646
+ .sort((a, b) => a.fbIdx - b.fbIdx)[0] ?? null;
647
+
648
+ // A solved cube satisfies many orderings, so both stagings can be technically
649
+ // valid. Disambiguate by which method's FIRST milestone is genuinely reached
650
+ // early: a real CFOP cross is built up front, whereas on a Roux solve no cross
651
+ // completes until LSE; conversely a full 1x2x3 block only forms mid-CFOP. The
652
+ // structure that actually happened owns the earlier first milestone; ties go
653
+ // to CFOP.
654
+ let method = "unknown";
655
+ if (cfopValid && rouxBuild) {
656
+ method = cfopChosen.build.crossIdx <= rouxBuild.fbIdx ? "CFOP" : "Roux";
657
+ } else if (cfopValid) {
658
+ method = "CFOP";
659
+ } else if (rouxBuild) {
660
+ method = "Roux";
661
+ }
662
+ const total = seq[n - 1].t;
663
+ const base = {
664
+ size,
665
+ method,
666
+ solved,
667
+ total,
668
+ tps: total > 0 ? simplifiedCount / (total / 1000) : 0,
669
+ moves: simplifiedMoves,
670
+ cross: null,
671
+ f2l: [],
672
+ oll: null,
673
+ pll: null,
674
+ firstBlock: null,
675
+ secondBlock: null,
676
+ cmll: null,
677
+ lse: null,
678
+ allCrosses,
679
+ unsupported,
680
+ };
681
+
682
+ // Roux: report 1st block / 2nd block / CMLL / LSE, chaining durations.
683
+ if (method === "Roux") {
684
+ const fbM = milestone(rouxBuild.fbIdx, 0);
685
+ const sbM = milestone(rouxBuild.sbIdx, fbM.at);
686
+ const cmllM = milestone(rouxBuild.cmllIdx, sbM.at);
687
+ const lseM = milestone(rouxBuild.lseIdx, cmllM.at);
688
+ return {
689
+ ...base,
690
+ firstBlock: fbM.record
691
+ ? { side: finalCenters[rouxBuild.firstSide], ...fbM.record }
692
+ : null,
693
+ secondBlock: sbM.record
694
+ ? { side: finalCenters[rouxBuild.secondSide], ...sbM.record }
695
+ : null,
696
+ cmll: cmllM.record,
697
+ lse: lseM.record,
698
+ };
699
+ }
497
700
 
498
- // Assemble timed records in solve order so durations chain correctly.
701
+ // CFOP (or unknown): report cross / F2L / OLL / PLL from the earliest cross.
702
+ const { color: crossColor, build } = cfopChosen;
499
703
  const crossM = milestone(build.crossIdx, 0);
500
704
  const cross = crossM.record
501
705
  ? { color: crossColor, ...crossM.record }
@@ -512,26 +716,15 @@ function analyzeSolution(moves, options = {}) {
512
716
  }
513
717
 
514
718
  const ollM = milestone(build.ollIdx, prevAt);
515
- const oll = ollM.record;
516
719
  prevAt = ollM.at;
517
-
518
720
  const pllM = milestone(build.pllIdx, prevAt);
519
- const pll = pllM.record;
520
721
 
521
- const total = seq[n - 1].t;
522
722
  return {
523
- size,
524
- method,
525
- solved,
526
- total,
527
- tps: total > 0 ? simplifiedCount / (total / 1000) : 0,
528
- moves: simplifiedMoves,
723
+ ...base,
529
724
  cross,
530
725
  f2l,
531
- oll,
532
- pll,
533
- allCrosses,
534
- unsupported,
726
+ oll: ollM.record,
727
+ pll: pllM.record,
535
728
  };
536
729
  }
537
730
 
package/dist/index.js CHANGED
@@ -270,6 +270,40 @@ function ollDone(st, geo, color) {
270
270
  }
271
271
  return true;
272
272
  }
273
+ function rouxBlockPieces(geo, sideFace, upFace) {
274
+ const downFace = geo.opposite[upFace];
275
+ const edges = geo.edgesByFace(sideFace).filter((e) => !e.faces.includes(upFace));
276
+ const corners = geo.cornersByFace(sideFace).filter((c) => c.faces.includes(downFace));
277
+ return { edges, corners };
278
+ }
279
+ function rouxBlockDone(st, geo, sideFace, upFace, slicePerm) {
280
+ const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
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;
290
+ }
291
+ function applyPerm(st, perm) {
292
+ const out = new Array(st.length);
293
+ for (let i = 0; i < st.length; i++) out[i] = st[perm[i]];
294
+ return out;
295
+ }
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;
306
+ }
273
307
  function completionIndex(bools) {
274
308
  const n = bools.length;
275
309
  if (n === 0 || !bools[n - 1]) return null;
@@ -315,15 +349,53 @@ function isCFOP(build) {
315
349
  const lastF2L = f2lSlots[3].idx;
316
350
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
317
351
  }
352
+ function buildForRoux(snapshots, geo, sideA, upFace, slicePerm) {
353
+ const n = snapshots.length;
354
+ 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 };
383
+ }
384
+ function isRoux(build) {
385
+ const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
386
+ if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
387
+ return false;
388
+ return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
389
+ }
318
390
  function analyzeSolution(moves, options = {}) {
319
- var _a;
391
+ var _a, _b;
320
392
  const size = options.size === 2 ? 2 : 3;
321
393
  const unsupported = [];
322
394
  const seq = (Array.isArray(moves) ? moves : []).map((x) => {
323
395
  var _a2;
324
396
  const m = String((_a2 = x == null ? void 0 : x.m) != null ? _a2 : "").trim();
325
- const { token, base } = normalizeToken(m);
326
- const supported = base != null && SUPPORTED_BASES.has(base);
397
+ const { token, base: base2 } = normalizeToken(m);
398
+ const supported = base2 != null && SUPPORTED_BASES.has(base2);
327
399
  if (m.length > 0 && !supported) unsupported.push(m);
328
400
  return { m, mm: supported ? token : "", t: Number(x == null ? void 0 : x.t) };
329
401
  }).filter((x) => x.m.length > 0);
@@ -343,6 +415,10 @@ function analyzeSolution(moves, options = {}) {
343
415
  f2l: [],
344
416
  oll: null,
345
417
  pll: null,
418
+ firstBlock: null,
419
+ secondBlock: null,
420
+ cmll: null,
421
+ lse: null,
346
422
  allCrosses: {},
347
423
  unsupported
348
424
  };
@@ -369,13 +445,13 @@ function analyzeSolution(moves, options = {}) {
369
445
  };
370
446
  };
371
447
  if (size !== 3) {
372
- const pll2 = milestone(pllIdxOnly, 0);
448
+ const pll = milestone(pllIdxOnly, 0);
373
449
  const total2 = seq[n - 1].t;
374
450
  return __spreadProps(__spreadValues({}, empty), {
375
451
  solved,
376
452
  total: total2,
377
453
  tps: total2 > 0 ? simplifiedCount / (total2 / 1e3) : 0,
378
- pll: pll2.record
454
+ pll: pll.record
379
455
  });
380
456
  }
381
457
  const finalCenters = centersOf(snapshots[n - 1], geo);
@@ -388,14 +464,66 @@ function analyzeSolution(moves, options = {}) {
388
464
  allCrosses[color] = idx == null ? null : { at: seq[idx].t, moveIndex: idx, move: seq[idx].m };
389
465
  }
390
466
  const ordered = colors.map((color) => ({ color, build: buildForCross(snapshots, geo, color) })).sort((a, b) => {
391
- var _a2, _b;
467
+ var _a2, _b2;
392
468
  const ai = (_a2 = a.build.crossIdx) != null ? _a2 : Infinity;
393
- const bi = (_b = b.build.crossIdx) != null ? _b : Infinity;
469
+ const bi = (_b2 = b.build.crossIdx) != null ? _b2 : Infinity;
394
470
  return ai - bi;
395
471
  });
396
- let chosen = (_a = ordered.find((c) => isCFOP(c.build))) != null ? _a : ordered[0];
397
- const method = chosen && isCFOP(chosen.build) ? "CFOP" : "unknown";
398
- const { color: crossColor, build } = chosen;
472
+ const cfopChosen = (_a = ordered.find((c) => isCFOP(c.build))) != null ? _a : ordered[0];
473
+ const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
474
+ const perms = getMovePermutations(size);
475
+ const axisSlicePerm = (face) => {
476
+ if (face === 1 || face === 3) return perms["M"].cw;
477
+ if (face === 0 || face === 5) return perms["E"].cw;
478
+ return perms["S"].cw;
479
+ };
480
+ const rouxCandidates = [];
481
+ for (let s = 0; s < 6; s++) {
482
+ for (const u of geo.neighbors[s]) {
483
+ rouxCandidates.push(buildForRoux(snapshots, geo, s, u, axisSlicePerm(s)));
484
+ }
485
+ }
486
+ const rouxBuild = (_b = rouxCandidates.filter((b) => isRoux(b)).sort((a, b) => a.fbIdx - b.fbIdx)[0]) != null ? _b : null;
487
+ let method = "unknown";
488
+ if (cfopValid && rouxBuild) {
489
+ method = cfopChosen.build.crossIdx <= rouxBuild.fbIdx ? "CFOP" : "Roux";
490
+ } else if (cfopValid) {
491
+ method = "CFOP";
492
+ } else if (rouxBuild) {
493
+ method = "Roux";
494
+ }
495
+ const total = seq[n - 1].t;
496
+ const base = {
497
+ size,
498
+ method,
499
+ solved,
500
+ total,
501
+ tps: total > 0 ? simplifiedCount / (total / 1e3) : 0,
502
+ moves: simplifiedMoves,
503
+ cross: null,
504
+ f2l: [],
505
+ oll: null,
506
+ pll: null,
507
+ firstBlock: null,
508
+ secondBlock: null,
509
+ cmll: null,
510
+ lse: null,
511
+ allCrosses,
512
+ unsupported
513
+ };
514
+ if (method === "Roux") {
515
+ const fbM = milestone(rouxBuild.fbIdx, 0);
516
+ const sbM = milestone(rouxBuild.sbIdx, fbM.at);
517
+ const cmllM = milestone(rouxBuild.cmllIdx, sbM.at);
518
+ const lseM = milestone(rouxBuild.lseIdx, cmllM.at);
519
+ return __spreadProps(__spreadValues({}, base), {
520
+ firstBlock: fbM.record ? __spreadValues({ side: finalCenters[rouxBuild.firstSide] }, fbM.record) : null,
521
+ secondBlock: sbM.record ? __spreadValues({ side: finalCenters[rouxBuild.secondSide] }, sbM.record) : null,
522
+ cmll: cmllM.record,
523
+ lse: lseM.record
524
+ });
525
+ }
526
+ const { color: crossColor, build } = cfopChosen;
399
527
  const crossM = milestone(build.crossIdx, 0);
400
528
  const cross = crossM.record ? __spreadValues({ color: crossColor }, crossM.record) : null;
401
529
  let prevAt = crossM.at;
@@ -408,25 +536,14 @@ function analyzeSolution(moves, options = {}) {
408
536
  }
409
537
  }
410
538
  const ollM = milestone(build.ollIdx, prevAt);
411
- const oll = ollM.record;
412
539
  prevAt = ollM.at;
413
540
  const pllM = milestone(build.pllIdx, prevAt);
414
- const pll = pllM.record;
415
- const total = seq[n - 1].t;
416
- return {
417
- size,
418
- method,
419
- solved,
420
- total,
421
- tps: total > 0 ? simplifiedCount / (total / 1e3) : 0,
422
- moves: simplifiedMoves,
541
+ return __spreadProps(__spreadValues({}, base), {
423
542
  cross,
424
543
  f2l,
425
- oll,
426
- pll,
427
- allCrosses,
428
- unsupported
429
- };
544
+ oll: ollM.record,
545
+ pll: pllM.record
546
+ });
430
547
  }
431
548
 
432
549
  // src/index.js
package/dist/index.mjs CHANGED
@@ -242,6 +242,40 @@ function ollDone(st, geo, color) {
242
242
  }
243
243
  return true;
244
244
  }
245
+ function rouxBlockPieces(geo, sideFace, upFace) {
246
+ const downFace = geo.opposite[upFace];
247
+ const edges = geo.edgesByFace(sideFace).filter((e) => !e.faces.includes(upFace));
248
+ const corners = geo.cornersByFace(sideFace).filter((c) => c.faces.includes(downFace));
249
+ return { edges, corners };
250
+ }
251
+ function rouxBlockDone(st, geo, sideFace, upFace, slicePerm) {
252
+ const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
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;
262
+ }
263
+ function applyPerm(st, perm) {
264
+ const out = new Array(st.length);
265
+ for (let i = 0; i < st.length; i++) out[i] = st[perm[i]];
266
+ return out;
267
+ }
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;
278
+ }
245
279
  function completionIndex(bools) {
246
280
  const n = bools.length;
247
281
  if (n === 0 || !bools[n - 1]) return null;
@@ -287,15 +321,53 @@ function isCFOP(build) {
287
321
  const lastF2L = f2lSlots[3].idx;
288
322
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
289
323
  }
324
+ function buildForRoux(snapshots, geo, sideA, upFace, slicePerm) {
325
+ const n = snapshots.length;
326
+ 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 };
355
+ }
356
+ function isRoux(build) {
357
+ const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
358
+ if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
359
+ return false;
360
+ return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
361
+ }
290
362
  function analyzeSolution(moves, options = {}) {
291
- var _a;
363
+ var _a, _b;
292
364
  const size = options.size === 2 ? 2 : 3;
293
365
  const unsupported = [];
294
366
  const seq = (Array.isArray(moves) ? moves : []).map((x) => {
295
367
  var _a2;
296
368
  const m = String((_a2 = x == null ? void 0 : x.m) != null ? _a2 : "").trim();
297
- const { token, base } = normalizeToken(m);
298
- const supported = base != null && SUPPORTED_BASES.has(base);
369
+ const { token, base: base2 } = normalizeToken(m);
370
+ const supported = base2 != null && SUPPORTED_BASES.has(base2);
299
371
  if (m.length > 0 && !supported) unsupported.push(m);
300
372
  return { m, mm: supported ? token : "", t: Number(x == null ? void 0 : x.t) };
301
373
  }).filter((x) => x.m.length > 0);
@@ -315,6 +387,10 @@ function analyzeSolution(moves, options = {}) {
315
387
  f2l: [],
316
388
  oll: null,
317
389
  pll: null,
390
+ firstBlock: null,
391
+ secondBlock: null,
392
+ cmll: null,
393
+ lse: null,
318
394
  allCrosses: {},
319
395
  unsupported
320
396
  };
@@ -341,13 +417,13 @@ function analyzeSolution(moves, options = {}) {
341
417
  };
342
418
  };
343
419
  if (size !== 3) {
344
- const pll2 = milestone(pllIdxOnly, 0);
420
+ const pll = milestone(pllIdxOnly, 0);
345
421
  const total2 = seq[n - 1].t;
346
422
  return __spreadProps(__spreadValues({}, empty), {
347
423
  solved,
348
424
  total: total2,
349
425
  tps: total2 > 0 ? simplifiedCount / (total2 / 1e3) : 0,
350
- pll: pll2.record
426
+ pll: pll.record
351
427
  });
352
428
  }
353
429
  const finalCenters = centersOf(snapshots[n - 1], geo);
@@ -360,14 +436,66 @@ function analyzeSolution(moves, options = {}) {
360
436
  allCrosses[color] = idx == null ? null : { at: seq[idx].t, moveIndex: idx, move: seq[idx].m };
361
437
  }
362
438
  const ordered = colors.map((color) => ({ color, build: buildForCross(snapshots, geo, color) })).sort((a, b) => {
363
- var _a2, _b;
439
+ var _a2, _b2;
364
440
  const ai = (_a2 = a.build.crossIdx) != null ? _a2 : Infinity;
365
- const bi = (_b = b.build.crossIdx) != null ? _b : Infinity;
441
+ const bi = (_b2 = b.build.crossIdx) != null ? _b2 : Infinity;
366
442
  return ai - bi;
367
443
  });
368
- let chosen = (_a = ordered.find((c) => isCFOP(c.build))) != null ? _a : ordered[0];
369
- const method = chosen && isCFOP(chosen.build) ? "CFOP" : "unknown";
370
- const { color: crossColor, build } = chosen;
444
+ const cfopChosen = (_a = ordered.find((c) => isCFOP(c.build))) != null ? _a : ordered[0];
445
+ const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
446
+ const perms = getMovePermutations(size);
447
+ const axisSlicePerm = (face) => {
448
+ if (face === 1 || face === 3) return perms["M"].cw;
449
+ if (face === 0 || face === 5) return perms["E"].cw;
450
+ return perms["S"].cw;
451
+ };
452
+ const rouxCandidates = [];
453
+ for (let s = 0; s < 6; s++) {
454
+ for (const u of geo.neighbors[s]) {
455
+ rouxCandidates.push(buildForRoux(snapshots, geo, s, u, axisSlicePerm(s)));
456
+ }
457
+ }
458
+ const rouxBuild = (_b = rouxCandidates.filter((b) => isRoux(b)).sort((a, b) => a.fbIdx - b.fbIdx)[0]) != null ? _b : null;
459
+ let method = "unknown";
460
+ if (cfopValid && rouxBuild) {
461
+ method = cfopChosen.build.crossIdx <= rouxBuild.fbIdx ? "CFOP" : "Roux";
462
+ } else if (cfopValid) {
463
+ method = "CFOP";
464
+ } else if (rouxBuild) {
465
+ method = "Roux";
466
+ }
467
+ const total = seq[n - 1].t;
468
+ const base = {
469
+ size,
470
+ method,
471
+ solved,
472
+ total,
473
+ tps: total > 0 ? simplifiedCount / (total / 1e3) : 0,
474
+ moves: simplifiedMoves,
475
+ cross: null,
476
+ f2l: [],
477
+ oll: null,
478
+ pll: null,
479
+ firstBlock: null,
480
+ secondBlock: null,
481
+ cmll: null,
482
+ lse: null,
483
+ allCrosses,
484
+ unsupported
485
+ };
486
+ if (method === "Roux") {
487
+ const fbM = milestone(rouxBuild.fbIdx, 0);
488
+ const sbM = milestone(rouxBuild.sbIdx, fbM.at);
489
+ const cmllM = milestone(rouxBuild.cmllIdx, sbM.at);
490
+ const lseM = milestone(rouxBuild.lseIdx, cmllM.at);
491
+ return __spreadProps(__spreadValues({}, base), {
492
+ firstBlock: fbM.record ? __spreadValues({ side: finalCenters[rouxBuild.firstSide] }, fbM.record) : null,
493
+ secondBlock: sbM.record ? __spreadValues({ side: finalCenters[rouxBuild.secondSide] }, sbM.record) : null,
494
+ cmll: cmllM.record,
495
+ lse: lseM.record
496
+ });
497
+ }
498
+ const { color: crossColor, build } = cfopChosen;
371
499
  const crossM = milestone(build.crossIdx, 0);
372
500
  const cross = crossM.record ? __spreadValues({ color: crossColor }, crossM.record) : null;
373
501
  let prevAt = crossM.at;
@@ -380,25 +508,14 @@ function analyzeSolution(moves, options = {}) {
380
508
  }
381
509
  }
382
510
  const ollM = milestone(build.ollIdx, prevAt);
383
- const oll = ollM.record;
384
511
  prevAt = ollM.at;
385
512
  const pllM = milestone(build.pllIdx, prevAt);
386
- const pll = pllM.record;
387
- const total = seq[n - 1].t;
388
- return {
389
- size,
390
- method,
391
- solved,
392
- total,
393
- tps: total > 0 ? simplifiedCount / (total / 1e3) : 0,
394
- moves: simplifiedMoves,
513
+ return __spreadProps(__spreadValues({}, base), {
395
514
  cross,
396
515
  f2l,
397
- oll,
398
- pll,
399
- allCrosses,
400
- unsupported
401
- };
516
+ oll: ollM.record,
517
+ pll: pllM.record
518
+ });
402
519
  }
403
520
 
404
521
  // src/index.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cube-state-engine",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
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",