cube-state-engine 1.3.0 → 1.5.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 +9 -0
- package/.idea/AICommit.xml +6 -0
- package/.idea/awsToolkit.xml +11 -0
- package/.idea/cube-state-engine.iml +1 -5
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/dist/index.d.mts +1217 -672
- package/dist/index.d.ts +1217 -672
- package/dist/index.js +862 -496
- package/dist/index.mjs +855 -495
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,736 +1,1281 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
// Move-sequence simplifier.
|
|
2
|
+
//
|
|
3
|
+
// Connects consecutive IDENTICAL quarter turns into a single double turn
|
|
4
|
+
// (R R -> R2, F F -> F2, y y -> y2, R' R' -> R2). It does NOT cancel opposite
|
|
5
|
+
// moves: F F' stays exactly as F F', and U2 U2 stays as U2 U2 — no reduction
|
|
6
|
+
// to identity, no net-rotation folding. The only transformation is "join two
|
|
7
|
+
// equal quarter turns into their double".
|
|
8
|
+
//
|
|
9
|
+
// Accepts a space-separated string, an array of token strings, or an array of
|
|
10
|
+
// timed moves (`[{ m, t }]`); returns the same shape. When merging timed moves
|
|
11
|
+
// the resulting double keeps the SECOND move's timestamp (the instant the
|
|
12
|
+
// double turn finishes), so it composes cleanly with analyzeSolution.
|
|
13
|
+
|
|
14
|
+
// Two tokens merge iff they are exactly equal AND are quarter turns (no "2").
|
|
15
|
+
// The merged token drops any prime and appends "2" (R'->R2, Rw->Rw2).
|
|
16
|
+
function canMerge(a, b) {
|
|
17
|
+
return typeof a === "string" && a === b && !a.includes("2");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function doubled(tok) {
|
|
21
|
+
return tok.replace("'", "") + "2";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Core pass over plain token strings. A single left-to-right pass suffices:
|
|
25
|
+
// merging two quarters into a double can never create a new mergeable pair.
|
|
26
|
+
function simplifyTokens(tokens) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (let i = 0; i < tokens.length; ) {
|
|
29
|
+
const a = tokens[i];
|
|
30
|
+
const b = tokens[i + 1];
|
|
31
|
+
if (i + 1 < tokens.length && canMerge(a, b)) {
|
|
32
|
+
out.push(doubled(a));
|
|
33
|
+
i += 2;
|
|
34
|
+
} else {
|
|
35
|
+
out.push(a);
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Same pass over timed moves, keeping the second move's timestamp on a merge.
|
|
43
|
+
function simplifyTimed(moves) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (let i = 0; i < moves.length; ) {
|
|
46
|
+
const a = moves[i];
|
|
47
|
+
const b = moves[i + 1];
|
|
48
|
+
if (i + 1 < moves.length && canMerge(a?.m, b?.m)) {
|
|
49
|
+
out.push({ m: doubled(a.m), t: b.t });
|
|
50
|
+
i += 2;
|
|
51
|
+
} else {
|
|
52
|
+
out.push(a);
|
|
53
|
+
i += 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Simplifies a move sequence by joining consecutive identical quarter turns
|
|
61
|
+
* into double turns, without cancelling opposite or redundant moves.
|
|
62
|
+
*
|
|
63
|
+
* @param {string|string[]|Array<{m: string, t: number}>} moves - The sequence,
|
|
64
|
+
* as a space-separated string, an array of tokens, or timed moves.
|
|
65
|
+
* @returns {string|string[]|Array<{m: string, t: number}>} The simplified
|
|
66
|
+
* sequence, in the same shape as the input.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* simplifyMoves("U R R D R' B"); // "U R2 D R' B"
|
|
70
|
+
* simplifyMoves("F F'"); // "F F'" (unchanged)
|
|
71
|
+
* simplifyMoves("R R R"); // "R2 R" (no cancellation)
|
|
72
|
+
* simplifyMoves(["y'", "y'", "L"]); // ["y2", "L"]
|
|
73
|
+
*/
|
|
74
|
+
function simplifyMoves(moves) {
|
|
75
|
+
if (typeof moves === "string") {
|
|
76
|
+
const tokens = moves.split(/\s+/).filter((t) => t.length > 0);
|
|
77
|
+
return simplifyTokens(tokens).join(" ");
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(moves)) {
|
|
80
|
+
if (moves.length === 0) return [];
|
|
81
|
+
if (typeof moves[0] === "string") return simplifyTokens(moves);
|
|
82
|
+
return simplifyTimed(moves);
|
|
83
|
+
}
|
|
84
|
+
return moves;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Solution analyzer: replays a recorded solve move-by-move and reports the
|
|
88
|
+
// timing of each method milestone (cross, the four F2L pairs, OLL and PLL),
|
|
89
|
+
// plus a best-effort guess of the solving method (currently CFOP).
|
|
90
|
+
//
|
|
91
|
+
// Input is just the solution as `[{ m, t }]` where `m` is a move token in the
|
|
92
|
+
// same notation the engine accepts (e.g. "R", "U'", "R2", "x", "M'") and `t`
|
|
93
|
+
// is the CUMULATIVE elapsed time (ms) up to and including that move. The
|
|
94
|
+
// scramble is not required: it is derived as the inverse of the solution.
|
|
95
|
+
//
|
|
96
|
+
// All milestone checks compare each sticker against the CURRENT center of its
|
|
97
|
+
// face, so detection is invariant to whole-cube rotations (x/y/z) and wide
|
|
98
|
+
// moves that may appear in real speedsolves.
|
|
12
99
|
|
|
13
|
-
// States object for the rotation
|
|
14
|
-
STATES = {
|
|
15
|
-
UPPER: [
|
|
16
|
-
// (White)
|
|
17
|
-
[COLOR.W[0], COLOR.W[1], COLOR.W[2]],
|
|
18
|
-
[COLOR.W[3], COLOR.W[4], COLOR.W[5]],
|
|
19
|
-
[COLOR.W[6], COLOR.W[7], COLOR.W[8]],
|
|
20
|
-
],
|
|
21
|
-
LEFT: [
|
|
22
|
-
// (Orange)
|
|
23
|
-
[COLOR.O[0], COLOR.O[1], COLOR.O[2]],
|
|
24
|
-
[COLOR.O[3], COLOR.O[4], COLOR.O[5]],
|
|
25
|
-
[COLOR.O[6], COLOR.O[7], COLOR.O[8]],
|
|
26
|
-
],
|
|
27
|
-
FRONT: [
|
|
28
|
-
// (Green)
|
|
29
|
-
[COLOR.G[0], COLOR.G[1], COLOR.G[2]],
|
|
30
|
-
[COLOR.G[3], COLOR.G[4], COLOR.G[5]],
|
|
31
|
-
[COLOR.G[6], COLOR.G[7], COLOR.G[8]],
|
|
32
|
-
],
|
|
33
|
-
RIGHT: [
|
|
34
|
-
// (Red)
|
|
35
|
-
[COLOR.R[0], COLOR.R[1], COLOR.R[2]],
|
|
36
|
-
[COLOR.R[3], COLOR.R[4], COLOR.R[5]],
|
|
37
|
-
[COLOR.R[6], COLOR.R[7], COLOR.R[8]],
|
|
38
|
-
],
|
|
39
|
-
BACK: [
|
|
40
|
-
// (Blue)
|
|
41
|
-
[COLOR.B[0], COLOR.B[1], COLOR.B[2]],
|
|
42
|
-
[COLOR.B[3], COLOR.B[4], COLOR.B[5]],
|
|
43
|
-
[COLOR.B[6], COLOR.B[7], COLOR.B[8]],
|
|
44
|
-
],
|
|
45
|
-
DOWN: [
|
|
46
|
-
// (Yellow)
|
|
47
|
-
[COLOR.Y[0], COLOR.Y[1], COLOR.Y[2]],
|
|
48
|
-
[COLOR.Y[3], COLOR.Y[4], COLOR.Y[5]],
|
|
49
|
-
[COLOR.Y[6], COLOR.Y[7], COLOR.Y[8]],
|
|
50
|
-
],
|
|
51
|
-
};
|
|
52
100
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*/
|
|
56
|
-
rotateU(clockwise = true) {
|
|
57
|
-
if (clockwise) {
|
|
58
|
-
this.#rotateU(true);
|
|
59
|
-
this.MOVES.push("U");
|
|
60
|
-
} else {
|
|
61
|
-
this.#rotateU(false);
|
|
62
|
-
this.MOVES.push("U'");
|
|
63
|
-
}
|
|
64
|
-
}
|
|
101
|
+
// Flat layout face order (must match CubeEngine.state()).
|
|
102
|
+
const FACE_NAMES$1 = ["UPPER", "LEFT", "FRONT", "RIGHT", "BACK", "DOWN"];
|
|
65
103
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
this.STATES.UPPER = this.#switchMatrix(this.STATES.UPPER, true);
|
|
69
|
-
|
|
70
|
-
const tempFront = [...this.STATES.FRONT[0]];
|
|
71
|
-
const tempRight = [...this.STATES.RIGHT[0]];
|
|
72
|
-
const tempLeft = [...this.STATES.LEFT[0]];
|
|
73
|
-
const tempBack = [...this.STATES.BACK[0]];
|
|
74
|
-
|
|
75
|
-
this.STATES.FRONT[0] = [...tempRight];
|
|
76
|
-
this.STATES.LEFT[0] = [...tempFront];
|
|
77
|
-
this.STATES.BACK[0] = [...tempLeft];
|
|
78
|
-
this.STATES.RIGHT[0] = [...tempBack];
|
|
79
|
-
} else {
|
|
80
|
-
this.STATES.UPPER = this.#switchMatrix(this.STATES.UPPER, false);
|
|
81
|
-
|
|
82
|
-
const tempFront = [...this.STATES.FRONT[0]];
|
|
83
|
-
const tempRight = [...this.STATES.RIGHT[0]];
|
|
84
|
-
const tempLeft = [...this.STATES.LEFT[0]];
|
|
85
|
-
const tempBack = [...this.STATES.BACK[0]];
|
|
86
|
-
|
|
87
|
-
this.STATES.FRONT[0] = [...tempLeft];
|
|
88
|
-
this.STATES.LEFT[0] = [...tempBack];
|
|
89
|
-
this.STATES.BACK[0] = [...tempRight];
|
|
90
|
-
this.STATES.RIGHT[0] = [...tempFront];
|
|
91
|
-
}
|
|
92
|
-
}
|
|
104
|
+
// Basic face moves mapped to their face index in the flat sticker layout.
|
|
105
|
+
const FACE_MOVE_TO_INDEX = { U: 0, L: 1, F: 2, R: 3, B: 4, D: 5 };
|
|
93
106
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} else {
|
|
102
|
-
this.#rotateF(false);
|
|
103
|
-
this.MOVES.push("F'");
|
|
104
|
-
}
|
|
105
|
-
}
|
|
107
|
+
// Move bases the engine can actually apply (after normalization).
|
|
108
|
+
const SUPPORTED_BASES = new Set([
|
|
109
|
+
"U", "D", "L", "R", "F", "B",
|
|
110
|
+
"x", "y", "z",
|
|
111
|
+
"M", "E", "S",
|
|
112
|
+
"Uw", "Dw", "Rw", "Lw", "Fw",
|
|
113
|
+
]);
|
|
106
114
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this.#rotateX(true);
|
|
110
|
-
this.#rotateU(true);
|
|
111
|
-
this.#rotateX(false);
|
|
112
|
-
} else {
|
|
113
|
-
this.#rotateX(true);
|
|
114
|
-
this.#rotateU(false);
|
|
115
|
-
this.#rotateX(false);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
115
|
+
// Lowercase single-letter wide moves -> the engine's wide notation.
|
|
116
|
+
const WIDE_LOWER = { r: "Rw", u: "Uw", f: "Fw", l: "Lw", d: "Dw", b: "Bw" };
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.MOVES.push("B'");
|
|
129
|
-
}
|
|
130
|
-
}
|
|
118
|
+
// Normalizes one move token to the engine's notation and reports its base.
|
|
119
|
+
// Returns { token, base } where base is null if the token is unparseable.
|
|
120
|
+
function normalizeToken(raw) {
|
|
121
|
+
const m = String(raw).trim().match(/^([A-Za-z]w?)('?2?|2?'?)$/);
|
|
122
|
+
if (!m) return { token: raw, base: null };
|
|
123
|
+
let base = m[1];
|
|
124
|
+
if (base.length === 1 && WIDE_LOWER[base]) base = WIDE_LOWER[base];
|
|
125
|
+
return { token: base + m[2], base };
|
|
126
|
+
}
|
|
131
127
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Clockwise/counterclockwise direction is preserved through y2 conjugation
|
|
135
|
-
this.#rotateY(true);
|
|
136
|
-
this.#rotateY(true);
|
|
137
|
-
if (clockwise) {
|
|
138
|
-
this.#rotateF(true);
|
|
139
|
-
} else {
|
|
140
|
-
this.#rotateF(false);
|
|
141
|
-
}
|
|
142
|
-
this.#rotateY(false);
|
|
143
|
-
this.#rotateY(false);
|
|
144
|
-
}
|
|
128
|
+
// Derived sticker geometry is identical for every cube of a given size.
|
|
129
|
+
const GEOMETRY_CACHE = new Map();
|
|
145
130
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
131
|
+
/**
|
|
132
|
+
* Derives the sticker adjacency of a cube from the engine's permutation tables.
|
|
133
|
+
*
|
|
134
|
+
* A facelet is displaced by a face turn iff it physically belongs to that face,
|
|
135
|
+
* so the SET of basic face turns that move a facelet identifies the cubie it
|
|
136
|
+
* sits on: 1 face => center, 2 faces => edge, 3 faces => corner. Grouping
|
|
137
|
+
* facelets by that signature reconstructs every edge/corner slot without any
|
|
138
|
+
* hardcoded layout, and stays correct for any matrix convention the engine uses.
|
|
139
|
+
*/
|
|
140
|
+
function buildGeometry(size) {
|
|
141
|
+
if (GEOMETRY_CACHE.has(size)) return GEOMETRY_CACHE.get(size);
|
|
158
142
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.#rotateU(true);
|
|
164
|
-
this.#rotateX(false);
|
|
165
|
-
this.#rotateY(false);
|
|
166
|
-
} else {
|
|
167
|
-
this.#rotateY(true);
|
|
168
|
-
this.#rotateX(true);
|
|
169
|
-
this.#rotateU(false);
|
|
170
|
-
this.#rotateX(false);
|
|
171
|
-
this.#rotateY(false);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
143
|
+
const perms = getMovePermutations(size);
|
|
144
|
+
const per = size * size;
|
|
145
|
+
const total = per * 6;
|
|
146
|
+
const faceMoves = Object.keys(FACE_MOVE_TO_INDEX);
|
|
174
147
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
} else {
|
|
183
|
-
this.#rotateL(false);
|
|
184
|
-
this.MOVES.push("L'");
|
|
148
|
+
// signature[i] = sorted face indices whose quarter turn displaces sticker i.
|
|
149
|
+
const edgeMap = new Map(); // "a,b" -> indices[]
|
|
150
|
+
const cornerMap = new Map(); // "a,b,c" -> indices[]
|
|
151
|
+
for (let i = 0; i < total; i++) {
|
|
152
|
+
const faces = [];
|
|
153
|
+
for (const mv of faceMoves) {
|
|
154
|
+
if (perms[mv].cw[i] !== i) faces.push(FACE_MOVE_TO_INDEX[mv]);
|
|
185
155
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.#rotateY(true);
|
|
195
|
-
} else {
|
|
196
|
-
this.#rotateY(false);
|
|
197
|
-
this.#rotateX(true);
|
|
198
|
-
this.#rotateU(false);
|
|
199
|
-
this.#rotateX(false);
|
|
200
|
-
this.#rotateY(true);
|
|
156
|
+
faces.sort((a, b) => a - b);
|
|
157
|
+
const key = faces.join(",");
|
|
158
|
+
if (faces.length === 2) {
|
|
159
|
+
if (!edgeMap.has(key)) edgeMap.set(key, []);
|
|
160
|
+
edgeMap.get(key).push(i);
|
|
161
|
+
} else if (faces.length === 3) {
|
|
162
|
+
if (!cornerMap.has(key)) cornerMap.set(key, []);
|
|
163
|
+
cornerMap.get(key).push(i);
|
|
201
164
|
}
|
|
202
165
|
}
|
|
203
166
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
this.#rotateD(false);
|
|
213
|
-
this.MOVES.push("D'");
|
|
214
|
-
}
|
|
215
|
-
}
|
|
167
|
+
const edges = [...edgeMap.entries()].map(([key, indices]) => ({
|
|
168
|
+
faces: key.split(",").map(Number),
|
|
169
|
+
indices,
|
|
170
|
+
}));
|
|
171
|
+
const corners = [...cornerMap.entries()].map(([key, indices]) => ({
|
|
172
|
+
faces: key.split(",").map(Number),
|
|
173
|
+
indices,
|
|
174
|
+
}));
|
|
216
175
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this.#rotateX(true);
|
|
224
|
-
this.#rotateF(false);
|
|
225
|
-
this.#rotateX(false);
|
|
226
|
-
}
|
|
176
|
+
// Neighbor / opposite relationships between the six face positions.
|
|
177
|
+
const neighbors = Array.from({ length: 6 }, () => new Set());
|
|
178
|
+
for (const e of edges) {
|
|
179
|
+
const [a, b] = e.faces;
|
|
180
|
+
neighbors[a].add(b);
|
|
181
|
+
neighbors[b].add(a);
|
|
227
182
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.MOVES.push("Dw");
|
|
236
|
-
} else {
|
|
237
|
-
this.#rotateDw(false);
|
|
238
|
-
this.MOVES.push("Dw'");
|
|
183
|
+
const opposite = new Array(6).fill(-1);
|
|
184
|
+
for (let f = 0; f < 6; f++) {
|
|
185
|
+
for (let g = 0; g < 6; g++) {
|
|
186
|
+
if (g !== f && !neighbors[f].has(g)) {
|
|
187
|
+
opposite[f] = g;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
239
190
|
}
|
|
240
191
|
}
|
|
241
192
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
193
|
+
const geo = {
|
|
194
|
+
size,
|
|
195
|
+
per,
|
|
196
|
+
centerIndex: (f) => f * per + Math.floor(per / 2),
|
|
197
|
+
edges,
|
|
198
|
+
corners,
|
|
199
|
+
neighbors,
|
|
200
|
+
opposite,
|
|
201
|
+
edgesByFace: (f) => edges.filter((e) => e.faces.includes(f)),
|
|
202
|
+
cornersByFace: (f) => corners.filter((c) => c.faces.includes(f)),
|
|
203
|
+
edgeByPair: (a, b) =>
|
|
204
|
+
edges.find(
|
|
205
|
+
(e) =>
|
|
206
|
+
(e.faces[0] === a && e.faces[1] === b) ||
|
|
207
|
+
(e.faces[0] === b && e.faces[1] === a)
|
|
208
|
+
),
|
|
209
|
+
};
|
|
251
210
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
rotateUw(clockwise = true) {
|
|
256
|
-
if (clockwise) {
|
|
257
|
-
this.#rotateUw(true);
|
|
258
|
-
this.MOVES.push("Uw");
|
|
259
|
-
} else {
|
|
260
|
-
this.#rotateUw(false);
|
|
261
|
-
this.MOVES.push("Uw'");
|
|
262
|
-
}
|
|
263
|
-
}
|
|
211
|
+
GEOMETRY_CACHE.set(size, geo);
|
|
212
|
+
return geo;
|
|
213
|
+
}
|
|
264
214
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
this.#rotateD(false);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
215
|
+
/** Inverts a single move token (R -> R', R' -> R, R2 -> R2). */
|
|
216
|
+
function invertToken(tok) {
|
|
217
|
+
if (tok.endsWith("2")) return tok;
|
|
218
|
+
if (tok.endsWith("'")) return tok.slice(0, -1);
|
|
219
|
+
return tok + "'";
|
|
220
|
+
}
|
|
274
221
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
this.#rotateRw(false);
|
|
284
|
-
this.MOVES.push("Rw'");
|
|
285
|
-
}
|
|
286
|
-
}
|
|
222
|
+
/**
|
|
223
|
+
* Inverts a sequence of move tokens (reverse order, each token inverted).
|
|
224
|
+
* @param {string[]} tokens
|
|
225
|
+
* @returns {string[]}
|
|
226
|
+
*/
|
|
227
|
+
function invertSequence(tokens) {
|
|
228
|
+
return tokens.slice().reverse().map(invertToken);
|
|
229
|
+
}
|
|
287
230
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
231
|
+
// Flattens CubeEngine.state() back into the flat sticker array layout.
|
|
232
|
+
function flattenState(state) {
|
|
233
|
+
const out = [];
|
|
234
|
+
for (const name of FACE_NAMES$1) {
|
|
235
|
+
const matrix = state[name];
|
|
236
|
+
for (const row of matrix) {
|
|
237
|
+
for (const v of row) out.push(v);
|
|
295
238
|
}
|
|
296
239
|
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
297
242
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
this.MOVES.push("Lw");
|
|
305
|
-
} else {
|
|
306
|
-
this.#rotateLw(false);
|
|
307
|
-
this.MOVES.push("Lw'");
|
|
308
|
-
}
|
|
309
|
-
}
|
|
243
|
+
// Center color currently shown on each face position.
|
|
244
|
+
function centersOf(st, geo) {
|
|
245
|
+
const centers = new Array(6);
|
|
246
|
+
for (let f = 0; f < 6; f++) centers[f] = st[geo.centerIndex(f)];
|
|
247
|
+
return centers;
|
|
248
|
+
}
|
|
310
249
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this.#rotateR(true);
|
|
316
|
-
} else {
|
|
317
|
-
this.#rotateX(true);
|
|
318
|
-
this.#rotateR(false);
|
|
319
|
-
}
|
|
250
|
+
// A slot is correctly placed when every facelet matches its own face center.
|
|
251
|
+
function slotCorrect(st, centers, indices, per) {
|
|
252
|
+
for (const x of indices) {
|
|
253
|
+
if (st[x] !== centers[Math.floor(x / per)]) return false;
|
|
320
254
|
}
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
321
257
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
this.MOVES.push("M");
|
|
329
|
-
} else {
|
|
330
|
-
this.#rotateM(false);
|
|
331
|
-
this.MOVES.push("M'");
|
|
332
|
-
}
|
|
258
|
+
// Whole cube solved: every face is a single (uniform) color.
|
|
259
|
+
function isSolvedFlat(st, per) {
|
|
260
|
+
for (let f = 0; f < 6; f++) {
|
|
261
|
+
const base = f * per;
|
|
262
|
+
const c = st[base];
|
|
263
|
+
for (let i = 1; i < per; i++) if (st[base + i] !== c) return false;
|
|
333
264
|
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
334
267
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
268
|
+
// True when the cross of the given color is complete in this state.
|
|
269
|
+
function crossDone(st, geo, color) {
|
|
270
|
+
const centers = centersOf(st, geo);
|
|
271
|
+
const C = centers.indexOf(color);
|
|
272
|
+
if (C < 0) return false;
|
|
273
|
+
return geo
|
|
274
|
+
.edgesByFace(C)
|
|
275
|
+
.every((e) => slotCorrect(st, centers, e.indices, geo.per));
|
|
276
|
+
}
|
|
344
277
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
278
|
+
// Per-slot F2L completion keyed by the (stable) pair of side-face colors.
|
|
279
|
+
function f2lSlotStates(st, geo, color) {
|
|
280
|
+
const centers = centersOf(st, geo);
|
|
281
|
+
const C = centers.indexOf(color);
|
|
282
|
+
const result = {};
|
|
283
|
+
if (C < 0) return result;
|
|
284
|
+
for (const corner of geo.cornersByFace(C)) {
|
|
285
|
+
const sides = corner.faces.filter((f) => f !== C);
|
|
286
|
+
if (sides.length !== 2) continue;
|
|
287
|
+
const [a, b] = sides;
|
|
288
|
+
const edge = geo.edgeByPair(a, b);
|
|
289
|
+
const cornerOk = slotCorrect(st, centers, corner.indices, geo.per);
|
|
290
|
+
const edgeOk = edge
|
|
291
|
+
? slotCorrect(st, centers, edge.indices, geo.per)
|
|
292
|
+
: false;
|
|
293
|
+
const key = [centers[a], centers[b]].sort().join("-");
|
|
294
|
+
result[key] = cornerOk && edgeOk;
|
|
356
295
|
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
357
298
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
// Balance the rotation
|
|
368
|
-
this.STATES.LEFT = this.#switchMatrix(tempLeft, false);
|
|
369
|
-
this.STATES.RIGHT = this.#switchMatrix(tempRight, true);
|
|
370
|
-
|
|
371
|
-
// Rotate mid X axis
|
|
372
|
-
this.STATES.FRONT = [...tempDown];
|
|
373
|
-
this.STATES.UPPER = [...tempFront];
|
|
374
|
-
|
|
375
|
-
// Special permutation (BACK view elements)
|
|
376
|
-
this.STATES.BACK = this.#specialFlip(tempUpper);
|
|
377
|
-
this.STATES.DOWN = this.#specialFlip(tempBack);
|
|
378
|
-
} else {
|
|
379
|
-
this.STATES.LEFT = this.#switchMatrix(tempLeft, true);
|
|
380
|
-
this.STATES.RIGHT = this.#switchMatrix(tempRight, false);
|
|
381
|
-
|
|
382
|
-
this.STATES.FRONT = [...tempUpper];
|
|
383
|
-
this.STATES.DOWN = [...tempFront];
|
|
384
|
-
|
|
385
|
-
this.STATES.BACK = this.#specialFlip(tempDown);
|
|
386
|
-
this.STATES.UPPER = this.#specialFlip(tempBack);
|
|
387
|
-
}
|
|
299
|
+
// Last layer (face opposite the cross) fully oriented = one color on its top.
|
|
300
|
+
function ollDone(st, geo, color) {
|
|
301
|
+
const centers = centersOf(st, geo);
|
|
302
|
+
const C = centers.indexOf(color);
|
|
303
|
+
if (C < 0) return false;
|
|
304
|
+
const O = geo.opposite[C];
|
|
305
|
+
const base = O * geo.per;
|
|
306
|
+
for (let i = 0; i < geo.per; i++) {
|
|
307
|
+
if (st[base + i] !== centers[O]) return false;
|
|
388
308
|
}
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
389
311
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
312
|
+
// Index of the move that COMPLETES a stage: the first move after which the
|
|
313
|
+
// condition holds, provided the stage is genuinely achieved by the end of the
|
|
314
|
+
// solve. Later moves are allowed to break it transiently (a turn mid-algorithm
|
|
315
|
+
// momentarily disturbs an already-finished cross/pair), which is why we take
|
|
316
|
+
// the first occurrence rather than requiring it to hold continuously.
|
|
317
|
+
function completionIndex(bools) {
|
|
318
|
+
const n = bools.length;
|
|
319
|
+
if (n === 0 || !bools[n - 1]) return null;
|
|
320
|
+
for (let i = 0; i < n; i++) if (bools[i]) return i;
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
402
323
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (clockwise) {
|
|
412
|
-
// Rotate faces on the rotation axis
|
|
413
|
-
this.STATES.FRONT = this.#switchMatrix(tempFront, true);
|
|
414
|
-
this.STATES.BACK = this.#switchMatrix(tempBack, false);
|
|
415
|
-
|
|
416
|
-
// Cycle U -> R -> D -> L -> U with proper orientation
|
|
417
|
-
this.STATES.RIGHT = this.#switchMatrix(tempUpper, true);
|
|
418
|
-
this.STATES.DOWN = this.#switchMatrix(tempRight, true);
|
|
419
|
-
this.STATES.LEFT = this.#switchMatrix(tempDown, true);
|
|
420
|
-
this.STATES.UPPER = this.#switchMatrix(tempLeft, true);
|
|
421
|
-
} else {
|
|
422
|
-
// Counterclockwise
|
|
423
|
-
this.STATES.FRONT = this.#switchMatrix(tempFront, false);
|
|
424
|
-
this.STATES.BACK = this.#switchMatrix(tempBack, true);
|
|
425
|
-
|
|
426
|
-
// Cycle U -> L -> D -> R -> U (inverse of clockwise), rotate CCW
|
|
427
|
-
this.STATES.RIGHT = this.#switchMatrix(tempDown, false);
|
|
428
|
-
this.STATES.DOWN = this.#switchMatrix(tempLeft, false);
|
|
429
|
-
this.STATES.LEFT = this.#switchMatrix(tempUpper, false);
|
|
430
|
-
this.STATES.UPPER = this.#switchMatrix(tempRight, false);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
324
|
+
// Builds the milestone indices for one assumed cross color. Detection is
|
|
325
|
+
// cumulative: an F2L pair only counts while the cross is solved, and OLL only
|
|
326
|
+
// counts once the full F2L is solved. This rejects transient false positives.
|
|
327
|
+
function buildForCross(snapshots, geo, color) {
|
|
328
|
+
const n = snapshots.length;
|
|
329
|
+
const crossBools = snapshots.map((st) => crossDone(st, geo, color));
|
|
330
|
+
const crossIdx = completionIndex(crossBools);
|
|
433
331
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
} else {
|
|
442
|
-
this.#rotateY(false);
|
|
443
|
-
this.MOVES.push("y'");
|
|
332
|
+
// Per-slot completion, gated by the cross being solved at the same instant.
|
|
333
|
+
const slotSeries = new Map();
|
|
334
|
+
for (let i = 0; i < n; i++) {
|
|
335
|
+
const states = f2lSlotStates(snapshots[i], geo, color);
|
|
336
|
+
for (const [key, val] of Object.entries(states)) {
|
|
337
|
+
if (!slotSeries.has(key)) slotSeries.set(key, new Array(n).fill(false));
|
|
338
|
+
slotSeries.get(key)[i] = crossBools[i] && val;
|
|
444
339
|
}
|
|
445
340
|
}
|
|
341
|
+
const f2lSlots = [...slotSeries.entries()]
|
|
342
|
+
.map(([slot, bools]) => ({ slot, idx: completionIndex(bools) }))
|
|
343
|
+
.filter((s) => s.idx != null)
|
|
344
|
+
.sort((a, b) => a.idx - b.idx);
|
|
446
345
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
this.STATES.FRONT = [...tempRight];
|
|
458
|
-
this.STATES.RIGHT = [...tempBack];
|
|
459
|
-
this.STATES.LEFT = [...tempFront];
|
|
460
|
-
this.STATES.BACK = [...tempLeft];
|
|
461
|
-
} else {
|
|
462
|
-
this.STATES.UPPER = this.#switchMatrix(this.STATES.UPPER, false);
|
|
463
|
-
this.STATES.DOWN = this.#switchMatrix(this.STATES.DOWN, true);
|
|
464
|
-
|
|
465
|
-
this.STATES.FRONT = [...tempLeft];
|
|
466
|
-
this.STATES.RIGHT = [...tempFront];
|
|
467
|
-
this.STATES.LEFT = [...tempBack];
|
|
468
|
-
this.STATES.BACK = [...tempRight];
|
|
346
|
+
// Full F2L solved = cross plus all four pairs solved at the same instant.
|
|
347
|
+
const f2lComplete = new Array(n).fill(false);
|
|
348
|
+
for (let i = 0; i < n; i++) {
|
|
349
|
+
if (!crossBools[i] || slotSeries.size < 4) continue;
|
|
350
|
+
let all = true;
|
|
351
|
+
for (const bools of slotSeries.values()) {
|
|
352
|
+
if (!bools[i]) {
|
|
353
|
+
all = false;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
469
356
|
}
|
|
357
|
+
f2lComplete[i] = all;
|
|
470
358
|
}
|
|
471
359
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const tempMatrix = [...clone[0], ...clone[1], ...clone[2]];
|
|
479
|
-
|
|
480
|
-
if (clockwise) {
|
|
481
|
-
return [
|
|
482
|
-
[tempMatrix[6], tempMatrix[3], tempMatrix[0]],
|
|
483
|
-
[tempMatrix[7], tempMatrix[4], tempMatrix[1]],
|
|
484
|
-
[tempMatrix[8], tempMatrix[5], tempMatrix[2]],
|
|
485
|
-
];
|
|
486
|
-
} else {
|
|
487
|
-
return [
|
|
488
|
-
[tempMatrix[2], tempMatrix[5], tempMatrix[8]],
|
|
489
|
-
[tempMatrix[1], tempMatrix[4], tempMatrix[7]],
|
|
490
|
-
[tempMatrix[0], tempMatrix[3], tempMatrix[6]],
|
|
491
|
-
];
|
|
492
|
-
}
|
|
493
|
-
}
|
|
360
|
+
// OLL is only meaningful once F2L is done (last layer oriented on top of it).
|
|
361
|
+
const ollIdx = completionIndex(
|
|
362
|
+
snapshots.map((st, i) => f2lComplete[i] && ollDone(st, geo, color))
|
|
363
|
+
);
|
|
364
|
+
const pllIdx = completionIndex(snapshots.map((st) => isSolvedFlat(st, geo.per)));
|
|
494
365
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
.reverse()
|
|
498
|
-
.map((row) => [...row].reverse());
|
|
499
|
-
}
|
|
366
|
+
return { crossIdx, f2lSlots, ollIdx, pllIdx };
|
|
367
|
+
}
|
|
500
368
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
369
|
+
// Does this breakdown follow the CFOP order: cross -> 4x F2L -> OLL -> PLL?
|
|
370
|
+
function isCFOP(build) {
|
|
371
|
+
const { crossIdx, f2lSlots, ollIdx, pllIdx } = build;
|
|
372
|
+
if (crossIdx == null || pllIdx == null || ollIdx == null) return false;
|
|
373
|
+
if (f2lSlots.length !== 4) return false;
|
|
374
|
+
if (!f2lSlots.every((s) => s.idx >= crossIdx)) return false;
|
|
375
|
+
const lastF2L = f2lSlots[3].idx;
|
|
376
|
+
return ollIdx >= lastF2L && pllIdx >= ollIdx;
|
|
377
|
+
}
|
|
509
378
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
379
|
+
/**
|
|
380
|
+
* Analyzes a solution and returns the timing of each method milestone.
|
|
381
|
+
*
|
|
382
|
+
* @param {Array<{m: string, t: number}>} moves - Solution moves with cumulative
|
|
383
|
+
* timestamps. `m` is a move token; `t` is elapsed ms up to that move.
|
|
384
|
+
* @param {{size?: number}} [options] - Cube size (defaults to 3). CFOP staging
|
|
385
|
+
* is only computed for 3x3; other sizes report the solved (PLL) time only.
|
|
386
|
+
* @returns {object} Breakdown with `method`, `total`, `cross`, `f2l[]`, `oll`,
|
|
387
|
+
* `pll` and `allCrosses` (cross time per face color).
|
|
388
|
+
*/
|
|
389
|
+
function analyzeSolution(moves, options = {}) {
|
|
390
|
+
const size = options.size === 2 ? 2 : 3;
|
|
517
391
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
392
|
+
// Keep both the original token (`m`, for display) and the engine-normalized
|
|
393
|
+
// token (`mm`, used for replay). Collect anything we cannot parse so the
|
|
394
|
+
// caller can see dropped moves instead of getting silently wrong timings.
|
|
395
|
+
const unsupported = [];
|
|
396
|
+
const seq = (Array.isArray(moves) ? moves : [])
|
|
397
|
+
.map((x) => {
|
|
398
|
+
const m = String(x?.m ?? "").trim();
|
|
399
|
+
const { token, base } = normalizeToken(m);
|
|
400
|
+
const supported = base != null && SUPPORTED_BASES.has(base);
|
|
401
|
+
if (m.length > 0 && !supported) unsupported.push(m);
|
|
402
|
+
return { m, mm: supported ? token : "", t: Number(x?.t) };
|
|
403
|
+
})
|
|
404
|
+
.filter((x) => x.m.length > 0);
|
|
405
|
+
const n = seq.length;
|
|
524
406
|
|
|
525
|
-
|
|
407
|
+
const simplifiedMoves = simplifyMoves(
|
|
408
|
+
(Array.isArray(moves) ? moves : []).filter((x) => x?.m)
|
|
409
|
+
);
|
|
410
|
+
const simplifiedCount = simplifiedMoves.length;
|
|
526
411
|
|
|
527
|
-
|
|
528
|
-
|
|
412
|
+
const empty = {
|
|
413
|
+
size,
|
|
414
|
+
method: "unknown",
|
|
415
|
+
solved: false,
|
|
416
|
+
total: n > 0 ? seq[n - 1].t : 0,
|
|
417
|
+
tps: 0,
|
|
418
|
+
moves: simplifiedMoves,
|
|
419
|
+
cross: null,
|
|
420
|
+
f2l: [],
|
|
421
|
+
oll: null,
|
|
422
|
+
pll: null,
|
|
423
|
+
allCrosses: {},
|
|
424
|
+
unsupported,
|
|
425
|
+
};
|
|
426
|
+
if (n === 0) return empty;
|
|
529
427
|
|
|
530
|
-
|
|
531
|
-
|
|
428
|
+
// Reproduce the scramble (inverse of the solution), then replay forward,
|
|
429
|
+
// capturing a flat snapshot after every solution move. Unsupported tokens
|
|
430
|
+
// contribute no move (mm === "") so the replay simply skips them.
|
|
431
|
+
const engine = new CubeEngine("", { size });
|
|
432
|
+
const scramble = invertSequence(seq.map((x) => x.mm).filter(Boolean)).join(" ");
|
|
433
|
+
engine.applyMoves(scramble, { record: false });
|
|
532
434
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
*/
|
|
539
|
-
getMoves(asString = true) {
|
|
540
|
-
return asString ? this.MOVES.join(" ") : this.MOVES;
|
|
435
|
+
const geo = buildGeometry(size);
|
|
436
|
+
const snapshots = new Array(n);
|
|
437
|
+
for (let i = 0; i < n; i++) {
|
|
438
|
+
if (seq[i].mm) engine.applyMoves(seq[i].mm, { record: false });
|
|
439
|
+
snapshots[i] = flattenState(engine.state());
|
|
541
440
|
}
|
|
542
441
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
BACK: [
|
|
569
|
-
[COLOR.B[0], COLOR.B[1], COLOR.B[2]],
|
|
570
|
-
[COLOR.B[3], COLOR.B[4], COLOR.B[5]],
|
|
571
|
-
[COLOR.B[6], COLOR.B[7], COLOR.B[8]],
|
|
572
|
-
],
|
|
573
|
-
DOWN: [
|
|
574
|
-
[COLOR.Y[0], COLOR.Y[1], COLOR.Y[2]],
|
|
575
|
-
[COLOR.Y[3], COLOR.Y[4], COLOR.Y[5]],
|
|
576
|
-
[COLOR.Y[6], COLOR.Y[7], COLOR.Y[8]],
|
|
577
|
-
],
|
|
442
|
+
const solved = isSolvedFlat(snapshots[n - 1], geo.per);
|
|
443
|
+
const pllIdxOnly = completionIndex(
|
|
444
|
+
snapshots.map((st) => isSolvedFlat(st, geo.per))
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
// Map a milestone index to a timed record, with duration since `prevAt`.
|
|
448
|
+
const milestone = (idx, prevAt) => {
|
|
449
|
+
if (idx == null) return { record: null, at: prevAt };
|
|
450
|
+
const at = seq[idx].t;
|
|
451
|
+
return {
|
|
452
|
+
record: { at, duration: at - prevAt, moveIndex: idx, move: seq[idx].m },
|
|
453
|
+
at,
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// Non-3x3: only the solved (PLL) milestone is meaningful.
|
|
458
|
+
if (size !== 3) {
|
|
459
|
+
const pll = milestone(pllIdxOnly, 0);
|
|
460
|
+
const total = seq[n - 1].t;
|
|
461
|
+
return {
|
|
462
|
+
...empty,
|
|
463
|
+
solved,
|
|
464
|
+
total,
|
|
465
|
+
tps: total > 0 ? simplifiedCount / (total / 1000) : 0,
|
|
466
|
+
pll: pll.record,
|
|
578
467
|
};
|
|
579
|
-
this.MOVES = [];
|
|
580
468
|
}
|
|
581
469
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
470
|
+
// Cross can complete on any of the six faces; record each, then pick the
|
|
471
|
+
// cross color that yields a valid CFOP staging (falling back to the earliest).
|
|
472
|
+
const finalCenters = centersOf(snapshots[n - 1], geo);
|
|
473
|
+
const colors = [...new Set(finalCenters)];
|
|
474
|
+
|
|
475
|
+
const allCrosses = {};
|
|
476
|
+
for (const color of colors) {
|
|
477
|
+
const idx = completionIndex(
|
|
478
|
+
snapshots.map((st) => crossDone(st, geo, color))
|
|
479
|
+
);
|
|
480
|
+
allCrosses[color] =
|
|
481
|
+
idx == null
|
|
482
|
+
? null
|
|
483
|
+
: { at: seq[idx].t, moveIndex: idx, move: seq[idx].m };
|
|
591
484
|
}
|
|
592
485
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
.
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
switch (base) {
|
|
622
|
-
case 'U':
|
|
623
|
-
{
|
|
624
|
-
const isWide = /w/i.test(rest);
|
|
625
|
-
if (isWide) {
|
|
626
|
-
exec(
|
|
627
|
-
() => (record ? this.rotateUw(true) : this.#rotateUw(true)),
|
|
628
|
-
() => (record ? this.rotateUw(false) : this.#rotateUw(false))
|
|
629
|
-
);
|
|
630
|
-
} else {
|
|
631
|
-
exec(
|
|
632
|
-
() => (record ? this.rotateU(true) : this.#rotateU(true)),
|
|
633
|
-
() => (record ? this.rotateU(false) : this.#rotateU(false))
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
break;
|
|
638
|
-
case 'D':
|
|
639
|
-
{
|
|
640
|
-
const isWide = /w/i.test(rest);
|
|
641
|
-
if (isWide) {
|
|
642
|
-
exec(
|
|
643
|
-
() => (record ? this.rotateDw(true) : this.#rotateDw(true)),
|
|
644
|
-
() => (record ? this.rotateDw(false) : this.#rotateDw(false))
|
|
645
|
-
);
|
|
646
|
-
} else {
|
|
647
|
-
exec(
|
|
648
|
-
() => (record ? this.rotateD(true) : this.#rotateD(true)),
|
|
649
|
-
() => (record ? this.rotateD(false) : this.#rotateD(false))
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
break;
|
|
654
|
-
case 'L':
|
|
655
|
-
{
|
|
656
|
-
const isWide = /w/i.test(rest);
|
|
657
|
-
if (isWide) {
|
|
658
|
-
exec(
|
|
659
|
-
() => (record ? this.rotateLw(true) : this.#rotateLw(true)),
|
|
660
|
-
() => (record ? this.rotateLw(false) : this.#rotateLw(false))
|
|
661
|
-
);
|
|
662
|
-
} else {
|
|
663
|
-
exec(
|
|
664
|
-
() => (record ? this.rotateL(true) : this.#rotateL(true)),
|
|
665
|
-
() => (record ? this.rotateL(false) : this.#rotateL(false))
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
break;
|
|
670
|
-
case 'R':
|
|
671
|
-
{
|
|
672
|
-
const isWide = /w/i.test(rest);
|
|
673
|
-
if (isWide) {
|
|
674
|
-
exec(
|
|
675
|
-
() => (record ? this.rotateRw(true) : this.#rotateRw(true)),
|
|
676
|
-
() => (record ? this.rotateRw(false) : this.#rotateRw(false))
|
|
677
|
-
);
|
|
678
|
-
} else {
|
|
679
|
-
exec(
|
|
680
|
-
() => (record ? this.rotateR(true) : this.#rotateR(true)),
|
|
681
|
-
() => (record ? this.rotateR(false) : this.#rotateR(false))
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
break;
|
|
686
|
-
case 'F':
|
|
687
|
-
exec(
|
|
688
|
-
() => (record ? this.rotateF(true) : this.#rotateF(true)),
|
|
689
|
-
() => (record ? this.rotateF(false) : this.#rotateF(false))
|
|
690
|
-
);
|
|
691
|
-
break;
|
|
692
|
-
case 'B':
|
|
693
|
-
exec(
|
|
694
|
-
() => (record ? this.rotateB(true) : this.#rotateB(true)),
|
|
695
|
-
() => (record ? this.rotateB(false) : this.#rotateB(false))
|
|
696
|
-
);
|
|
697
|
-
break;
|
|
698
|
-
case 'x':
|
|
699
|
-
exec(
|
|
700
|
-
() => (record ? this.rotateX(true) : this.#rotateX(true)),
|
|
701
|
-
() => (record ? this.rotateX(false) : this.#rotateX(false))
|
|
702
|
-
);
|
|
703
|
-
break;
|
|
704
|
-
case 'y':
|
|
705
|
-
exec(
|
|
706
|
-
() => (record ? this.rotateY(true) : this.#rotateY(true)),
|
|
707
|
-
() => (record ? this.rotateY(false) : this.#rotateY(false))
|
|
708
|
-
);
|
|
709
|
-
break;
|
|
710
|
-
case 'z':
|
|
711
|
-
exec(
|
|
712
|
-
() => (record ? this.rotateZ(true) : this.#rotateZ(true)),
|
|
713
|
-
() => (record ? this.rotateZ(false) : this.#rotateZ(false))
|
|
714
|
-
);
|
|
715
|
-
break;
|
|
716
|
-
case 'M':
|
|
717
|
-
exec(
|
|
718
|
-
() => (record ? this.rotateM(true) : this.#rotateM(true)),
|
|
719
|
-
() => (record ? this.rotateM(false) : this.#rotateM(false))
|
|
720
|
-
);
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
486
|
+
const ordered = colors
|
|
487
|
+
.map((color) => ({ color, build: buildForCross(snapshots, geo, color) }))
|
|
488
|
+
.sort((a, b) => {
|
|
489
|
+
const ai = a.build.crossIdx ?? Infinity;
|
|
490
|
+
const bi = b.build.crossIdx ?? Infinity;
|
|
491
|
+
return ai - bi;
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
let chosen = ordered.find((c) => isCFOP(c.build)) ?? ordered[0];
|
|
495
|
+
const method = chosen && isCFOP(chosen.build) ? "CFOP" : "unknown";
|
|
496
|
+
const { color: crossColor, build } = chosen;
|
|
497
|
+
|
|
498
|
+
// Assemble timed records in solve order so durations chain correctly.
|
|
499
|
+
const crossM = milestone(build.crossIdx, 0);
|
|
500
|
+
const cross = crossM.record
|
|
501
|
+
? { color: crossColor, ...crossM.record }
|
|
502
|
+
: null;
|
|
503
|
+
|
|
504
|
+
let prevAt = crossM.at;
|
|
505
|
+
const f2l = [];
|
|
506
|
+
for (const slot of build.f2lSlots) {
|
|
507
|
+
const m = milestone(slot.idx, prevAt);
|
|
508
|
+
if (m.record) {
|
|
509
|
+
f2l.push({ slot: slot.slot, ...m.record });
|
|
510
|
+
prevAt = m.at;
|
|
723
511
|
}
|
|
724
512
|
}
|
|
725
|
-
}
|
|
726
513
|
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
514
|
+
const ollM = milestone(build.ollIdx, prevAt);
|
|
515
|
+
const oll = ollM.record;
|
|
516
|
+
prevAt = ollM.at;
|
|
517
|
+
|
|
518
|
+
const pllM = milestone(build.pllIdx, prevAt);
|
|
519
|
+
const pll = pllM.record;
|
|
520
|
+
|
|
521
|
+
const total = seq[n - 1].t;
|
|
522
|
+
return {
|
|
523
|
+
size,
|
|
524
|
+
method,
|
|
525
|
+
solved,
|
|
526
|
+
total,
|
|
527
|
+
tps: total > 0 ? simplifiedCount / (total / 1000) : 0,
|
|
528
|
+
moves: simplifiedMoves,
|
|
529
|
+
cross,
|
|
530
|
+
f2l,
|
|
531
|
+
oll,
|
|
532
|
+
pll,
|
|
533
|
+
allCrosses,
|
|
534
|
+
unsupported,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Face order used across the flat sticker representation.
|
|
539
|
+
// Index: 0=UPPER 1=LEFT 2=FRONT 3=RIGHT 4=BACK 5=DOWN
|
|
540
|
+
const FACE_NAMES = ["UPPER", "LEFT", "FRONT", "RIGHT", "BACK", "DOWN"];
|
|
541
|
+
const FACE_COLORS = ["W", "O", "G", "R", "B", "Y"];
|
|
542
|
+
|
|
543
|
+
// Moves that only affect inner/double layers and therefore are no-ops on a 2x2.
|
|
544
|
+
const NOOP_SIZE2 = new Set(["Uw", "Dw", "Rw", "Lw", "Fw", "M", "E", "S"]);
|
|
545
|
+
|
|
546
|
+
// Maps each move key to the oracle method that performs it.
|
|
547
|
+
const MOVE_FNS = {
|
|
548
|
+
U: "rotateU",
|
|
549
|
+
D: "rotateD",
|
|
550
|
+
L: "rotateL",
|
|
551
|
+
R: "rotateR",
|
|
552
|
+
F: "rotateF",
|
|
553
|
+
B: "rotateB",
|
|
554
|
+
x: "rotateX",
|
|
555
|
+
y: "rotateY",
|
|
556
|
+
z: "rotateZ",
|
|
557
|
+
M: "rotateM",
|
|
558
|
+
E: "rotateE",
|
|
559
|
+
S: "rotateS",
|
|
560
|
+
Uw: "rotateUw",
|
|
561
|
+
Dw: "rotateDw",
|
|
562
|
+
Rw: "rotateRw",
|
|
563
|
+
Lw: "rotateLw",
|
|
564
|
+
Fw: "rotateFw",
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// Permutation tables are derived once per cube size and shared across instances.
|
|
568
|
+
const PERM_CACHE = new Map();
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Reference (matrix-based) cube used only to derive permutation tables.
|
|
572
|
+
*
|
|
573
|
+
* It reproduces the exact rotation algorithm the engine has always used, but
|
|
574
|
+
* operates on integer sticker tags (the flat index each sticker starts at).
|
|
575
|
+
* Applying a move and flattening the result yields a permutation array `perm`
|
|
576
|
+
* such that `newState[i] = oldState[perm[i]]`. This runs a handful of times per
|
|
577
|
+
* size at module load and is then cached, so the runtime hot path never touches
|
|
578
|
+
* matrices, structuredClone, or move composition.
|
|
579
|
+
*/
|
|
580
|
+
class _OracleCube {
|
|
581
|
+
constructor(size) {
|
|
582
|
+
this.size = size;
|
|
583
|
+
this.STATES = {};
|
|
584
|
+
for (let f = 0; f < FACE_NAMES.length; f++) {
|
|
585
|
+
const face = [];
|
|
586
|
+
for (let r = 0; r < size; r++) {
|
|
587
|
+
const row = [];
|
|
588
|
+
for (let c = 0; c < size; c++) {
|
|
589
|
+
row.push(f * size * size + r * size + c);
|
|
590
|
+
}
|
|
591
|
+
face.push(row);
|
|
592
|
+
}
|
|
593
|
+
this.STATES[FACE_NAMES[f]] = face;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Concatenate every face row-major into a single flat array of tags.
|
|
598
|
+
flatten() {
|
|
599
|
+
const out = [];
|
|
600
|
+
for (const name of FACE_NAMES) {
|
|
601
|
+
const face = this.STATES[name];
|
|
602
|
+
for (let r = 0; r < this.size; r++) {
|
|
603
|
+
for (let c = 0; c < this.size; c++) {
|
|
604
|
+
out.push(face[r][c]);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
switchMatrix(matrix, clockwise = true) {
|
|
612
|
+
const clone = structuredClone(matrix);
|
|
613
|
+
const size = this.size;
|
|
614
|
+
|
|
615
|
+
let tempMatrix = [];
|
|
616
|
+
for (let i = 0; i < size; i++) {
|
|
617
|
+
tempMatrix = [...tempMatrix, ...clone[i]];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (size === 2) {
|
|
621
|
+
if (clockwise) {
|
|
622
|
+
return [
|
|
623
|
+
[tempMatrix[2], tempMatrix[0]],
|
|
624
|
+
[tempMatrix[3], tempMatrix[1]],
|
|
625
|
+
];
|
|
626
|
+
} else {
|
|
627
|
+
return [
|
|
628
|
+
[tempMatrix[1], tempMatrix[3]],
|
|
629
|
+
[tempMatrix[0], tempMatrix[2]],
|
|
630
|
+
];
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
if (clockwise) {
|
|
634
|
+
return [
|
|
635
|
+
[tempMatrix[6], tempMatrix[3], tempMatrix[0]],
|
|
636
|
+
[tempMatrix[7], tempMatrix[4], tempMatrix[1]],
|
|
637
|
+
[tempMatrix[8], tempMatrix[5], tempMatrix[2]],
|
|
638
|
+
];
|
|
639
|
+
} else {
|
|
640
|
+
return [
|
|
641
|
+
[tempMatrix[2], tempMatrix[5], tempMatrix[8]],
|
|
642
|
+
[tempMatrix[1], tempMatrix[4], tempMatrix[7]],
|
|
643
|
+
[tempMatrix[0], tempMatrix[3], tempMatrix[6]],
|
|
644
|
+
];
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
specialFlip(matrix) {
|
|
650
|
+
return structuredClone(matrix)
|
|
651
|
+
.reverse()
|
|
652
|
+
.map((row) => [...row].reverse());
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
rotateU(clockwise = true) {
|
|
656
|
+
if (clockwise) {
|
|
657
|
+
this.STATES.UPPER = this.switchMatrix(this.STATES.UPPER, true);
|
|
658
|
+
|
|
659
|
+
const tempFront = [...this.STATES.FRONT[0]];
|
|
660
|
+
const tempRight = [...this.STATES.RIGHT[0]];
|
|
661
|
+
const tempLeft = [...this.STATES.LEFT[0]];
|
|
662
|
+
const tempBack = [...this.STATES.BACK[0]];
|
|
663
|
+
|
|
664
|
+
this.STATES.FRONT[0] = [...tempRight];
|
|
665
|
+
this.STATES.LEFT[0] = [...tempFront];
|
|
666
|
+
this.STATES.BACK[0] = [...tempLeft];
|
|
667
|
+
this.STATES.RIGHT[0] = [...tempBack];
|
|
668
|
+
} else {
|
|
669
|
+
this.STATES.UPPER = this.switchMatrix(this.STATES.UPPER, false);
|
|
670
|
+
|
|
671
|
+
const tempFront = [...this.STATES.FRONT[0]];
|
|
672
|
+
const tempRight = [...this.STATES.RIGHT[0]];
|
|
673
|
+
const tempLeft = [...this.STATES.LEFT[0]];
|
|
674
|
+
const tempBack = [...this.STATES.BACK[0]];
|
|
675
|
+
|
|
676
|
+
this.STATES.FRONT[0] = [...tempLeft];
|
|
677
|
+
this.STATES.LEFT[0] = [...tempBack];
|
|
678
|
+
this.STATES.BACK[0] = [...tempRight];
|
|
679
|
+
this.STATES.RIGHT[0] = [...tempFront];
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
rotateF(clockwise = true) {
|
|
684
|
+
if (clockwise) {
|
|
685
|
+
this.rotateX(true);
|
|
686
|
+
this.rotateU(true);
|
|
687
|
+
this.rotateX(false);
|
|
688
|
+
} else {
|
|
689
|
+
this.rotateX(true);
|
|
690
|
+
this.rotateU(false);
|
|
691
|
+
this.rotateX(false);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
rotateB(clockwise = true) {
|
|
696
|
+
this.rotateY(true);
|
|
697
|
+
this.rotateY(true);
|
|
698
|
+
if (clockwise) {
|
|
699
|
+
this.rotateF(true);
|
|
700
|
+
} else {
|
|
701
|
+
this.rotateF(false);
|
|
702
|
+
}
|
|
703
|
+
this.rotateY(false);
|
|
704
|
+
this.rotateY(false);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
rotateR(clockwise = true) {
|
|
708
|
+
if (clockwise) {
|
|
709
|
+
this.rotateY(true);
|
|
710
|
+
this.rotateX(true);
|
|
711
|
+
this.rotateU(true);
|
|
712
|
+
this.rotateX(false);
|
|
713
|
+
this.rotateY(false);
|
|
714
|
+
} else {
|
|
715
|
+
this.rotateY(true);
|
|
716
|
+
this.rotateX(true);
|
|
717
|
+
this.rotateU(false);
|
|
718
|
+
this.rotateX(false);
|
|
719
|
+
this.rotateY(false);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
rotateL(clockwise = true) {
|
|
724
|
+
if (clockwise) {
|
|
725
|
+
this.rotateY(false);
|
|
726
|
+
this.rotateX(true);
|
|
727
|
+
this.rotateU(true);
|
|
728
|
+
this.rotateX(false);
|
|
729
|
+
this.rotateY(true);
|
|
730
|
+
} else {
|
|
731
|
+
this.rotateY(false);
|
|
732
|
+
this.rotateX(true);
|
|
733
|
+
this.rotateU(false);
|
|
734
|
+
this.rotateX(false);
|
|
735
|
+
this.rotateY(true);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
rotateD(clockwise = true) {
|
|
740
|
+
if (clockwise) {
|
|
741
|
+
this.rotateX(true);
|
|
742
|
+
this.rotateF(true);
|
|
743
|
+
this.rotateX(false);
|
|
744
|
+
} else {
|
|
745
|
+
this.rotateX(true);
|
|
746
|
+
this.rotateF(false);
|
|
747
|
+
this.rotateX(false);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
rotateDw(clockwise = true) {
|
|
752
|
+
if (this.size === 2) return;
|
|
753
|
+
if (clockwise) {
|
|
754
|
+
this.rotateY(false);
|
|
755
|
+
this.rotateU(true);
|
|
756
|
+
} else {
|
|
757
|
+
this.rotateY(true);
|
|
758
|
+
this.rotateU(false);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
rotateUw(clockwise = true) {
|
|
763
|
+
if (this.size === 2) return;
|
|
764
|
+
if (clockwise) {
|
|
765
|
+
this.rotateY(true);
|
|
766
|
+
this.rotateD(true);
|
|
767
|
+
} else {
|
|
768
|
+
this.rotateY(false);
|
|
769
|
+
this.rotateD(false);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
rotateRw(clockwise = true) {
|
|
774
|
+
if (this.size === 2) return;
|
|
775
|
+
if (clockwise) {
|
|
776
|
+
this.rotateX(true);
|
|
777
|
+
this.rotateL(true);
|
|
778
|
+
} else {
|
|
779
|
+
this.rotateX(false);
|
|
780
|
+
this.rotateL(false);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
rotateLw(clockwise = true) {
|
|
785
|
+
if (this.size === 2) return;
|
|
786
|
+
if (clockwise) {
|
|
787
|
+
this.rotateX(false);
|
|
788
|
+
this.rotateR(true);
|
|
789
|
+
} else {
|
|
790
|
+
this.rotateX(true);
|
|
791
|
+
this.rotateR(false);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
rotateM(clockwise = true) {
|
|
796
|
+
if (this.size === 2) return;
|
|
797
|
+
if (clockwise) {
|
|
798
|
+
this.rotateLw(true);
|
|
799
|
+
this.rotateL(false);
|
|
800
|
+
} else {
|
|
801
|
+
this.rotateLw(false);
|
|
802
|
+
this.rotateL(true);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
rotateE(clockwise = true) {
|
|
807
|
+
if (this.size === 2) return;
|
|
808
|
+
if (clockwise) {
|
|
809
|
+
this.rotateDw(true);
|
|
810
|
+
this.rotateD(false);
|
|
811
|
+
} else {
|
|
812
|
+
this.rotateDw(false);
|
|
813
|
+
this.rotateD(true);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
rotateFw(clockwise = true) {
|
|
818
|
+
if (this.size === 2) return;
|
|
819
|
+
if (clockwise) {
|
|
820
|
+
this.rotateZ(true);
|
|
821
|
+
this.rotateB(true);
|
|
822
|
+
} else {
|
|
823
|
+
this.rotateZ(false);
|
|
824
|
+
this.rotateB(false);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
rotateS(clockwise = true) {
|
|
829
|
+
if (this.size === 2) return;
|
|
830
|
+
if (clockwise) {
|
|
831
|
+
this.rotateFw(true);
|
|
832
|
+
this.rotateF(false);
|
|
833
|
+
} else {
|
|
834
|
+
this.rotateFw(false);
|
|
835
|
+
this.rotateF(true);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
rotateX(clockwise = true) {
|
|
840
|
+
const tempFront = structuredClone(this.STATES.FRONT);
|
|
841
|
+
const tempDown = structuredClone(this.STATES.DOWN);
|
|
842
|
+
const tempUpper = structuredClone(this.STATES.UPPER);
|
|
843
|
+
const tempBack = structuredClone(this.STATES.BACK);
|
|
844
|
+
const tempLeft = structuredClone(this.STATES.LEFT);
|
|
845
|
+
const tempRight = structuredClone(this.STATES.RIGHT);
|
|
846
|
+
|
|
847
|
+
if (clockwise) {
|
|
848
|
+
this.STATES.LEFT = this.switchMatrix(tempLeft, false);
|
|
849
|
+
this.STATES.RIGHT = this.switchMatrix(tempRight, true);
|
|
850
|
+
|
|
851
|
+
this.STATES.FRONT = [...tempDown];
|
|
852
|
+
this.STATES.UPPER = [...tempFront];
|
|
853
|
+
|
|
854
|
+
this.STATES.BACK = this.specialFlip(tempUpper);
|
|
855
|
+
this.STATES.DOWN = this.specialFlip(tempBack);
|
|
856
|
+
} else {
|
|
857
|
+
this.STATES.LEFT = this.switchMatrix(tempLeft, true);
|
|
858
|
+
this.STATES.RIGHT = this.switchMatrix(tempRight, false);
|
|
859
|
+
|
|
860
|
+
this.STATES.FRONT = [...tempUpper];
|
|
861
|
+
this.STATES.DOWN = [...tempFront];
|
|
862
|
+
|
|
863
|
+
this.STATES.BACK = this.specialFlip(tempDown);
|
|
864
|
+
this.STATES.UPPER = this.specialFlip(tempBack);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
rotateZ(clockwise = true) {
|
|
869
|
+
const tempUpper = structuredClone(this.STATES.UPPER);
|
|
870
|
+
const tempRight = structuredClone(this.STATES.RIGHT);
|
|
871
|
+
const tempDown = structuredClone(this.STATES.DOWN);
|
|
872
|
+
const tempLeft = structuredClone(this.STATES.LEFT);
|
|
873
|
+
const tempFront = structuredClone(this.STATES.FRONT);
|
|
874
|
+
const tempBack = structuredClone(this.STATES.BACK);
|
|
875
|
+
|
|
876
|
+
if (clockwise) {
|
|
877
|
+
this.STATES.FRONT = this.switchMatrix(tempFront, true);
|
|
878
|
+
this.STATES.BACK = this.switchMatrix(tempBack, false);
|
|
879
|
+
|
|
880
|
+
this.STATES.RIGHT = this.switchMatrix(tempUpper, true);
|
|
881
|
+
this.STATES.DOWN = this.switchMatrix(tempRight, true);
|
|
882
|
+
this.STATES.LEFT = this.switchMatrix(tempDown, true);
|
|
883
|
+
this.STATES.UPPER = this.switchMatrix(tempLeft, true);
|
|
884
|
+
} else {
|
|
885
|
+
this.STATES.FRONT = this.switchMatrix(tempFront, false);
|
|
886
|
+
this.STATES.BACK = this.switchMatrix(tempBack, true);
|
|
887
|
+
|
|
888
|
+
this.STATES.RIGHT = this.switchMatrix(tempDown, false);
|
|
889
|
+
this.STATES.DOWN = this.switchMatrix(tempLeft, false);
|
|
890
|
+
this.STATES.LEFT = this.switchMatrix(tempUpper, false);
|
|
891
|
+
this.STATES.UPPER = this.switchMatrix(tempRight, false);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
rotateY(clockwise = true) {
|
|
896
|
+
const tempFront = structuredClone(this.STATES.FRONT);
|
|
897
|
+
const tempRight = structuredClone(this.STATES.RIGHT);
|
|
898
|
+
const tempBack = structuredClone(this.STATES.BACK);
|
|
899
|
+
const tempLeft = structuredClone(this.STATES.LEFT);
|
|
900
|
+
|
|
901
|
+
if (clockwise) {
|
|
902
|
+
this.STATES.UPPER = this.switchMatrix(this.STATES.UPPER, true);
|
|
903
|
+
this.STATES.DOWN = this.switchMatrix(this.STATES.DOWN, false);
|
|
904
|
+
|
|
905
|
+
this.STATES.FRONT = [...tempRight];
|
|
906
|
+
this.STATES.RIGHT = [...tempBack];
|
|
907
|
+
this.STATES.LEFT = [...tempFront];
|
|
908
|
+
this.STATES.BACK = [...tempLeft];
|
|
909
|
+
} else {
|
|
910
|
+
this.STATES.UPPER = this.switchMatrix(this.STATES.UPPER, false);
|
|
911
|
+
this.STATES.DOWN = this.switchMatrix(this.STATES.DOWN, true);
|
|
912
|
+
|
|
913
|
+
this.STATES.FRONT = [...tempLeft];
|
|
914
|
+
this.STATES.RIGHT = [...tempFront];
|
|
915
|
+
this.STATES.LEFT = [...tempBack];
|
|
916
|
+
this.STATES.BACK = [...tempRight];
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Build a single move's permutation by applying it to a tagged oracle cube.
|
|
922
|
+
function buildPerm(size, fnName, clockwise) {
|
|
923
|
+
const oracle = new _OracleCube(size);
|
|
924
|
+
oracle[fnName](clockwise);
|
|
925
|
+
return oracle.flatten();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Build (and cache) the full clockwise/counterclockwise permutation set for a size.
|
|
929
|
+
function getPerms(size) {
|
|
930
|
+
if (PERM_CACHE.has(size)) return PERM_CACHE.get(size);
|
|
931
|
+
const perms = {};
|
|
932
|
+
for (const key of Object.keys(MOVE_FNS)) {
|
|
933
|
+
perms[key] = {
|
|
934
|
+
cw: buildPerm(size, MOVE_FNS[key], true),
|
|
935
|
+
ccw: buildPerm(size, MOVE_FNS[key], false),
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
PERM_CACHE.set(size, perms);
|
|
939
|
+
return perms;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Exposes the (cached) permutation tables for a given cube size.
|
|
944
|
+
*
|
|
945
|
+
* Each entry maps a move key to `{ cw, ccw }` permutation arrays such that
|
|
946
|
+
* `newState[i] = oldState[perm[i]]`. This is primarily an advanced/introspection
|
|
947
|
+
* helper: the solution analyzer uses it to derive the cube's sticker adjacency
|
|
948
|
+
* (which stickers form each edge/corner) without hardcoding any geometry.
|
|
949
|
+
*
|
|
950
|
+
* @param {number} size - 2 or 3 (defaults to 3).
|
|
951
|
+
* @returns {Object<string, {cw: number[], ccw: number[]}>}
|
|
952
|
+
*/
|
|
953
|
+
function getMovePermutations(size = 3) {
|
|
954
|
+
const allowedSizes = [2, 3];
|
|
955
|
+
return getPerms(allowedSizes.includes(size) ? size : 3);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
class CubeEngine {
|
|
959
|
+
MOVES = [];
|
|
960
|
+
size = 3;
|
|
961
|
+
#stickers = [];
|
|
962
|
+
#perms = null;
|
|
963
|
+
|
|
964
|
+
constructor(initialScramble = "", options = { size: 3 }) {
|
|
965
|
+
const allowedSizes = [2, 3];
|
|
966
|
+
this.size = allowedSizes.includes(options.size) ? options.size : 3;
|
|
967
|
+
this.#perms = getPerms(this.size);
|
|
968
|
+
|
|
969
|
+
this.#initializeState();
|
|
970
|
+
|
|
971
|
+
// If an initial scramble string is provided, apply it without recording moves
|
|
972
|
+
if (typeof initialScramble === "string" && initialScramble.trim().length > 0) {
|
|
973
|
+
this.#applyMovesFromString(initialScramble, false);
|
|
974
|
+
this.MOVES = [];
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
#initializeState() {
|
|
979
|
+
const per = this.size * this.size;
|
|
980
|
+
const stickers = new Array(FACE_COLORS.length * per);
|
|
981
|
+
for (let f = 0; f < FACE_COLORS.length; f++) {
|
|
982
|
+
const color = FACE_COLORS[f];
|
|
983
|
+
const base = f * per;
|
|
984
|
+
for (let i = 0; i < per; i++) {
|
|
985
|
+
stickers[base + i] = color;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
this.#stickers = stickers;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Apply a precomputed permutation: newState[i] = oldState[perm[i]].
|
|
992
|
+
#applyPerm(perm) {
|
|
993
|
+
const current = this.#stickers;
|
|
994
|
+
const next = new Array(current.length);
|
|
995
|
+
for (let i = 0; i < current.length; i++) {
|
|
996
|
+
next[i] = current[perm[i]];
|
|
997
|
+
}
|
|
998
|
+
this.#stickers = next;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Core move dispatch. dir is "cw" or "ccw"; record controls history logging.
|
|
1002
|
+
#apply(key, dir, record) {
|
|
1003
|
+
if (this.size === 2 && NOOP_SIZE2.has(key)) return;
|
|
1004
|
+
this.#applyPerm(this.#perms[key][dir]);
|
|
1005
|
+
if (record) this.MOVES.push(dir === "ccw" ? key + "'" : key);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Build a single face matrix from the flat sticker array.
|
|
1009
|
+
#faceMatrix(faceIndex) {
|
|
1010
|
+
const size = this.size;
|
|
1011
|
+
const base = faceIndex * size * size;
|
|
1012
|
+
const matrix = [];
|
|
1013
|
+
for (let r = 0; r < size; r++) {
|
|
1014
|
+
const row = [];
|
|
1015
|
+
for (let c = 0; c < size; c++) {
|
|
1016
|
+
row.push(this.#stickers[base + r * size + c]);
|
|
1017
|
+
}
|
|
1018
|
+
matrix.push(row);
|
|
1019
|
+
}
|
|
1020
|
+
return matrix;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Rotates the (UPPER) layer clockwise or counterclockwise.
|
|
1025
|
+
*/
|
|
1026
|
+
rotateU(clockwise = true) {
|
|
1027
|
+
this.#apply("U", clockwise ? "cw" : "ccw", true);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Rotates the (FRONT) layer clockwise or counterclockwise.
|
|
1032
|
+
*/
|
|
1033
|
+
rotateF(clockwise = true) {
|
|
1034
|
+
this.#apply("F", clockwise ? "cw" : "ccw", true);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Rotates the (BACK) layer clockwise or counterclockwise.
|
|
1039
|
+
*/
|
|
1040
|
+
rotateB(clockwise = true) {
|
|
1041
|
+
this.#apply("B", clockwise ? "cw" : "ccw", true);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Rotates the (RIGHT) layer clockwise or counterclockwise.
|
|
1046
|
+
*/
|
|
1047
|
+
rotateR(clockwise = true) {
|
|
1048
|
+
this.#apply("R", clockwise ? "cw" : "ccw", true);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Rotates the (LEFT) layer clockwise or counterclockwise.
|
|
1053
|
+
*/
|
|
1054
|
+
rotateL(clockwise = true) {
|
|
1055
|
+
this.#apply("L", clockwise ? "cw" : "ccw", true);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Rotates the (DOWN) layer clockwise or counterclockwise.
|
|
1060
|
+
*/
|
|
1061
|
+
rotateD(clockwise = true) {
|
|
1062
|
+
this.#apply("D", clockwise ? "cw" : "ccw", true);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Rotates the wide (DOWN two layers) clockwise or counterclockwise.
|
|
1067
|
+
*/
|
|
1068
|
+
rotateDw(clockwise = true) {
|
|
1069
|
+
this.#apply("Dw", clockwise ? "cw" : "ccw", true);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Rotates the wide (UPPER two layers) clockwise or counterclockwise.
|
|
1074
|
+
*/
|
|
1075
|
+
rotateUw(clockwise = true) {
|
|
1076
|
+
this.#apply("Uw", clockwise ? "cw" : "ccw", true);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Rotates the wide (RIGHT two layers) clockwise or counterclockwise.
|
|
1081
|
+
*/
|
|
1082
|
+
rotateRw(clockwise = true) {
|
|
1083
|
+
this.#apply("Rw", clockwise ? "cw" : "ccw", true);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Rotates the wide (LEFT two layers) clockwise or counterclockwise.
|
|
1088
|
+
*/
|
|
1089
|
+
rotateLw(clockwise = true) {
|
|
1090
|
+
this.#apply("Lw", clockwise ? "cw" : "ccw", true);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Rotates the middle slice (M) parallel to L/R. Clockwise corresponds to Lw followed by L'.
|
|
1095
|
+
*/
|
|
1096
|
+
rotateM(clockwise = true) {
|
|
1097
|
+
this.#apply("M", clockwise ? "cw" : "ccw", true);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Rotates the equatorial slice (E) parallel to U/D. Clockwise follows the D direction (E = Dw D').
|
|
1102
|
+
*/
|
|
1103
|
+
rotateE(clockwise = true) {
|
|
1104
|
+
this.#apply("E", clockwise ? "cw" : "ccw", true);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Rotates the wide (FRONT two layers) clockwise or counterclockwise. Equivalent to z B.
|
|
1109
|
+
*/
|
|
1110
|
+
rotateFw(clockwise = true) {
|
|
1111
|
+
this.#apply("Fw", clockwise ? "cw" : "ccw", true);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Rotates the standing slice (S) parallel to F/B. Clockwise follows the F direction (S = Fw F').
|
|
1116
|
+
*/
|
|
1117
|
+
rotateS(clockwise = true) {
|
|
1118
|
+
this.#apply("S", clockwise ? "cw" : "ccw", true);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Rotates the (x) axis clockwise or counterclockwise.
|
|
1123
|
+
*/
|
|
1124
|
+
rotateX(clockwise = true) {
|
|
1125
|
+
this.#apply("x", clockwise ? "cw" : "ccw", true);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Rotates the (z) axis clockwise or counterclockwise.
|
|
1130
|
+
*/
|
|
1131
|
+
rotateZ(clockwise = true) {
|
|
1132
|
+
this.#apply("z", clockwise ? "cw" : "ccw", true);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Rotates the (y) axis clockwise or counterclockwise.
|
|
1137
|
+
*/
|
|
1138
|
+
rotateY(clockwise = true) {
|
|
1139
|
+
this.#apply("y", clockwise ? "cw" : "ccw", true);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Logs the current state of the cube.
|
|
1144
|
+
*/
|
|
1145
|
+
state() {
|
|
1146
|
+
return {
|
|
1147
|
+
UPPER: this.#faceMatrix(0),
|
|
1148
|
+
LEFT: this.#faceMatrix(1),
|
|
1149
|
+
FRONT: this.#faceMatrix(2),
|
|
1150
|
+
RIGHT: this.#faceMatrix(3),
|
|
1151
|
+
BACK: this.#faceMatrix(4),
|
|
1152
|
+
DOWN: this.#faceMatrix(5),
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Indicates if the cube is solve or not in all layers.
|
|
1158
|
+
*/
|
|
1159
|
+
isSolved() {
|
|
1160
|
+
const per = this.size * this.size;
|
|
1161
|
+
// 2x2 has no center, so use the first sticker; 3x3 uses the center (index 4).
|
|
1162
|
+
const centerOffset = this.size === 2 ? 0 : 4;
|
|
1163
|
+
for (let f = 0; f < FACE_COLORS.length; f++) {
|
|
1164
|
+
const base = f * per;
|
|
1165
|
+
const centerColor = this.#stickers[base + centerOffset];
|
|
1166
|
+
for (let i = 0; i < per; i++) {
|
|
1167
|
+
if (this.#stickers[base + i] !== centerColor) return false;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return true;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Returns the history of all movements made.
|
|
1175
|
+
*
|
|
1176
|
+
* @param {boolean} asString - If true, returns the history as a string; otherwise, returns it as an array.
|
|
1177
|
+
* @returns {string|array} The history of movements as an array or string.
|
|
1178
|
+
*/
|
|
1179
|
+
getMoves(asString = true) {
|
|
1180
|
+
return asString ? this.MOVES.join(" ") : this.MOVES;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Resets the cube to the solved state and clears the move history.
|
|
1185
|
+
*/
|
|
1186
|
+
reset() {
|
|
1187
|
+
this.#initializeState();
|
|
1188
|
+
this.MOVES = [];
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Applies a sequence of moves provided as a string.
|
|
1193
|
+
* Supports: U, D, L, R, F, B, x, y, z; slice moves: M, E, S; and wide moves: Dw, Uw, Rw, Lw, Fw with optional ' for counterclockwise and 2 for double turns.
|
|
1194
|
+
* @param {string} sequence - e.g. "R U' F R2 D Dw Uw Rw Rw' Lw Lw2 M M' M2 E E' S S2 Fw"
|
|
1195
|
+
* @param {object} options - { record: boolean } whether to record moves in history (default true)
|
|
1196
|
+
*/
|
|
1197
|
+
applyMoves(sequence, options = { record: false }) {
|
|
1198
|
+
const record = options?.record !== false;
|
|
1199
|
+
this.#applyMovesFromString(sequence, record);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Internal: parses and applies moves, optionally recording them in history.
|
|
1203
|
+
#applyMovesFromString(sequence, record = true) {
|
|
1204
|
+
if (typeof sequence !== "string") return;
|
|
1205
|
+
const tokens = sequence
|
|
1206
|
+
.split(/\s+/)
|
|
1207
|
+
.map((t) => t.trim())
|
|
1208
|
+
.filter((t) => t.length > 0);
|
|
1209
|
+
|
|
1210
|
+
for (const token of tokens) {
|
|
1211
|
+
const base = token[0];
|
|
1212
|
+
const rest = token.slice(1);
|
|
1213
|
+
const isDouble = rest.includes("2");
|
|
1214
|
+
const isPrime = rest.includes("'");
|
|
1215
|
+
const isWide = /w/i.test(rest);
|
|
1216
|
+
|
|
1217
|
+
let key;
|
|
1218
|
+
switch (base) {
|
|
1219
|
+
case "U":
|
|
1220
|
+
key = isWide ? "Uw" : "U";
|
|
1221
|
+
break;
|
|
1222
|
+
case "D":
|
|
1223
|
+
key = isWide ? "Dw" : "D";
|
|
1224
|
+
break;
|
|
1225
|
+
case "L":
|
|
1226
|
+
key = isWide ? "Lw" : "L";
|
|
1227
|
+
break;
|
|
1228
|
+
case "R":
|
|
1229
|
+
key = isWide ? "Rw" : "R";
|
|
1230
|
+
break;
|
|
1231
|
+
case "F":
|
|
1232
|
+
key = isWide ? "Fw" : "F";
|
|
1233
|
+
break;
|
|
1234
|
+
case "B":
|
|
1235
|
+
key = "B";
|
|
1236
|
+
break;
|
|
1237
|
+
case "x":
|
|
1238
|
+
key = "x";
|
|
1239
|
+
break;
|
|
1240
|
+
case "y":
|
|
1241
|
+
key = "y";
|
|
1242
|
+
break;
|
|
1243
|
+
case "z":
|
|
1244
|
+
key = "z";
|
|
1245
|
+
break;
|
|
1246
|
+
case "M":
|
|
1247
|
+
key = "M";
|
|
1248
|
+
break;
|
|
1249
|
+
case "E":
|
|
1250
|
+
key = "E";
|
|
1251
|
+
break;
|
|
1252
|
+
case "S":
|
|
1253
|
+
key = "S";
|
|
1254
|
+
break;
|
|
1255
|
+
default:
|
|
1256
|
+
// Unsupported token. Ignore silently for now.
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (isDouble) {
|
|
1261
|
+
this.#apply(key, "cw", record);
|
|
1262
|
+
this.#apply(key, "cw", record);
|
|
1263
|
+
} else if (isPrime) {
|
|
1264
|
+
this.#apply(key, "ccw", record);
|
|
1265
|
+
} else {
|
|
1266
|
+
this.#apply(key, "cw", record);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const COLOR = {
|
|
1273
|
+
W: ["W", "W", "W", "W", "W", "W", "W", "W", "W"],
|
|
1274
|
+
G: ["G", "G", "G", "G", "G", "G", "G", "G", "G"],
|
|
1275
|
+
R: ["R", "R", "R", "R", "R", "R", "R", "R", "R"],
|
|
1276
|
+
B: ["B", "B", "B", "B", "B", "B", "B", "B", "B"],
|
|
1277
|
+
O: ["O", "O", "O", "O", "O", "O", "O", "O", "O"],
|
|
1278
|
+
Y: ["Y", "Y", "Y", "Y", "Y", "Y", "Y", "Y", "Y"],
|
|
734
1279
|
};
|
|
735
1280
|
|
|
736
|
-
export { COLOR, CubeEngine };
|
|
1281
|
+
export { COLOR, CubeEngine, analyzeSolution, getMovePermutations, invertSequence, simplifyMoves };
|