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