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