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