cube-state-engine 1.5.1 → 1.6.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.
package/dist/index.d.mts CHANGED
@@ -309,6 +309,59 @@ 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
+ function rouxBlockDone(st, centers, geo, sideFace, upFace) {
332
+ const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
333
+ if (edges.length !== 3 || corners.length !== 2) return false;
334
+ for (const e of edges)
335
+ if (!slotCorrect(st, centers, e.indices, geo.per)) return false;
336
+ for (const c of corners)
337
+ if (!slotCorrect(st, centers, c.indices, geo.per)) return false;
338
+ return true;
339
+ }
340
+
341
+ // Applies a permutation (out[i] = st[perm[i]]) to a flat sticker array.
342
+ function applyPerm(st, perm) {
343
+ const out = new Array(st.length);
344
+ for (let i = 0; i < st.length; i++) out[i] = st[perm[i]];
345
+ return out;
346
+ }
347
+
348
+ // CMLL is done when the four corners touching the up face are solved, allowing
349
+ // the M slice to be unaligned: we accept the state if ANY of the four M-slice
350
+ // rotations makes those corners correct. M moves neither the corners nor the
351
+ // L/R blocks, only the U/F/D/B centers (and M-slice edges), so this captures
352
+ // exactly "last-layer corners solved, M not necessarily aligned yet".
353
+ function cmllDone(st, geo, upFace, mPerm) {
354
+ let cur = st;
355
+ const corners = geo.cornersByFace(upFace);
356
+ for (let m = 0; m < 4; m++) {
357
+ if (m > 0) cur = applyPerm(cur, mPerm);
358
+ const centers = centersOf(cur, geo);
359
+ if (corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per)))
360
+ return true;
361
+ }
362
+ return false;
363
+ }
364
+
312
365
  // Index of the move that COMPLETES a stage: the first move after which the
313
366
  // condition holds, provided the stage is genuinely achieved by the end of the
314
367
  // solve. Later moves are allowed to break it transiently (a turn mid-algorithm
@@ -376,15 +429,72 @@ function isCFOP(build) {
376
429
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
377
430
  }
378
431
 
432
+ // Builds the Roux milestone indices for one (sideFace, upFace) orientation.
433
+ // Detection is cumulative, mirroring buildForCross: the second block only counts
434
+ // while the first block holds, CMLL only once both blocks hold, and LSE is the
435
+ // fully solved cube. The first block is whichever of the two opposite side faces
436
+ // finishes its block earliest; the other side is the second block.
437
+ function buildForRoux(snapshots, geo, sideA, upFace, mPerm) {
438
+ snapshots.length;
439
+ const sideB = geo.opposite[sideA];
440
+
441
+ const centersAt = snapshots.map((st) => centersOf(st, geo));
442
+ const aDone = snapshots.map((st, i) =>
443
+ rouxBlockDone(st, centersAt[i], geo, sideA, upFace)
444
+ );
445
+ const bDone = snapshots.map((st, i) =>
446
+ rouxBlockDone(st, centersAt[i], geo, sideB, upFace)
447
+ );
448
+ const aIdx = completionIndex(aDone);
449
+ const bIdx = completionIndex(bDone);
450
+
451
+ // First block = the side that completes earliest; second block = the other.
452
+ let firstSide = sideA;
453
+ let secondSide = sideB;
454
+ let fbBools = aDone;
455
+ let sbBools = bDone;
456
+ if ((bIdx ?? Infinity) < (aIdx ?? Infinity)) {
457
+ firstSide = sideB;
458
+ secondSide = sideA;
459
+ fbBools = bDone;
460
+ sbBools = aDone;
461
+ }
462
+
463
+ const fbIdx = completionIndex(fbBools);
464
+ // Second block gated by the first block holding at the same instant.
465
+ const secondBlockBools = snapshots.map((_, i) => fbBools[i] && sbBools[i]);
466
+ const sbIdx = completionIndex(secondBlockBools);
467
+
468
+ const cmllIdx = completionIndex(
469
+ snapshots.map(
470
+ (st, i) => secondBlockBools[i] && cmllDone(st, geo, upFace, mPerm)
471
+ )
472
+ );
473
+ const lseIdx = completionIndex(snapshots.map((st) => isSolvedFlat(st, geo.per)));
474
+
475
+ return { firstSide, secondSide, upFace, fbIdx, sbIdx, cmllIdx, lseIdx };
476
+ }
477
+
478
+ // Does this breakdown follow the Roux order: 1st block -> 2nd block -> CMLL -> LSE?
479
+ function isRoux(build) {
480
+ const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
481
+ if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
482
+ return false;
483
+ return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
484
+ }
485
+
379
486
  /**
380
487
  * Analyzes a solution and returns the timing of each method milestone.
381
488
  *
382
489
  * @param {Array<{m: string, t: number}>} moves - Solution moves with cumulative
383
490
  * 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
491
+ * @param {{size?: number}} [options] - Cube size (defaults to 3). Method staging
385
492
  * 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).
493
+ * @returns {object} Breakdown with `method` ("CFOP", "Roux" or "unknown"),
494
+ * `total`, `tps` and `allCrosses` (cross time per face color). For CFOP it
495
+ * carries `cross`, `f2l[]`, `oll`, `pll`; for Roux it carries `firstBlock`,
496
+ * `secondBlock`, `cmll`, `lse` (the other method's fields are null). Each
497
+ * block record also includes the `side` center color it was built on.
388
498
  */
389
499
  function analyzeSolution(moves, options = {}) {
390
500
  const size = options.size === 2 ? 2 : 3;
@@ -420,6 +530,10 @@ function analyzeSolution(moves, options = {}) {
420
530
  f2l: [],
421
531
  oll: null,
422
532
  pll: null,
533
+ firstBlock: null,
534
+ secondBlock: null,
535
+ cmll: null,
536
+ lse: null,
423
537
  allCrosses: {},
424
538
  unsupported,
425
539
  };
@@ -491,11 +605,80 @@ function analyzeSolution(moves, options = {}) {
491
605
  return ai - bi;
492
606
  });
493
607
 
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;
608
+ const cfopChosen = ordered.find((c) => isCFOP(c.build)) ?? ordered[0];
609
+ const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
610
+
611
+ // Stage the solve as Roux (1st block -> 2nd block -> CMLL -> LSE) on every
612
+ // orientation. Each candidate fixes a side face and a perpendicular up face;
613
+ // buildForRoux assigns first/second block by which side finishes earliest.
614
+ // Pick the valid candidate whose first block completes earliest.
615
+ const mPerm = getMovePermutations(size)["M"].cw;
616
+ const rouxCandidates = [];
617
+ for (let s = 0; s < 6; s++) {
618
+ for (const u of geo.neighbors[s]) {
619
+ rouxCandidates.push(buildForRoux(snapshots, geo, s, u, mPerm));
620
+ }
621
+ }
622
+ const rouxBuild =
623
+ rouxCandidates
624
+ .filter((b) => isRoux(b))
625
+ .sort((a, b) => a.fbIdx - b.fbIdx)[0] ?? null;
626
+
627
+ // A solved cube satisfies many orderings, so both stagings can be technically
628
+ // valid. Disambiguate by which method's FIRST milestone is genuinely reached
629
+ // early: a real CFOP cross is built up front, whereas on a Roux solve no cross
630
+ // completes until LSE; conversely a full 1x2x3 block only forms mid-CFOP. The
631
+ // structure that actually happened owns the earlier first milestone; ties go
632
+ // to CFOP.
633
+ let method = "unknown";
634
+ if (cfopValid && rouxBuild) {
635
+ method = cfopChosen.build.crossIdx <= rouxBuild.fbIdx ? "CFOP" : "Roux";
636
+ } else if (cfopValid) {
637
+ method = "CFOP";
638
+ } else if (rouxBuild) {
639
+ method = "Roux";
640
+ }
641
+ const total = seq[n - 1].t;
642
+ const base = {
643
+ size,
644
+ method,
645
+ solved,
646
+ total,
647
+ tps: total > 0 ? simplifiedCount / (total / 1000) : 0,
648
+ moves: simplifiedMoves,
649
+ cross: null,
650
+ f2l: [],
651
+ oll: null,
652
+ pll: null,
653
+ firstBlock: null,
654
+ secondBlock: null,
655
+ cmll: null,
656
+ lse: null,
657
+ allCrosses,
658
+ unsupported,
659
+ };
497
660
 
498
- // Assemble timed records in solve order so durations chain correctly.
661
+ // Roux: report 1st block / 2nd block / CMLL / LSE, chaining durations.
662
+ if (method === "Roux") {
663
+ const fbM = milestone(rouxBuild.fbIdx, 0);
664
+ const sbM = milestone(rouxBuild.sbIdx, fbM.at);
665
+ const cmllM = milestone(rouxBuild.cmllIdx, sbM.at);
666
+ const lseM = milestone(rouxBuild.lseIdx, cmllM.at);
667
+ return {
668
+ ...base,
669
+ firstBlock: fbM.record
670
+ ? { side: finalCenters[rouxBuild.firstSide], ...fbM.record }
671
+ : null,
672
+ secondBlock: sbM.record
673
+ ? { side: finalCenters[rouxBuild.secondSide], ...sbM.record }
674
+ : null,
675
+ cmll: cmllM.record,
676
+ lse: lseM.record,
677
+ };
678
+ }
679
+
680
+ // CFOP (or unknown): report cross / F2L / OLL / PLL from the earliest cross.
681
+ const { color: crossColor, build } = cfopChosen;
499
682
  const crossM = milestone(build.crossIdx, 0);
500
683
  const cross = crossM.record
501
684
  ? { color: crossColor, ...crossM.record }
@@ -512,26 +695,15 @@ function analyzeSolution(moves, options = {}) {
512
695
  }
513
696
 
514
697
  const ollM = milestone(build.ollIdx, prevAt);
515
- const oll = ollM.record;
516
698
  prevAt = ollM.at;
517
-
518
699
  const pllM = milestone(build.pllIdx, prevAt);
519
- const pll = pllM.record;
520
700
 
521
- const total = seq[n - 1].t;
522
701
  return {
523
- size,
524
- method,
525
- solved,
526
- total,
527
- tps: total > 0 ? simplifiedCount / (total / 1000) : 0,
528
- moves: simplifiedMoves,
702
+ ...base,
529
703
  cross,
530
704
  f2l,
531
- oll,
532
- pll,
533
- allCrosses,
534
- unsupported,
705
+ oll: ollM.record,
706
+ pll: pllM.record,
535
707
  };
536
708
  }
537
709
 
package/dist/index.d.ts CHANGED
@@ -309,6 +309,59 @@ 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
+ function rouxBlockDone(st, centers, geo, sideFace, upFace) {
332
+ const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
333
+ if (edges.length !== 3 || corners.length !== 2) return false;
334
+ for (const e of edges)
335
+ if (!slotCorrect(st, centers, e.indices, geo.per)) return false;
336
+ for (const c of corners)
337
+ if (!slotCorrect(st, centers, c.indices, geo.per)) return false;
338
+ return true;
339
+ }
340
+
341
+ // Applies a permutation (out[i] = st[perm[i]]) to a flat sticker array.
342
+ function applyPerm(st, perm) {
343
+ const out = new Array(st.length);
344
+ for (let i = 0; i < st.length; i++) out[i] = st[perm[i]];
345
+ return out;
346
+ }
347
+
348
+ // CMLL is done when the four corners touching the up face are solved, allowing
349
+ // the M slice to be unaligned: we accept the state if ANY of the four M-slice
350
+ // rotations makes those corners correct. M moves neither the corners nor the
351
+ // L/R blocks, only the U/F/D/B centers (and M-slice edges), so this captures
352
+ // exactly "last-layer corners solved, M not necessarily aligned yet".
353
+ function cmllDone(st, geo, upFace, mPerm) {
354
+ let cur = st;
355
+ const corners = geo.cornersByFace(upFace);
356
+ for (let m = 0; m < 4; m++) {
357
+ if (m > 0) cur = applyPerm(cur, mPerm);
358
+ const centers = centersOf(cur, geo);
359
+ if (corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per)))
360
+ return true;
361
+ }
362
+ return false;
363
+ }
364
+
312
365
  // Index of the move that COMPLETES a stage: the first move after which the
313
366
  // condition holds, provided the stage is genuinely achieved by the end of the
314
367
  // solve. Later moves are allowed to break it transiently (a turn mid-algorithm
@@ -376,15 +429,72 @@ function isCFOP(build) {
376
429
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
377
430
  }
378
431
 
432
+ // Builds the Roux milestone indices for one (sideFace, upFace) orientation.
433
+ // Detection is cumulative, mirroring buildForCross: the second block only counts
434
+ // while the first block holds, CMLL only once both blocks hold, and LSE is the
435
+ // fully solved cube. The first block is whichever of the two opposite side faces
436
+ // finishes its block earliest; the other side is the second block.
437
+ function buildForRoux(snapshots, geo, sideA, upFace, mPerm) {
438
+ snapshots.length;
439
+ const sideB = geo.opposite[sideA];
440
+
441
+ const centersAt = snapshots.map((st) => centersOf(st, geo));
442
+ const aDone = snapshots.map((st, i) =>
443
+ rouxBlockDone(st, centersAt[i], geo, sideA, upFace)
444
+ );
445
+ const bDone = snapshots.map((st, i) =>
446
+ rouxBlockDone(st, centersAt[i], geo, sideB, upFace)
447
+ );
448
+ const aIdx = completionIndex(aDone);
449
+ const bIdx = completionIndex(bDone);
450
+
451
+ // First block = the side that completes earliest; second block = the other.
452
+ let firstSide = sideA;
453
+ let secondSide = sideB;
454
+ let fbBools = aDone;
455
+ let sbBools = bDone;
456
+ if ((bIdx ?? Infinity) < (aIdx ?? Infinity)) {
457
+ firstSide = sideB;
458
+ secondSide = sideA;
459
+ fbBools = bDone;
460
+ sbBools = aDone;
461
+ }
462
+
463
+ const fbIdx = completionIndex(fbBools);
464
+ // Second block gated by the first block holding at the same instant.
465
+ const secondBlockBools = snapshots.map((_, i) => fbBools[i] && sbBools[i]);
466
+ const sbIdx = completionIndex(secondBlockBools);
467
+
468
+ const cmllIdx = completionIndex(
469
+ snapshots.map(
470
+ (st, i) => secondBlockBools[i] && cmllDone(st, geo, upFace, mPerm)
471
+ )
472
+ );
473
+ const lseIdx = completionIndex(snapshots.map((st) => isSolvedFlat(st, geo.per)));
474
+
475
+ return { firstSide, secondSide, upFace, fbIdx, sbIdx, cmllIdx, lseIdx };
476
+ }
477
+
478
+ // Does this breakdown follow the Roux order: 1st block -> 2nd block -> CMLL -> LSE?
479
+ function isRoux(build) {
480
+ const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
481
+ if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
482
+ return false;
483
+ return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
484
+ }
485
+
379
486
  /**
380
487
  * Analyzes a solution and returns the timing of each method milestone.
381
488
  *
382
489
  * @param {Array<{m: string, t: number}>} moves - Solution moves with cumulative
383
490
  * 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
491
+ * @param {{size?: number}} [options] - Cube size (defaults to 3). Method staging
385
492
  * 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).
493
+ * @returns {object} Breakdown with `method` ("CFOP", "Roux" or "unknown"),
494
+ * `total`, `tps` and `allCrosses` (cross time per face color). For CFOP it
495
+ * carries `cross`, `f2l[]`, `oll`, `pll`; for Roux it carries `firstBlock`,
496
+ * `secondBlock`, `cmll`, `lse` (the other method's fields are null). Each
497
+ * block record also includes the `side` center color it was built on.
388
498
  */
389
499
  function analyzeSolution(moves, options = {}) {
390
500
  const size = options.size === 2 ? 2 : 3;
@@ -420,6 +530,10 @@ function analyzeSolution(moves, options = {}) {
420
530
  f2l: [],
421
531
  oll: null,
422
532
  pll: null,
533
+ firstBlock: null,
534
+ secondBlock: null,
535
+ cmll: null,
536
+ lse: null,
423
537
  allCrosses: {},
424
538
  unsupported,
425
539
  };
@@ -491,11 +605,80 @@ function analyzeSolution(moves, options = {}) {
491
605
  return ai - bi;
492
606
  });
493
607
 
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;
608
+ const cfopChosen = ordered.find((c) => isCFOP(c.build)) ?? ordered[0];
609
+ const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
610
+
611
+ // Stage the solve as Roux (1st block -> 2nd block -> CMLL -> LSE) on every
612
+ // orientation. Each candidate fixes a side face and a perpendicular up face;
613
+ // buildForRoux assigns first/second block by which side finishes earliest.
614
+ // Pick the valid candidate whose first block completes earliest.
615
+ const mPerm = getMovePermutations(size)["M"].cw;
616
+ const rouxCandidates = [];
617
+ for (let s = 0; s < 6; s++) {
618
+ for (const u of geo.neighbors[s]) {
619
+ rouxCandidates.push(buildForRoux(snapshots, geo, s, u, mPerm));
620
+ }
621
+ }
622
+ const rouxBuild =
623
+ rouxCandidates
624
+ .filter((b) => isRoux(b))
625
+ .sort((a, b) => a.fbIdx - b.fbIdx)[0] ?? null;
626
+
627
+ // A solved cube satisfies many orderings, so both stagings can be technically
628
+ // valid. Disambiguate by which method's FIRST milestone is genuinely reached
629
+ // early: a real CFOP cross is built up front, whereas on a Roux solve no cross
630
+ // completes until LSE; conversely a full 1x2x3 block only forms mid-CFOP. The
631
+ // structure that actually happened owns the earlier first milestone; ties go
632
+ // to CFOP.
633
+ let method = "unknown";
634
+ if (cfopValid && rouxBuild) {
635
+ method = cfopChosen.build.crossIdx <= rouxBuild.fbIdx ? "CFOP" : "Roux";
636
+ } else if (cfopValid) {
637
+ method = "CFOP";
638
+ } else if (rouxBuild) {
639
+ method = "Roux";
640
+ }
641
+ const total = seq[n - 1].t;
642
+ const base = {
643
+ size,
644
+ method,
645
+ solved,
646
+ total,
647
+ tps: total > 0 ? simplifiedCount / (total / 1000) : 0,
648
+ moves: simplifiedMoves,
649
+ cross: null,
650
+ f2l: [],
651
+ oll: null,
652
+ pll: null,
653
+ firstBlock: null,
654
+ secondBlock: null,
655
+ cmll: null,
656
+ lse: null,
657
+ allCrosses,
658
+ unsupported,
659
+ };
497
660
 
498
- // Assemble timed records in solve order so durations chain correctly.
661
+ // Roux: report 1st block / 2nd block / CMLL / LSE, chaining durations.
662
+ if (method === "Roux") {
663
+ const fbM = milestone(rouxBuild.fbIdx, 0);
664
+ const sbM = milestone(rouxBuild.sbIdx, fbM.at);
665
+ const cmllM = milestone(rouxBuild.cmllIdx, sbM.at);
666
+ const lseM = milestone(rouxBuild.lseIdx, cmllM.at);
667
+ return {
668
+ ...base,
669
+ firstBlock: fbM.record
670
+ ? { side: finalCenters[rouxBuild.firstSide], ...fbM.record }
671
+ : null,
672
+ secondBlock: sbM.record
673
+ ? { side: finalCenters[rouxBuild.secondSide], ...sbM.record }
674
+ : null,
675
+ cmll: cmllM.record,
676
+ lse: lseM.record,
677
+ };
678
+ }
679
+
680
+ // CFOP (or unknown): report cross / F2L / OLL / PLL from the earliest cross.
681
+ const { color: crossColor, build } = cfopChosen;
499
682
  const crossM = milestone(build.crossIdx, 0);
500
683
  const cross = crossM.record
501
684
  ? { color: crossColor, ...crossM.record }
@@ -512,26 +695,15 @@ function analyzeSolution(moves, options = {}) {
512
695
  }
513
696
 
514
697
  const ollM = milestone(build.ollIdx, prevAt);
515
- const oll = ollM.record;
516
698
  prevAt = ollM.at;
517
-
518
699
  const pllM = milestone(build.pllIdx, prevAt);
519
- const pll = pllM.record;
520
700
 
521
- const total = seq[n - 1].t;
522
701
  return {
523
- size,
524
- method,
525
- solved,
526
- total,
527
- tps: total > 0 ? simplifiedCount / (total / 1000) : 0,
528
- moves: simplifiedMoves,
702
+ ...base,
529
703
  cross,
530
704
  f2l,
531
- oll,
532
- pll,
533
- allCrosses,
534
- unsupported,
705
+ oll: ollM.record,
706
+ pll: pllM.record,
535
707
  };
536
708
  }
537
709
 
package/dist/index.js CHANGED
@@ -270,6 +270,37 @@ 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, centers, geo, sideFace, upFace) {
280
+ const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
281
+ if (edges.length !== 3 || corners.length !== 2) return false;
282
+ for (const e of edges)
283
+ if (!slotCorrect(st, centers, e.indices, geo.per)) return false;
284
+ for (const c of corners)
285
+ if (!slotCorrect(st, centers, c.indices, geo.per)) return false;
286
+ return true;
287
+ }
288
+ function applyPerm(st, perm) {
289
+ const out = new Array(st.length);
290
+ for (let i = 0; i < st.length; i++) out[i] = st[perm[i]];
291
+ return out;
292
+ }
293
+ function cmllDone(st, geo, upFace, mPerm) {
294
+ let cur = st;
295
+ const corners = geo.cornersByFace(upFace);
296
+ for (let m = 0; m < 4; m++) {
297
+ if (m > 0) cur = applyPerm(cur, mPerm);
298
+ const centers = centersOf(cur, geo);
299
+ if (corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per)))
300
+ return true;
301
+ }
302
+ return false;
303
+ }
273
304
  function completionIndex(bools) {
274
305
  const n = bools.length;
275
306
  if (n === 0 || !bools[n - 1]) return null;
@@ -315,15 +346,54 @@ function isCFOP(build) {
315
346
  const lastF2L = f2lSlots[3].idx;
316
347
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
317
348
  }
349
+ function buildForRoux(snapshots, geo, sideA, upFace, mPerm) {
350
+ const n = snapshots.length;
351
+ const sideB = geo.opposite[sideA];
352
+ const centersAt = snapshots.map((st) => centersOf(st, geo));
353
+ const aDone = snapshots.map(
354
+ (st, i) => rouxBlockDone(st, centersAt[i], geo, sideA, upFace)
355
+ );
356
+ const bDone = snapshots.map(
357
+ (st, i) => rouxBlockDone(st, centersAt[i], geo, sideB, upFace)
358
+ );
359
+ const aIdx = completionIndex(aDone);
360
+ const bIdx = completionIndex(bDone);
361
+ let firstSide = sideA;
362
+ let secondSide = sideB;
363
+ let fbBools = aDone;
364
+ let sbBools = bDone;
365
+ if ((bIdx != null ? bIdx : Infinity) < (aIdx != null ? aIdx : Infinity)) {
366
+ firstSide = sideB;
367
+ secondSide = sideA;
368
+ fbBools = bDone;
369
+ sbBools = aDone;
370
+ }
371
+ const fbIdx = completionIndex(fbBools);
372
+ const secondBlockBools = snapshots.map((_, i) => fbBools[i] && sbBools[i]);
373
+ const sbIdx = completionIndex(secondBlockBools);
374
+ const cmllIdx = completionIndex(
375
+ snapshots.map(
376
+ (st, i) => secondBlockBools[i] && cmllDone(st, geo, upFace, mPerm)
377
+ )
378
+ );
379
+ const lseIdx = completionIndex(snapshots.map((st) => isSolvedFlat(st, geo.per)));
380
+ return { firstSide, secondSide, upFace, fbIdx, sbIdx, cmllIdx, lseIdx };
381
+ }
382
+ function isRoux(build) {
383
+ const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
384
+ if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
385
+ return false;
386
+ return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
387
+ }
318
388
  function analyzeSolution(moves, options = {}) {
319
- var _a;
389
+ var _a, _b;
320
390
  const size = options.size === 2 ? 2 : 3;
321
391
  const unsupported = [];
322
392
  const seq = (Array.isArray(moves) ? moves : []).map((x) => {
323
393
  var _a2;
324
394
  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);
395
+ const { token, base: base2 } = normalizeToken(m);
396
+ const supported = base2 != null && SUPPORTED_BASES.has(base2);
327
397
  if (m.length > 0 && !supported) unsupported.push(m);
328
398
  return { m, mm: supported ? token : "", t: Number(x == null ? void 0 : x.t) };
329
399
  }).filter((x) => x.m.length > 0);
@@ -343,6 +413,10 @@ function analyzeSolution(moves, options = {}) {
343
413
  f2l: [],
344
414
  oll: null,
345
415
  pll: null,
416
+ firstBlock: null,
417
+ secondBlock: null,
418
+ cmll: null,
419
+ lse: null,
346
420
  allCrosses: {},
347
421
  unsupported
348
422
  };
@@ -369,13 +443,13 @@ function analyzeSolution(moves, options = {}) {
369
443
  };
370
444
  };
371
445
  if (size !== 3) {
372
- const pll2 = milestone(pllIdxOnly, 0);
446
+ const pll = milestone(pllIdxOnly, 0);
373
447
  const total2 = seq[n - 1].t;
374
448
  return __spreadProps(__spreadValues({}, empty), {
375
449
  solved,
376
450
  total: total2,
377
451
  tps: total2 > 0 ? simplifiedCount / (total2 / 1e3) : 0,
378
- pll: pll2.record
452
+ pll: pll.record
379
453
  });
380
454
  }
381
455
  const finalCenters = centersOf(snapshots[n - 1], geo);
@@ -388,14 +462,61 @@ function analyzeSolution(moves, options = {}) {
388
462
  allCrosses[color] = idx == null ? null : { at: seq[idx].t, moveIndex: idx, move: seq[idx].m };
389
463
  }
390
464
  const ordered = colors.map((color) => ({ color, build: buildForCross(snapshots, geo, color) })).sort((a, b) => {
391
- var _a2, _b;
465
+ var _a2, _b2;
392
466
  const ai = (_a2 = a.build.crossIdx) != null ? _a2 : Infinity;
393
- const bi = (_b = b.build.crossIdx) != null ? _b : Infinity;
467
+ const bi = (_b2 = b.build.crossIdx) != null ? _b2 : Infinity;
394
468
  return ai - bi;
395
469
  });
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;
470
+ const cfopChosen = (_a = ordered.find((c) => isCFOP(c.build))) != null ? _a : ordered[0];
471
+ const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
472
+ const mPerm = getMovePermutations(size)["M"].cw;
473
+ const rouxCandidates = [];
474
+ for (let s = 0; s < 6; s++) {
475
+ for (const u of geo.neighbors[s]) {
476
+ rouxCandidates.push(buildForRoux(snapshots, geo, s, u, mPerm));
477
+ }
478
+ }
479
+ const rouxBuild = (_b = rouxCandidates.filter((b) => isRoux(b)).sort((a, b) => a.fbIdx - b.fbIdx)[0]) != null ? _b : null;
480
+ let method = "unknown";
481
+ if (cfopValid && rouxBuild) {
482
+ method = cfopChosen.build.crossIdx <= rouxBuild.fbIdx ? "CFOP" : "Roux";
483
+ } else if (cfopValid) {
484
+ method = "CFOP";
485
+ } else if (rouxBuild) {
486
+ method = "Roux";
487
+ }
488
+ const total = seq[n - 1].t;
489
+ const base = {
490
+ size,
491
+ method,
492
+ solved,
493
+ total,
494
+ tps: total > 0 ? simplifiedCount / (total / 1e3) : 0,
495
+ moves: simplifiedMoves,
496
+ cross: null,
497
+ f2l: [],
498
+ oll: null,
499
+ pll: null,
500
+ firstBlock: null,
501
+ secondBlock: null,
502
+ cmll: null,
503
+ lse: null,
504
+ allCrosses,
505
+ unsupported
506
+ };
507
+ if (method === "Roux") {
508
+ const fbM = milestone(rouxBuild.fbIdx, 0);
509
+ const sbM = milestone(rouxBuild.sbIdx, fbM.at);
510
+ const cmllM = milestone(rouxBuild.cmllIdx, sbM.at);
511
+ const lseM = milestone(rouxBuild.lseIdx, cmllM.at);
512
+ return __spreadProps(__spreadValues({}, base), {
513
+ firstBlock: fbM.record ? __spreadValues({ side: finalCenters[rouxBuild.firstSide] }, fbM.record) : null,
514
+ secondBlock: sbM.record ? __spreadValues({ side: finalCenters[rouxBuild.secondSide] }, sbM.record) : null,
515
+ cmll: cmllM.record,
516
+ lse: lseM.record
517
+ });
518
+ }
519
+ const { color: crossColor, build } = cfopChosen;
399
520
  const crossM = milestone(build.crossIdx, 0);
400
521
  const cross = crossM.record ? __spreadValues({ color: crossColor }, crossM.record) : null;
401
522
  let prevAt = crossM.at;
@@ -408,25 +529,14 @@ function analyzeSolution(moves, options = {}) {
408
529
  }
409
530
  }
410
531
  const ollM = milestone(build.ollIdx, prevAt);
411
- const oll = ollM.record;
412
532
  prevAt = ollM.at;
413
533
  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,
534
+ return __spreadProps(__spreadValues({}, base), {
423
535
  cross,
424
536
  f2l,
425
- oll,
426
- pll,
427
- allCrosses,
428
- unsupported
429
- };
537
+ oll: ollM.record,
538
+ pll: pllM.record
539
+ });
430
540
  }
431
541
 
432
542
  // src/index.js
package/dist/index.mjs CHANGED
@@ -242,6 +242,37 @@ 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, centers, geo, sideFace, upFace) {
252
+ const { edges, corners } = rouxBlockPieces(geo, sideFace, upFace);
253
+ if (edges.length !== 3 || corners.length !== 2) return false;
254
+ for (const e of edges)
255
+ if (!slotCorrect(st, centers, e.indices, geo.per)) return false;
256
+ for (const c of corners)
257
+ if (!slotCorrect(st, centers, c.indices, geo.per)) return false;
258
+ return true;
259
+ }
260
+ function applyPerm(st, perm) {
261
+ const out = new Array(st.length);
262
+ for (let i = 0; i < st.length; i++) out[i] = st[perm[i]];
263
+ return out;
264
+ }
265
+ function cmllDone(st, geo, upFace, mPerm) {
266
+ let cur = st;
267
+ const corners = geo.cornersByFace(upFace);
268
+ for (let m = 0; m < 4; m++) {
269
+ if (m > 0) cur = applyPerm(cur, mPerm);
270
+ const centers = centersOf(cur, geo);
271
+ if (corners.every((c) => slotCorrect(cur, centers, c.indices, geo.per)))
272
+ return true;
273
+ }
274
+ return false;
275
+ }
245
276
  function completionIndex(bools) {
246
277
  const n = bools.length;
247
278
  if (n === 0 || !bools[n - 1]) return null;
@@ -287,15 +318,54 @@ function isCFOP(build) {
287
318
  const lastF2L = f2lSlots[3].idx;
288
319
  return ollIdx >= lastF2L && pllIdx >= ollIdx;
289
320
  }
321
+ function buildForRoux(snapshots, geo, sideA, upFace, mPerm) {
322
+ const n = snapshots.length;
323
+ const sideB = geo.opposite[sideA];
324
+ const centersAt = snapshots.map((st) => centersOf(st, geo));
325
+ const aDone = snapshots.map(
326
+ (st, i) => rouxBlockDone(st, centersAt[i], geo, sideA, upFace)
327
+ );
328
+ const bDone = snapshots.map(
329
+ (st, i) => rouxBlockDone(st, centersAt[i], geo, sideB, upFace)
330
+ );
331
+ const aIdx = completionIndex(aDone);
332
+ const bIdx = completionIndex(bDone);
333
+ let firstSide = sideA;
334
+ let secondSide = sideB;
335
+ let fbBools = aDone;
336
+ let sbBools = bDone;
337
+ if ((bIdx != null ? bIdx : Infinity) < (aIdx != null ? aIdx : Infinity)) {
338
+ firstSide = sideB;
339
+ secondSide = sideA;
340
+ fbBools = bDone;
341
+ sbBools = aDone;
342
+ }
343
+ const fbIdx = completionIndex(fbBools);
344
+ const secondBlockBools = snapshots.map((_, i) => fbBools[i] && sbBools[i]);
345
+ const sbIdx = completionIndex(secondBlockBools);
346
+ const cmllIdx = completionIndex(
347
+ snapshots.map(
348
+ (st, i) => secondBlockBools[i] && cmllDone(st, geo, upFace, mPerm)
349
+ )
350
+ );
351
+ const lseIdx = completionIndex(snapshots.map((st) => isSolvedFlat(st, geo.per)));
352
+ return { firstSide, secondSide, upFace, fbIdx, sbIdx, cmllIdx, lseIdx };
353
+ }
354
+ function isRoux(build) {
355
+ const { fbIdx, sbIdx, cmllIdx, lseIdx } = build;
356
+ if (fbIdx == null || sbIdx == null || cmllIdx == null || lseIdx == null)
357
+ return false;
358
+ return fbIdx <= sbIdx && sbIdx <= cmllIdx && cmllIdx <= lseIdx;
359
+ }
290
360
  function analyzeSolution(moves, options = {}) {
291
- var _a;
361
+ var _a, _b;
292
362
  const size = options.size === 2 ? 2 : 3;
293
363
  const unsupported = [];
294
364
  const seq = (Array.isArray(moves) ? moves : []).map((x) => {
295
365
  var _a2;
296
366
  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);
367
+ const { token, base: base2 } = normalizeToken(m);
368
+ const supported = base2 != null && SUPPORTED_BASES.has(base2);
299
369
  if (m.length > 0 && !supported) unsupported.push(m);
300
370
  return { m, mm: supported ? token : "", t: Number(x == null ? void 0 : x.t) };
301
371
  }).filter((x) => x.m.length > 0);
@@ -315,6 +385,10 @@ function analyzeSolution(moves, options = {}) {
315
385
  f2l: [],
316
386
  oll: null,
317
387
  pll: null,
388
+ firstBlock: null,
389
+ secondBlock: null,
390
+ cmll: null,
391
+ lse: null,
318
392
  allCrosses: {},
319
393
  unsupported
320
394
  };
@@ -341,13 +415,13 @@ function analyzeSolution(moves, options = {}) {
341
415
  };
342
416
  };
343
417
  if (size !== 3) {
344
- const pll2 = milestone(pllIdxOnly, 0);
418
+ const pll = milestone(pllIdxOnly, 0);
345
419
  const total2 = seq[n - 1].t;
346
420
  return __spreadProps(__spreadValues({}, empty), {
347
421
  solved,
348
422
  total: total2,
349
423
  tps: total2 > 0 ? simplifiedCount / (total2 / 1e3) : 0,
350
- pll: pll2.record
424
+ pll: pll.record
351
425
  });
352
426
  }
353
427
  const finalCenters = centersOf(snapshots[n - 1], geo);
@@ -360,14 +434,61 @@ function analyzeSolution(moves, options = {}) {
360
434
  allCrosses[color] = idx == null ? null : { at: seq[idx].t, moveIndex: idx, move: seq[idx].m };
361
435
  }
362
436
  const ordered = colors.map((color) => ({ color, build: buildForCross(snapshots, geo, color) })).sort((a, b) => {
363
- var _a2, _b;
437
+ var _a2, _b2;
364
438
  const ai = (_a2 = a.build.crossIdx) != null ? _a2 : Infinity;
365
- const bi = (_b = b.build.crossIdx) != null ? _b : Infinity;
439
+ const bi = (_b2 = b.build.crossIdx) != null ? _b2 : Infinity;
366
440
  return ai - bi;
367
441
  });
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;
442
+ const cfopChosen = (_a = ordered.find((c) => isCFOP(c.build))) != null ? _a : ordered[0];
443
+ const cfopValid = !!cfopChosen && isCFOP(cfopChosen.build);
444
+ const mPerm = getMovePermutations(size)["M"].cw;
445
+ const rouxCandidates = [];
446
+ for (let s = 0; s < 6; s++) {
447
+ for (const u of geo.neighbors[s]) {
448
+ rouxCandidates.push(buildForRoux(snapshots, geo, s, u, mPerm));
449
+ }
450
+ }
451
+ const rouxBuild = (_b = rouxCandidates.filter((b) => isRoux(b)).sort((a, b) => a.fbIdx - b.fbIdx)[0]) != null ? _b : null;
452
+ let method = "unknown";
453
+ if (cfopValid && rouxBuild) {
454
+ method = cfopChosen.build.crossIdx <= rouxBuild.fbIdx ? "CFOP" : "Roux";
455
+ } else if (cfopValid) {
456
+ method = "CFOP";
457
+ } else if (rouxBuild) {
458
+ method = "Roux";
459
+ }
460
+ const total = seq[n - 1].t;
461
+ const base = {
462
+ size,
463
+ method,
464
+ solved,
465
+ total,
466
+ tps: total > 0 ? simplifiedCount / (total / 1e3) : 0,
467
+ moves: simplifiedMoves,
468
+ cross: null,
469
+ f2l: [],
470
+ oll: null,
471
+ pll: null,
472
+ firstBlock: null,
473
+ secondBlock: null,
474
+ cmll: null,
475
+ lse: null,
476
+ allCrosses,
477
+ unsupported
478
+ };
479
+ if (method === "Roux") {
480
+ const fbM = milestone(rouxBuild.fbIdx, 0);
481
+ const sbM = milestone(rouxBuild.sbIdx, fbM.at);
482
+ const cmllM = milestone(rouxBuild.cmllIdx, sbM.at);
483
+ const lseM = milestone(rouxBuild.lseIdx, cmllM.at);
484
+ return __spreadProps(__spreadValues({}, base), {
485
+ firstBlock: fbM.record ? __spreadValues({ side: finalCenters[rouxBuild.firstSide] }, fbM.record) : null,
486
+ secondBlock: sbM.record ? __spreadValues({ side: finalCenters[rouxBuild.secondSide] }, sbM.record) : null,
487
+ cmll: cmllM.record,
488
+ lse: lseM.record
489
+ });
490
+ }
491
+ const { color: crossColor, build } = cfopChosen;
371
492
  const crossM = milestone(build.crossIdx, 0);
372
493
  const cross = crossM.record ? __spreadValues({ color: crossColor }, crossM.record) : null;
373
494
  let prevAt = crossM.at;
@@ -380,25 +501,14 @@ function analyzeSolution(moves, options = {}) {
380
501
  }
381
502
  }
382
503
  const ollM = milestone(build.ollIdx, prevAt);
383
- const oll = ollM.record;
384
504
  prevAt = ollM.at;
385
505
  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,
506
+ return __spreadProps(__spreadValues({}, base), {
395
507
  cross,
396
508
  f2l,
397
- oll,
398
- pll,
399
- allCrosses,
400
- unsupported
401
- };
509
+ oll: ollM.record,
510
+ pll: pllM.record
511
+ });
402
512
  }
403
513
 
404
514
  // 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.6.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",