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.
- package/.claude/settings.local.json +6 -1
- package/dist/index.d.mts +214 -21
- package/dist/index.d.ts +214 -21
- package/dist/index.js +142 -25
- package/dist/index.mjs +142 -25
- package/package.json +1 -1
|
@@ -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).
|
|
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
|
|
387
|
-
* `
|
|
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
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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).
|
|
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
|
|
387
|
-
* `
|
|
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
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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,
|
|
467
|
+
var _a2, _b2;
|
|
392
468
|
const ai = (_a2 = a.build.crossIdx) != null ? _a2 : Infinity;
|
|
393
|
-
const bi = (
|
|
469
|
+
const bi = (_b2 = b.build.crossIdx) != null ? _b2 : Infinity;
|
|
394
470
|
return ai - bi;
|
|
395
471
|
});
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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,
|
|
439
|
+
var _a2, _b2;
|
|
364
440
|
const ai = (_a2 = a.build.crossIdx) != null ? _a2 : Infinity;
|
|
365
|
-
const bi = (
|
|
441
|
+
const bi = (_b2 = b.build.crossIdx) != null ? _b2 : Infinity;
|
|
366
442
|
return ai - bi;
|
|
367
443
|
});
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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