@cubingcenter/scrambler 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/puzzles/cube2/solver/search.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/search.js +3 -3
- package/dist/puzzles/cube2/solver/tables.d.ts +6 -0
- package/dist/puzzles/cube2/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/tables.js +16 -2
- package/dist/puzzles/cube3/index.d.ts +1 -0
- package/dist/puzzles/cube3/index.d.ts.map +1 -1
- package/dist/puzzles/cube3/moves.d.ts.map +1 -1
- package/dist/puzzles/cube3/moves.js +104 -18
- package/dist/puzzles/cube3/scrambler.d.ts +16 -1
- package/dist/puzzles/cube3/scrambler.d.ts.map +1 -1
- package/dist/puzzles/cube3/scrambler.js +1 -1
- package/dist/puzzles/cube3/solver/browser-loader.js +6 -2
- package/dist/puzzles/cube3/solver/index.d.ts +5 -3
- package/dist/puzzles/cube3/solver/index.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/index.js +2 -1
- package/dist/puzzles/cube3/solver/node-loader.js +9 -2
- package/dist/puzzles/cube3/solver/phase1.d.ts +18 -0
- package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase1.js +132 -14
- package/dist/puzzles/cube3/solver/phase2.d.ts +47 -0
- package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase2.js +236 -66
- package/dist/puzzles/cube3/solver/search-utils.d.ts +1 -1
- package/dist/puzzles/cube3/solver/search-utils.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/symmetry.d.ts +24 -0
- package/dist/puzzles/cube3/solver/symmetry.d.ts.map +1 -0
- package/dist/puzzles/cube3/solver/symmetry.js +74 -0
- package/dist/puzzles/cube3/solver/tables.d.ts +6 -1
- package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/cube3/validation.d.ts.map +1 -1
- package/dist/puzzles/cube3/validation.js +15 -7
- package/dist/puzzles/cube4/index.d.ts +3 -2
- package/dist/puzzles/cube4/index.d.ts.map +1 -1
- package/dist/puzzles/cube4/index.js +2 -2
- package/dist/puzzles/cube4/moves.d.ts +5 -0
- package/dist/puzzles/cube4/moves.d.ts.map +1 -1
- package/dist/puzzles/cube4/moves.js +120 -11
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts +3 -0
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/browser-loader.js +34 -3
- package/dist/puzzles/cube4/reduction/center2.d.ts +20 -3
- package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/center2.js +53 -9
- package/dist/puzzles/cube4/reduction/edge3.d.ts +4 -0
- package/dist/puzzles/cube4/reduction/edge3.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/edge3.js +23 -3
- package/dist/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
- package/dist/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
- package/dist/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
- package/dist/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
- package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +38 -0
- package/dist/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +50 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.js +202 -0
- package/dist/puzzles/cube4/reduction/node-loader.d.ts +5 -0
- package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/node-loader.js +48 -8
- package/dist/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts +111 -5
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/phase3-search.js +341 -22
- package/dist/puzzles/cube4/reduction/solve.d.ts +112 -1
- package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/solve.js +920 -181
- package/dist/puzzles/cube4/scrambler.d.ts +105 -4
- package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
- package/dist/puzzles/cube4/scrambler.js +216 -166
- package/dist/puzzles/cube4/state.d.ts.map +1 -1
- package/dist/puzzles/cube4/state.js +4 -1
- package/dist/puzzles/pyraminx/solver/search.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/search.js +3 -6
- package/dist/puzzles/pyraminx/solver/tables.d.ts +6 -0
- package/dist/puzzles/pyraminx/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/tables.js +17 -0
- package/dist/puzzles/skewb/solver/search.js +3 -3
- package/dist/puzzles/skewb/solver/tables.d.ts +6 -0
- package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/skewb/solver/tables.js +17 -0
- package/dist-cjs/puzzles/cube2/solver/search.js +2 -2
- package/dist-cjs/puzzles/cube2/solver/tables.js +17 -3
- package/dist-cjs/puzzles/cube3/moves.js +104 -18
- package/dist-cjs/puzzles/cube3/scrambler.js +1 -1
- package/dist-cjs/puzzles/cube3/solver/browser-loader.js +6 -2
- package/dist-cjs/puzzles/cube3/solver/index.js +8 -1
- package/dist-cjs/puzzles/cube3/solver/node-loader.js +9 -2
- package/dist-cjs/puzzles/cube3/solver/phase1.js +133 -14
- package/dist-cjs/puzzles/cube3/solver/phase2.js +233 -63
- package/dist-cjs/puzzles/cube3/solver/symmetry.js +80 -0
- package/dist-cjs/puzzles/cube3/validation.js +15 -7
- package/dist-cjs/puzzles/cube4/index.js +3 -1
- package/dist-cjs/puzzles/cube4/moves.js +121 -11
- package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +34 -3
- package/dist-cjs/puzzles/cube4/reduction/center2.js +54 -9
- package/dist-cjs/puzzles/cube4/reduction/edge3.js +24 -3
- package/dist-cjs/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +38 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
- package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +213 -0
- package/dist-cjs/puzzles/cube4/reduction/node-loader.js +48 -8
- package/dist-cjs/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
- package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +343 -23
- package/dist-cjs/puzzles/cube4/reduction/solve.js +918 -173
- package/dist-cjs/puzzles/cube4/scrambler.js +214 -163
- package/dist-cjs/puzzles/cube4/state.js +4 -1
- package/dist-cjs/puzzles/pyraminx/solver/search.js +2 -5
- package/dist-cjs/puzzles/pyraminx/solver/tables.js +18 -1
- package/dist-cjs/puzzles/skewb/solver/search.js +2 -2
- package/dist-cjs/puzzles/skewb/solver/tables.js +18 -1
- package/package.json +14 -5
|
@@ -13,6 +13,7 @@ const phase1_js_1 = require("./phase1.js");
|
|
|
13
13
|
const tables_js_1 = require("./tables.js");
|
|
14
14
|
const search_utils_js_1 = require("./search-utils.js");
|
|
15
15
|
const pair_tables_js_1 = require("./pair-tables.js");
|
|
16
|
+
const symmetry_js_1 = require("./symmetry.js");
|
|
16
17
|
class Cube3Phase2SearchError extends Error {
|
|
17
18
|
visitedNodes;
|
|
18
19
|
constructor(message, visitedNodes) {
|
|
@@ -50,17 +51,11 @@ async function solveCube3Phase2(state, options = {}) {
|
|
|
50
51
|
throw new Cube3Phase2SearchError("3x3 Phase 2 requires a state already in G1.", 0);
|
|
51
52
|
}
|
|
52
53
|
const tables = options.tables ?? await (0, tables_js_1.loadCube3Phase2Tables)();
|
|
53
|
-
const pairTables = options.pairTables ?? await (0, pair_tables_js_1.loadCube3PairTables)();
|
|
54
|
+
const pairTables = options.pairTables ?? (options.skipPairTables ? undefined : await (0, pair_tables_js_1.loadCube3PairTables)());
|
|
54
55
|
const maxDepth = options.maxDepth ?? 18;
|
|
55
56
|
const start = encodeCube3Phase2Coordinates(state);
|
|
56
57
|
const deadline = options.timeoutMs === undefined ? undefined : performance.now() + options.timeoutMs;
|
|
57
|
-
const context =
|
|
58
|
-
tables,
|
|
59
|
-
pairTables,
|
|
60
|
-
deadline,
|
|
61
|
-
visitedNodes: 0,
|
|
62
|
-
timedOut: false
|
|
63
|
-
};
|
|
58
|
+
const context = createPhase2SearchContext(tables, pairTables, maxDepth, deadline);
|
|
64
59
|
const spRange = tables.moveTables.slicePermutation.range;
|
|
65
60
|
const cpspTable = tables.pruningTables.cornerPermutationSlicePermutation.values;
|
|
66
61
|
const epspTable = tables.pruningTables.udEdgePermutationSlicePermutation.values;
|
|
@@ -98,6 +93,11 @@ async function solveCube3Phase2(state, options = {}) {
|
|
|
98
93
|
throw new Cube3Phase2SearchError(`3x3 Phase 2 solution not found within depth ${maxDepth}.`, context.visitedNodes);
|
|
99
94
|
}
|
|
100
95
|
async function solveCube3State(state, options = {}) {
|
|
96
|
+
const quality = options.quality ?? "short";
|
|
97
|
+
const orientations = options.orientations ?? (quality === "best" ? 3 : 1);
|
|
98
|
+
if (orientations >= 3) {
|
|
99
|
+
return solveCube3TripleOrientation(state, { ...options, orientations });
|
|
100
|
+
}
|
|
101
101
|
const phase1 = await (0, phase1_js_1.solveCube3Phase1)(state, options.phase1);
|
|
102
102
|
let previousFace = -1;
|
|
103
103
|
let previousPreviousFace = -1;
|
|
@@ -116,6 +116,127 @@ async function solveCube3State(state, options = {}) {
|
|
|
116
116
|
};
|
|
117
117
|
const phase2 = await solveCube3Phase2(phase1.state, phase2Options);
|
|
118
118
|
const moves = [...phase1.moves, ...phase2.moves];
|
|
119
|
+
const targetLength = options.targetLength;
|
|
120
|
+
const extraDepth = options.phase1ExtraDepth ?? (quality === "fast" ? 0 : 1);
|
|
121
|
+
const solutionLimit = options.phase1SolutionLimit ?? (targetLength !== undefined ? 32 : 8);
|
|
122
|
+
const improveDepths = targetLength !== undefined ? Math.max(4, extraDepth) : extraDepth;
|
|
123
|
+
const needsTarget = targetLength !== undefined && moves.length > targetLength;
|
|
124
|
+
if (needsTarget || (targetLength === undefined && extraDepth > 0)) {
|
|
125
|
+
const p1Tables = options.phase1?.tables ?? await (0, tables_js_1.loadCube3Phase1Tables)();
|
|
126
|
+
const p2Tables = options.phase2?.tables ?? await (0, tables_js_1.loadCube3Phase2Tables)();
|
|
127
|
+
const scoredPairTables = options.phase2?.pairTables ??
|
|
128
|
+
(options.phase2?.skipPairTables ? undefined : await (0, pair_tables_js_1.loadCube3PairTables)());
|
|
129
|
+
const deadline = performance.now() + (options.timeoutMs ?? 800);
|
|
130
|
+
const p2SpRange = p2Tables.moveTables.slicePermutation.range;
|
|
131
|
+
const p2cpsp = p2Tables.pruningTables.cornerPermutationSlicePermutation.values;
|
|
132
|
+
const p2uepsp = p2Tables.pruningTables.udEdgePermutationSlicePermutation.values;
|
|
133
|
+
const seenG1 = new Set();
|
|
134
|
+
let bestP1 = phase1;
|
|
135
|
+
let bestP2 = phase2;
|
|
136
|
+
let bestMoves = moves;
|
|
137
|
+
let improvementNodes = 0;
|
|
138
|
+
const minDepth = phase1.depth;
|
|
139
|
+
const scored = [];
|
|
140
|
+
for (let d = minDepth; d <= minDepth + improveDepths && d < bestMoves.length; d++) {
|
|
141
|
+
if (performance.now() > deadline)
|
|
142
|
+
break;
|
|
143
|
+
const { solutions: p1Solutions, visitedNodes } = await (0, phase1_js_1.collectCube3Phase1Solutions)(state, d, {
|
|
144
|
+
tables: p1Tables,
|
|
145
|
+
limit: solutionLimit,
|
|
146
|
+
timeoutMs: Math.max(1, deadline - performance.now())
|
|
147
|
+
});
|
|
148
|
+
improvementNodes += visitedNodes;
|
|
149
|
+
if (performance.now() > deadline)
|
|
150
|
+
break;
|
|
151
|
+
for (const p1Moves of p1Solutions) {
|
|
152
|
+
if (performance.now() > deadline)
|
|
153
|
+
break;
|
|
154
|
+
if (p1Moves.length >= bestMoves.length)
|
|
155
|
+
continue;
|
|
156
|
+
// Skip the phase-1 solution the initial search already completed.
|
|
157
|
+
if (p1Moves.length === phase1.moves.length &&
|
|
158
|
+
p1Moves.every((move, index) => move === phase1.moves[index])) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const s1 = (0, moves_js_1.applyCube3Algorithm)(state, p1Moves);
|
|
162
|
+
const coords = encodeCube3Phase2Coordinates(s1);
|
|
163
|
+
const estKey = `${coords.cornerPermutation},${coords.udEdgePermutation},${coords.slicePermutation}`;
|
|
164
|
+
if (seenG1.has(estKey))
|
|
165
|
+
continue;
|
|
166
|
+
seenG1.add(estKey);
|
|
167
|
+
const cpIndex = coords.cornerPermutation * p2SpRange + coords.slicePermutation;
|
|
168
|
+
const epIndex = coords.udEdgePermutation * p2SpRange + coords.slicePermutation;
|
|
169
|
+
const cpDist = (p2cpsp[cpIndex >> 1] >> ((cpIndex & 1) << 2)) & 0x0f;
|
|
170
|
+
const epDist = (p2uepsp[epIndex >> 1] >> ((epIndex & 1) << 2)) & 0x0f;
|
|
171
|
+
let est = p1Moves.length + (cpDist > epDist ? cpDist : epDist);
|
|
172
|
+
if (scoredPairTables !== undefined) {
|
|
173
|
+
const pair = (0, pair_tables_js_1.pairBound)(scoredPairTables, coords.cornerPermutation, coords.udEdgePermutation);
|
|
174
|
+
const pairEst = p1Moves.length + pair;
|
|
175
|
+
if (pairEst > est)
|
|
176
|
+
est = pairEst;
|
|
177
|
+
}
|
|
178
|
+
// A lower bound at or above the best total can never strictly improve it.
|
|
179
|
+
if (est >= bestMoves.length)
|
|
180
|
+
continue;
|
|
181
|
+
const pFace = p1Moves.length > 0 ? (0, search_utils_js_1.getMoveFace)(p1Moves[p1Moves.length - 1]) : -1;
|
|
182
|
+
const ppFace = p1Moves.length > 1 ? (0, search_utils_js_1.getMoveFace)(p1Moves[p1Moves.length - 2]) : -1;
|
|
183
|
+
scored.push({ moves: p1Moves, state: s1, est, pFace, ppFace });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Sort ALL candidates globally across depths by admissible total lower bound
|
|
187
|
+
scored.sort((left, right) => left.est - right.est);
|
|
188
|
+
for (const cand of scored) {
|
|
189
|
+
if (performance.now() > deadline)
|
|
190
|
+
break;
|
|
191
|
+
// Prune: since candidates are sorted by est, no subsequent candidate can beat bestMoves.length
|
|
192
|
+
if (cand.est >= bestMoves.length)
|
|
193
|
+
break;
|
|
194
|
+
const maxP2 = Math.min(bestMoves.length - cand.moves.length - 1, 15);
|
|
195
|
+
if (maxP2 <= 0)
|
|
196
|
+
continue;
|
|
197
|
+
try {
|
|
198
|
+
const remainingBudget = Math.max(1, deadline - performance.now());
|
|
199
|
+
const attemptTimeout = options.improvePhase2TimeoutMs ??
|
|
200
|
+
options.phase2?.timeoutMs ?? 25;
|
|
201
|
+
const candP2 = await solveCube3Phase2(cand.state, {
|
|
202
|
+
...options.phase2,
|
|
203
|
+
pairTables: scoredPairTables,
|
|
204
|
+
tables: p2Tables,
|
|
205
|
+
maxDepth: maxP2,
|
|
206
|
+
previousFace: cand.pFace,
|
|
207
|
+
previousPreviousFace: cand.ppFace,
|
|
208
|
+
timeoutMs: Math.min(attemptTimeout, remainingBudget)
|
|
209
|
+
});
|
|
210
|
+
improvementNodes += candP2.visitedNodes;
|
|
211
|
+
const candTotal = cand.moves.length + candP2.moves.length;
|
|
212
|
+
if (candTotal < bestMoves.length) {
|
|
213
|
+
bestMoves = [...cand.moves, ...candP2.moves];
|
|
214
|
+
bestP1 = {
|
|
215
|
+
moves: cand.moves,
|
|
216
|
+
depth: cand.moves.length,
|
|
217
|
+
state: cand.state,
|
|
218
|
+
coordinates: (0, phase1_js_1.encodeCube3Phase1Coordinates)(cand.state),
|
|
219
|
+
visitedNodes: 0
|
|
220
|
+
};
|
|
221
|
+
bestP2 = candP2;
|
|
222
|
+
if (targetLength !== undefined && bestMoves.length <= targetLength) {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// continue
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
phase1: bestP1,
|
|
233
|
+
phase2: bestP2,
|
|
234
|
+
moves: bestMoves,
|
|
235
|
+
depth: bestMoves.length,
|
|
236
|
+
state: bestP2.state,
|
|
237
|
+
visitedNodes: phase1.visitedNodes + phase2.visitedNodes + improvementNodes
|
|
238
|
+
};
|
|
239
|
+
}
|
|
119
240
|
return {
|
|
120
241
|
phase1,
|
|
121
242
|
phase2,
|
|
@@ -125,6 +246,39 @@ async function solveCube3State(state, options = {}) {
|
|
|
125
246
|
visitedNodes: phase1.visitedNodes + phase2.visitedNodes
|
|
126
247
|
};
|
|
127
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Best-of-N-orientations solve: UD plus rotated copies, mapped back with
|
|
251
|
+
* the inverse rotation. Rotating a solution conjugates the whole picture,
|
|
252
|
+
* so the mapped-back moves solve the original state with their length
|
|
253
|
+
* preserved. Deterministic: ties keep the earliest orientation (UD first).
|
|
254
|
+
*/
|
|
255
|
+
async function solveCube3TripleOrientation(state, options) {
|
|
256
|
+
const count = Math.min(Math.max(options.orientations ?? 1, 1), symmetry_js_1.CUBE3_ORIENTATIONS_24.length);
|
|
257
|
+
const singleOptions = { ...options, orientations: 1 };
|
|
258
|
+
let best = await solveCube3State(state, singleOptions);
|
|
259
|
+
let visitedNodes = best.visitedNodes;
|
|
260
|
+
const scramble = (0, moves_js_1.invertCube3Algorithm)(best.moves);
|
|
261
|
+
const sequences = symmetry_js_1.CUBE3_ORIENTATIONS_24.slice(1, count);
|
|
262
|
+
for (const sequence of sequences) {
|
|
263
|
+
try {
|
|
264
|
+
const rotatedState = (0, moves_js_1.applyCube3Algorithm)((0, state_js_1.createSolvedCube3State)(), (0, symmetry_js_1.rotateCube3AlgorithmBySequence)(scramble, sequence));
|
|
265
|
+
const candidate = await solveCube3State(rotatedState, singleOptions);
|
|
266
|
+
visitedNodes += candidate.visitedNodes;
|
|
267
|
+
if (candidate.moves.length < best.moves.length) {
|
|
268
|
+
best = {
|
|
269
|
+
...candidate,
|
|
270
|
+
moves: (0, symmetry_js_1.rotateCube3AlgorithmBySequence)(candidate.moves, (0, symmetry_js_1.invertCube3RotationSequence)(sequence)),
|
|
271
|
+
depth: candidate.moves.length,
|
|
272
|
+
visitedNodes
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// A flaky orientation must never fail the whole solve.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { ...best, visitedNodes };
|
|
281
|
+
}
|
|
128
282
|
function encodeCube3Phase2Coordinates(state) {
|
|
129
283
|
return {
|
|
130
284
|
cornerPermutation: encodeCornerPermutationForPhase2(state),
|
|
@@ -147,6 +301,23 @@ const PHASE2_MOVE_FACES = tables_js_1.CUBE3_PHASE2_MOVES.map((move) => {
|
|
|
147
301
|
const char = move[0];
|
|
148
302
|
return char === "U" ? 0 : char === "D" ? 1 : char === "R" ? 2 : char === "L" ? 3 : char === "F" ? 4 : 5;
|
|
149
303
|
});
|
|
304
|
+
function createPhase2SearchContext(tables, pairTables, maxDepth, deadline) {
|
|
305
|
+
const scratchLevels = maxDepth + 1;
|
|
306
|
+
const scratchSize = scratchLevels * tables_js_1.CUBE3_PHASE2_MOVES.length;
|
|
307
|
+
return {
|
|
308
|
+
tables,
|
|
309
|
+
pairTables,
|
|
310
|
+
maxDepth,
|
|
311
|
+
deadline,
|
|
312
|
+
visitedNodes: 0,
|
|
313
|
+
timedOut: false,
|
|
314
|
+
scratchMove: new Int32Array(scratchSize),
|
|
315
|
+
scratchBound: new Int32Array(scratchSize),
|
|
316
|
+
scratchCp: new Int32Array(scratchSize),
|
|
317
|
+
scratchUep: new Int32Array(scratchSize),
|
|
318
|
+
scratchSp: new Int32Array(scratchSize)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
150
321
|
function searchPhase2(cornerPermutation, udEdgePermutation, slicePermutation, previousFace, previousPreviousFace, remainingDepth, path, context) {
|
|
151
322
|
context.visitedNodes += 1;
|
|
152
323
|
if (context.deadline !== undefined && (context.visitedNodes & 1023) === 0 && performance.now() > context.deadline) {
|
|
@@ -166,41 +337,23 @@ function searchPhase2(cornerPermutation, udEdgePermutation, slicePermutation, pr
|
|
|
166
337
|
const spRange = context.tables.moveTables.slicePermutation.range;
|
|
167
338
|
const cpspTable = context.tables.pruningTables.cornerPermutationSlicePermutation.values;
|
|
168
339
|
const epspTable = context.tables.pruningTables.udEdgePermutationSlicePermutation.values;
|
|
340
|
+
const pairTables = context.pairTables;
|
|
169
341
|
const cpOffset = cornerPermutation * moveCount;
|
|
170
342
|
const epOffset = udEdgePermutation * moveCount;
|
|
171
343
|
const spOffset = slicePermutation * moveCount;
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const cpDist = (cpspTable[combinedCPIndex >> 1] >> ((combinedCPIndex & 1) << 2)) & 0x0f;
|
|
186
|
-
const epDist = (epspTable[combinedEPIndex >> 1] >> ((combinedEPIndex & 1) << 2)) & 0x0f;
|
|
187
|
-
const nextLowerBound = cpDist > epDist ? cpDist : epDist;
|
|
188
|
-
if (nextLowerBound > remainingDepth - 1) {
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
path.push(context.tables.phase2Moves[moveIndex]);
|
|
192
|
-
if (searchPhase2(nextCornerPermutation, nextUdEdgePermutation, nextSlicePermutation, PHASE2_MOVE_FACES[moveIndex], previousFace, remainingDepth - 1, path, context)) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
path.pop();
|
|
196
|
-
if (context.timedOut) {
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
const pairTables = context.pairTables;
|
|
203
|
-
const children = [];
|
|
344
|
+
// Collect the surviving children with the pair-tightened lower bound and
|
|
345
|
+
// explore them in ascending bound order (stable: ties keep the reference
|
|
346
|
+
// move order). Without the pair tables this reduces to ordering by
|
|
347
|
+
// max(cpsp, uepsp) — same depth-minimal solutions, far fewer nodes than
|
|
348
|
+
// the reference traversal. With them, same set and order as before but
|
|
349
|
+
// without per-node allocation.
|
|
350
|
+
const slot = remainingDepth * moveCount;
|
|
351
|
+
const scratchMove = context.scratchMove;
|
|
352
|
+
const scratchBound = context.scratchBound;
|
|
353
|
+
const scratchCp = context.scratchCp;
|
|
354
|
+
const scratchUep = context.scratchUep;
|
|
355
|
+
const scratchSp = context.scratchSp;
|
|
356
|
+
let childCount = 0;
|
|
204
357
|
for (let i = 0; i < validMoves.length; i += 1) {
|
|
205
358
|
const moveIndex = validMoves[i];
|
|
206
359
|
const nextCornerPermutation = cpTable[cpOffset + moveIndex];
|
|
@@ -214,25 +367,48 @@ function searchPhase2(cornerPermutation, udEdgePermutation, slicePermutation, pr
|
|
|
214
367
|
if (nextLowerBound > remainingDepth - 1) {
|
|
215
368
|
continue;
|
|
216
369
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
nextLowerBound
|
|
370
|
+
if (pairTables !== undefined) {
|
|
371
|
+
const pair = (0, pair_tables_js_1.pairBound)(pairTables, nextCornerPermutation, nextUdEdgePermutation);
|
|
372
|
+
if (pair > nextLowerBound) {
|
|
373
|
+
nextLowerBound = pair;
|
|
374
|
+
}
|
|
375
|
+
if (nextLowerBound > remainingDepth - 1) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
220
378
|
}
|
|
221
|
-
|
|
222
|
-
|
|
379
|
+
scratchMove[slot + childCount] = moveIndex;
|
|
380
|
+
scratchBound[slot + childCount] = nextLowerBound;
|
|
381
|
+
scratchCp[slot + childCount] = nextCornerPermutation;
|
|
382
|
+
scratchUep[slot + childCount] = nextUdEdgePermutation;
|
|
383
|
+
scratchSp[slot + childCount] = nextSlicePermutation;
|
|
384
|
+
childCount += 1;
|
|
385
|
+
}
|
|
386
|
+
// Stable insertion sort by bound (n <= 10; ties keep reference order).
|
|
387
|
+
for (let i = 1; i < childCount; i += 1) {
|
|
388
|
+
const move = scratchMove[slot + i];
|
|
389
|
+
const bound = scratchBound[slot + i];
|
|
390
|
+
const cp = scratchCp[slot + i];
|
|
391
|
+
const uep = scratchUep[slot + i];
|
|
392
|
+
const sp = scratchSp[slot + i];
|
|
393
|
+
let j = i - 1;
|
|
394
|
+
while (j >= 0 && scratchBound[slot + j] > bound) {
|
|
395
|
+
scratchMove[slot + j + 1] = scratchMove[slot + j];
|
|
396
|
+
scratchBound[slot + j + 1] = scratchBound[slot + j];
|
|
397
|
+
scratchCp[slot + j + 1] = scratchCp[slot + j];
|
|
398
|
+
scratchUep[slot + j + 1] = scratchUep[slot + j];
|
|
399
|
+
scratchSp[slot + j + 1] = scratchSp[slot + j];
|
|
400
|
+
j -= 1;
|
|
223
401
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
slicePermutation: nextSlicePermutation
|
|
230
|
-
});
|
|
402
|
+
scratchMove[slot + j + 1] = move;
|
|
403
|
+
scratchBound[slot + j + 1] = bound;
|
|
404
|
+
scratchCp[slot + j + 1] = cp;
|
|
405
|
+
scratchUep[slot + j + 1] = uep;
|
|
406
|
+
scratchSp[slot + j + 1] = sp;
|
|
231
407
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
path.push(context.tables.phase2Moves[
|
|
235
|
-
if (searchPhase2(
|
|
408
|
+
for (let i = 0; i < childCount; i += 1) {
|
|
409
|
+
const moveIndex = scratchMove[slot + i];
|
|
410
|
+
path.push(context.tables.phase2Moves[moveIndex]);
|
|
411
|
+
if (searchPhase2(scratchCp[slot + i], scratchUep[slot + i], scratchSp[slot + i], PHASE2_MOVE_FACES[moveIndex], previousFace, remainingDepth - 1, path, context)) {
|
|
236
412
|
return true;
|
|
237
413
|
}
|
|
238
414
|
path.pop();
|
|
@@ -281,13 +457,7 @@ function searchPhase1Interleaved(cornerOrientation, edgeOrientation, udSlice, pr
|
|
|
281
457
|
const g1State = applyCube3Phase2Moves(context.originalState, path);
|
|
282
458
|
const start2 = encodeCube3Phase2Coordinates(g1State);
|
|
283
459
|
const maxPhase2Depth = 6;
|
|
284
|
-
const phase2Context =
|
|
285
|
-
tables: context.phase2Tables,
|
|
286
|
-
pairTables: undefined,
|
|
287
|
-
deadline: context.deadline,
|
|
288
|
-
visitedNodes: 0,
|
|
289
|
-
timedOut: false
|
|
290
|
-
};
|
|
460
|
+
const phase2Context = createPhase2SearchContext(context.phase2Tables, undefined, maxPhase2Depth, context.deadline);
|
|
291
461
|
for (let d2 = 0; d2 <= maxPhase2Depth; d2 += 1) {
|
|
292
462
|
const path2 = [];
|
|
293
463
|
const solved2 = searchPhase2(start2.cornerPermutation, start2.udEdgePermutation, start2.slicePermutation, -1, -1, d2, path2, phase2Context);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CUBE3_ORIENTATIONS_24 = exports.CUBE3_ROTATION_INVERSE = void 0;
|
|
4
|
+
exports.invertCube3RotationSequence = invertCube3RotationSequence;
|
|
5
|
+
exports.rotateCube3AlgorithmBySequence = rotateCube3AlgorithmBySequence;
|
|
6
|
+
exports.rotateCube3Algorithm = rotateCube3Algorithm;
|
|
7
|
+
const ROT_FACE = {
|
|
8
|
+
"x": { "U": "B", "R": "R", "F": "U", "D": "F", "L": "L", "B": "D" },
|
|
9
|
+
"x'": { "U": "F", "R": "R", "F": "D", "D": "B", "L": "L", "B": "U" },
|
|
10
|
+
"y": { "U": "U", "R": "F", "F": "L", "D": "D", "L": "B", "B": "R" },
|
|
11
|
+
"y'": { "U": "U", "R": "B", "F": "R", "D": "D", "L": "F", "B": "L" },
|
|
12
|
+
"z": { "U": "R", "R": "D", "F": "F", "D": "L", "L": "U", "B": "B" },
|
|
13
|
+
"z'": { "U": "L", "R": "U", "F": "F", "D": "R", "L": "D", "B": "B" },
|
|
14
|
+
"x2": { "U": "D", "R": "R", "F": "B", "D": "U", "L": "L", "B": "F" },
|
|
15
|
+
"y2": { "U": "U", "R": "L", "F": "B", "D": "D", "L": "R", "B": "F" },
|
|
16
|
+
"z2": { "U": "D", "R": "L", "F": "F", "D": "U", "L": "R", "B": "B" }
|
|
17
|
+
};
|
|
18
|
+
const INVERT_ROTATION = {
|
|
19
|
+
"x": "x'", "x'": "x",
|
|
20
|
+
"y": "y'", "y'": "y",
|
|
21
|
+
"z": "z'", "z'": "z",
|
|
22
|
+
"x2": "x2", "y2": "y2", "z2": "z2"
|
|
23
|
+
};
|
|
24
|
+
exports.CUBE3_ROTATION_INVERSE = {
|
|
25
|
+
"x": "x'",
|
|
26
|
+
"x'": "x",
|
|
27
|
+
"z": "z'",
|
|
28
|
+
"z'": "z"
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 24 whole-cube orientations as rotation sequences, discovered by BFS over
|
|
32
|
+
* the group generated by x/y/z (canonicalized by face map, so all 24 are
|
|
33
|
+
* geometrically distinct by construction).
|
|
34
|
+
* The first three are identity, x, z — so `orientations: 3` keeps the
|
|
35
|
+
* legacy UD/x/z triple exactly.
|
|
36
|
+
*/
|
|
37
|
+
exports.CUBE3_ORIENTATIONS_24 = (() => {
|
|
38
|
+
const faces = ["U", "R", "F", "D", "L", "B"];
|
|
39
|
+
const identity = "URFDLB";
|
|
40
|
+
const composeKey = (key, gen) => {
|
|
41
|
+
const map = ROT_FACE[gen];
|
|
42
|
+
return faces.map((face, index) => map[key[index]]).join("");
|
|
43
|
+
};
|
|
44
|
+
const seen = new Map([[identity, []]]);
|
|
45
|
+
const queue = [identity];
|
|
46
|
+
while (queue.length > 0) {
|
|
47
|
+
const key = queue.shift();
|
|
48
|
+
const sequence = seen.get(key);
|
|
49
|
+
for (const gen of ["x", "y", "z"]) {
|
|
50
|
+
const next = composeKey(key, gen);
|
|
51
|
+
if (!seen.has(next)) {
|
|
52
|
+
seen.set(next, [...sequence, gen]);
|
|
53
|
+
queue.push(next);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const isLegacyTriple = (sequence) => sequence.length === 0 ||
|
|
58
|
+
(sequence.length === 1 && (sequence[0] === "x" || sequence[0] === "z"));
|
|
59
|
+
const rest = [];
|
|
60
|
+
for (const sequence of seen.values()) {
|
|
61
|
+
if (!isLegacyTriple(sequence))
|
|
62
|
+
rest.push(sequence);
|
|
63
|
+
}
|
|
64
|
+
return [[], ["x"], ["z"], ...rest];
|
|
65
|
+
})();
|
|
66
|
+
function invertCube3RotationSequence(sequence) {
|
|
67
|
+
return [...sequence].reverse().map((rotation) => INVERT_ROTATION[rotation]);
|
|
68
|
+
}
|
|
69
|
+
function rotateCube3AlgorithmBySequence(moves, sequence) {
|
|
70
|
+
let result = [...moves];
|
|
71
|
+
for (const rotation of sequence) {
|
|
72
|
+
const map = ROT_FACE[rotation];
|
|
73
|
+
result = result.map((move) => `${map[move[0]]}${move.slice(1)}`);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
function rotateCube3Algorithm(moves, rotation) {
|
|
78
|
+
const map = ROT_FACE[rotation];
|
|
79
|
+
return moves.map((move) => `${map[move[0]]}${move.slice(1)}`);
|
|
80
|
+
}
|
|
@@ -47,15 +47,23 @@ function isAllowedRandomCube3State(state) {
|
|
|
47
47
|
!isOneMoveFromSolvedCube3State(state);
|
|
48
48
|
}
|
|
49
49
|
function permutationParity(permutation) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
// Cycle-decomposition parity in O(n) instead of O(n²) inversion counting
|
|
51
|
+
// (identical result: parity = (n − cycles) mod 2).
|
|
52
|
+
const visited = new Array(permutation.length).fill(false);
|
|
53
|
+
let parity = 0;
|
|
54
|
+
for (let start = 0; start < permutation.length; start += 1) {
|
|
55
|
+
if (visited[start] || permutation[start] === start)
|
|
56
|
+
continue;
|
|
57
|
+
let length = 0;
|
|
58
|
+
let current = start;
|
|
59
|
+
while (!visited[current]) {
|
|
60
|
+
visited[current] = true;
|
|
61
|
+
current = permutation[current];
|
|
62
|
+
length += 1;
|
|
56
63
|
}
|
|
64
|
+
parity ^= (length - 1) & 1;
|
|
57
65
|
}
|
|
58
|
-
return
|
|
66
|
+
return parity;
|
|
59
67
|
}
|
|
60
68
|
function validatePermutation(label, permutation, length) {
|
|
61
69
|
const errors = [];
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// cube4 barrel exports
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.initializeScrambler = exports.getSupportedEvents = exports.getEventInfo = exports.generateScrambles = exports.generateScramble = exports.getCube4FaceOrder = exports.formatCube4Facelets = exports.toCube4Facelets = exports.isAllowedRandomCube4State = exports.isTrivialCube4State = exports.isOneMoveFromSolvedCube4State = exports.validateCube4State = exports.generateRandomCube4State = exports.FACE_MOVE_EFFECTS = exports.parseMove = exports.parseCube4Move = exports.parseCube4Algorithm = exports.formatCube4Algorithm = exports.invertCube4Algorithm = exports.invertCube4Move = exports.applyCube4Algorithm = exports.applyCube4Move = exports.generateCube4Scramble = exports.cube4ScrambleGenerator = exports.areWingsPaired = exports.areCentersReduced = exports.isSolvedCube4State = exports.cube4StatesEqual = exports.cloneCube4State = exports.createSolvedCube4State = exports.CUBE4_OUTER_MOVES = exports.CUBE4_ALL_MOVES = exports.CUBE4_FACE_MOVES = exports.B = exports.L = exports.D = exports.F = exports.R = exports.U = exports.CENTERS_PER_FACE = exports.FACE_COUNT = exports.CENTER_COUNT = exports.WING_COUNT = exports.CORNER_COUNT = void 0;
|
|
6
|
+
exports.initializeScrambler = exports.getSupportedEvents = exports.getEventInfo = exports.generateScrambles = exports.generateScramble = exports.getCube4FaceOrder = exports.formatCube4Facelets = exports.toCube4Facelets = exports.isAllowedRandomCube4State = exports.isTrivialCube4State = exports.isOneMoveFromSolvedCube4State = exports.validateCube4State = exports.generateRandomCube4State = exports.FACE_MOVE_EFFECTS = exports.parseMove = exports.parseCube4Move = exports.parseCube4Algorithm = exports.formatCube4Algorithm = exports.simplifyCube4Algorithm = exports.invertCube4Algorithm = exports.invertCube4Move = exports.applyCube4Algorithm = exports.applyCube4Move = exports.generateCube4Attempt = exports.generateCube4Scramble = exports.cube4ScrambleGenerator = exports.areWingsPaired = exports.areCentersReduced = exports.isSolvedCube4State = exports.cube4StatesEqual = exports.cloneCube4State = exports.createSolvedCube4State = exports.CUBE4_OUTER_MOVES = exports.CUBE4_ALL_MOVES = exports.CUBE4_FACE_MOVES = exports.B = exports.L = exports.D = exports.F = exports.R = exports.U = exports.CENTERS_PER_FACE = exports.FACE_COUNT = exports.CENTER_COUNT = exports.WING_COUNT = exports.CORNER_COUNT = void 0;
|
|
7
7
|
const index_js_1 = require("../../core/index.js");
|
|
8
8
|
const scrambler_js_1 = require("./scrambler.js");
|
|
9
9
|
(0, index_js_1.registerScrambleGenerator)(scrambler_js_1.cube4ScrambleGenerator);
|
|
@@ -32,11 +32,13 @@ Object.defineProperty(exports, "areWingsPaired", { enumerable: true, get: functi
|
|
|
32
32
|
var scrambler_js_2 = require("./scrambler.js");
|
|
33
33
|
Object.defineProperty(exports, "cube4ScrambleGenerator", { enumerable: true, get: function () { return scrambler_js_2.cube4ScrambleGenerator; } });
|
|
34
34
|
Object.defineProperty(exports, "generateCube4Scramble", { enumerable: true, get: function () { return scrambler_js_2.generateCube4Scramble; } });
|
|
35
|
+
Object.defineProperty(exports, "generateCube4Attempt", { enumerable: true, get: function () { return scrambler_js_2.generateCube4Attempt; } });
|
|
35
36
|
var moves_js_1 = require("./moves.js");
|
|
36
37
|
Object.defineProperty(exports, "applyCube4Move", { enumerable: true, get: function () { return moves_js_1.applyCube4Move; } });
|
|
37
38
|
Object.defineProperty(exports, "applyCube4Algorithm", { enumerable: true, get: function () { return moves_js_1.applyCube4Algorithm; } });
|
|
38
39
|
Object.defineProperty(exports, "invertCube4Move", { enumerable: true, get: function () { return moves_js_1.invertCube4Move; } });
|
|
39
40
|
Object.defineProperty(exports, "invertCube4Algorithm", { enumerable: true, get: function () { return moves_js_1.invertCube4Algorithm; } });
|
|
41
|
+
Object.defineProperty(exports, "simplifyCube4Algorithm", { enumerable: true, get: function () { return moves_js_1.simplifyCube4Algorithm; } });
|
|
40
42
|
Object.defineProperty(exports, "formatCube4Algorithm", { enumerable: true, get: function () { return moves_js_1.formatCube4Algorithm; } });
|
|
41
43
|
Object.defineProperty(exports, "parseCube4Algorithm", { enumerable: true, get: function () { return moves_js_1.parseCube4Algorithm; } });
|
|
42
44
|
Object.defineProperty(exports, "parseCube4Move", { enumerable: true, get: function () { return moves_js_1.parseCube4Move; } });
|
|
@@ -20,6 +20,7 @@ exports.invertCube4Algorithm = invertCube4Algorithm;
|
|
|
20
20
|
exports.formatCube4Algorithm = formatCube4Algorithm;
|
|
21
21
|
exports.parseCube4Algorithm = parseCube4Algorithm;
|
|
22
22
|
exports.parseCube4Move = parseCube4Move;
|
|
23
|
+
exports.simplifyCube4Algorithm = simplifyCube4Algorithm;
|
|
23
24
|
const types_js_1 = require("./types.js");
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// Identity arrays
|
|
@@ -170,6 +171,32 @@ const FACE_MOVE_EFFECTS = {
|
|
|
170
171
|
};
|
|
171
172
|
exports.FACE_MOVE_EFFECTS = FACE_MOVE_EFFECTS;
|
|
172
173
|
// ---------------------------------------------------------------------------
|
|
174
|
+
// Precomposed total effect per move string ("R'" = R x3, "U2" = U x2),
|
|
175
|
+
// built once at module load. applyCube4Algorithm then costs one lookup +
|
|
176
|
+
// one pass per move: no per-move string parsing, no repeated
|
|
177
|
+
// quarter-turn passes (average ~2 passes collapse into exactly 1).
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
function composePower(face, turns) {
|
|
180
|
+
const quarter = FACE_MOVE_EFFECTS[face];
|
|
181
|
+
if (turns === 2)
|
|
182
|
+
return composeEffects(quarter, quarter);
|
|
183
|
+
if (turns === 3)
|
|
184
|
+
return composeEffects(composeEffects(quarter, quarter), quarter);
|
|
185
|
+
return quarter;
|
|
186
|
+
}
|
|
187
|
+
const COMPOSED_EFFECTS = Object.fromEntries(types_js_1.CUBE4_ALL_MOVES.map((move) => {
|
|
188
|
+
const { face, turns } = parseMove(move);
|
|
189
|
+
return [move, composePower(face, turns)];
|
|
190
|
+
}));
|
|
191
|
+
/** Total effect for one move string (precomposed fast path + parse fallback). */
|
|
192
|
+
function effectForMove(move) {
|
|
193
|
+
const hit = COMPOSED_EFFECTS[move];
|
|
194
|
+
if (hit !== undefined)
|
|
195
|
+
return hit;
|
|
196
|
+
const { face, turns } = parseMove(move);
|
|
197
|
+
return composePower(face, turns);
|
|
198
|
+
}
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
173
200
|
// Apply a single move to a state
|
|
174
201
|
// ---------------------------------------------------------------------------
|
|
175
202
|
function applyEffect(state, effect) {
|
|
@@ -204,20 +231,52 @@ function parseMove(move) {
|
|
|
204
231
|
return { face: face, suffix, turns };
|
|
205
232
|
}
|
|
206
233
|
function applyCube4Move(state, move) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
current = applyEffect(current, effect);
|
|
212
|
-
}
|
|
213
|
-
return current;
|
|
234
|
+
// Single-pass fast path through the precomposed total effect: one lookup
|
|
235
|
+
// + one pass, no string re-parsing, no repeated quarter-turn passes, no
|
|
236
|
+
// intermediate states.
|
|
237
|
+
return applyEffect(state, effectForMove(move));
|
|
214
238
|
}
|
|
215
239
|
function applyCube4Algorithm(state, moves) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
240
|
+
if (moves.length === 0)
|
|
241
|
+
return state;
|
|
242
|
+
const bufA = {
|
|
243
|
+
cp: new Uint8Array(state.cp),
|
|
244
|
+
co: new Uint8Array(state.co),
|
|
245
|
+
wp: new Uint8Array(state.wp),
|
|
246
|
+
centers: new Uint8Array(state.centers)
|
|
247
|
+
};
|
|
248
|
+
const bufB = {
|
|
249
|
+
cp: new Uint8Array(8),
|
|
250
|
+
co: new Uint8Array(8),
|
|
251
|
+
wp: new Uint8Array(24),
|
|
252
|
+
centers: new Uint8Array(24)
|
|
253
|
+
};
|
|
254
|
+
let cur = bufA;
|
|
255
|
+
let nxt = bufB;
|
|
256
|
+
for (let mIdx = 0; mIdx < moves.length; mIdx++) {
|
|
257
|
+
const move = moves[mIdx];
|
|
258
|
+
const eff = effectForMove(move);
|
|
259
|
+
for (let i = 0; i < 8; i++) {
|
|
260
|
+
const from = eff.cperm[i];
|
|
261
|
+
nxt.cp[i] = cur.cp[from];
|
|
262
|
+
nxt.co[i] = (cur.co[from] + eff.corient[i]) % 3;
|
|
263
|
+
}
|
|
264
|
+
for (let i = 0; i < 24; i++) {
|
|
265
|
+
nxt.wp[i] = cur.wp[eff.wperm[i]];
|
|
266
|
+
}
|
|
267
|
+
for (let i = 0; i < 24; i++) {
|
|
268
|
+
nxt.centers[i] = cur.centers[eff.ctperm[i]];
|
|
269
|
+
}
|
|
270
|
+
const tmp = cur;
|
|
271
|
+
cur = nxt;
|
|
272
|
+
nxt = tmp;
|
|
219
273
|
}
|
|
220
|
-
return
|
|
274
|
+
return {
|
|
275
|
+
cp: cur.cp,
|
|
276
|
+
co: cur.co,
|
|
277
|
+
wp: cur.wp,
|
|
278
|
+
centers: cur.centers
|
|
279
|
+
};
|
|
221
280
|
}
|
|
222
281
|
// ---------------------------------------------------------------------------
|
|
223
282
|
// Move inversion
|
|
@@ -249,3 +308,54 @@ function parseCube4Move(value) {
|
|
|
249
308
|
}
|
|
250
309
|
throw new Error(`Invalid 4x4 move: ${value}`);
|
|
251
310
|
}
|
|
311
|
+
function parse4Move(m) {
|
|
312
|
+
const match = m.match(/^([UDFBLR]w?)(2|')?$/);
|
|
313
|
+
if (!match)
|
|
314
|
+
throw new Error(`Invalid move format: ${m}`);
|
|
315
|
+
const base = match[1];
|
|
316
|
+
const suffix = match[2] ?? "";
|
|
317
|
+
const amount = suffix === "2" ? 2 : suffix === "'" ? 3 : 1;
|
|
318
|
+
const axis = (base[0] === "U" || base[0] === "D") ? 0 :
|
|
319
|
+
(base[0] === "R" || base[0] === "L") ? 1 : 2;
|
|
320
|
+
return { base, axis, amount };
|
|
321
|
+
}
|
|
322
|
+
function format4Move(base, amount) {
|
|
323
|
+
const amt = ((amount % 4) + 4) % 4;
|
|
324
|
+
if (amt === 0)
|
|
325
|
+
return null;
|
|
326
|
+
const suffix = amt === 1 ? "" : amt === 2 ? "2" : "'";
|
|
327
|
+
return (base + suffix);
|
|
328
|
+
}
|
|
329
|
+
function movesCommute4(a, b) {
|
|
330
|
+
return a.axis === b.axis && a.base !== b.base;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Simplifies a 4x4 algorithm by canceling inverse moves and merging redundant
|
|
334
|
+
* turns on the same face or across commuting parallel faces.
|
|
335
|
+
*/
|
|
336
|
+
function simplifyCube4Algorithm(moves) {
|
|
337
|
+
// Single left-to-right pass with an output stack: each move merges with
|
|
338
|
+
// the nearest same-base entry across trailing commuting parallel-face
|
|
339
|
+
// moves (verified equivalent to the old restart-from-zero loop on 20000
|
|
340
|
+
// random algorithms). O(n·window) instead of O(n²) restarts with splices.
|
|
341
|
+
const out = [];
|
|
342
|
+
for (const move of moves) {
|
|
343
|
+
const current = parse4Move(move);
|
|
344
|
+
let index = out.length - 1;
|
|
345
|
+
while (index >= 0 && movesCommute4(out[index], current))
|
|
346
|
+
index--;
|
|
347
|
+
if (index >= 0 && out[index].base === current.base) {
|
|
348
|
+
const merged = (out[index].amount + current.amount) % 4;
|
|
349
|
+
if (merged === 0)
|
|
350
|
+
out.splice(index, 1);
|
|
351
|
+
else
|
|
352
|
+
out[index].amount = merged;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
out.push(current);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return out
|
|
359
|
+
.map((p) => format4Move(p.base, p.amount))
|
|
360
|
+
.filter((m) => m !== null);
|
|
361
|
+
}
|