@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.
Files changed (115) hide show
  1. package/README.md +2 -0
  2. package/dist/puzzles/cube2/solver/search.d.ts.map +1 -1
  3. package/dist/puzzles/cube2/solver/search.js +3 -3
  4. package/dist/puzzles/cube2/solver/tables.d.ts +6 -0
  5. package/dist/puzzles/cube2/solver/tables.d.ts.map +1 -1
  6. package/dist/puzzles/cube2/solver/tables.js +16 -2
  7. package/dist/puzzles/cube3/index.d.ts +1 -0
  8. package/dist/puzzles/cube3/index.d.ts.map +1 -1
  9. package/dist/puzzles/cube3/moves.d.ts.map +1 -1
  10. package/dist/puzzles/cube3/moves.js +104 -18
  11. package/dist/puzzles/cube3/scrambler.d.ts +16 -1
  12. package/dist/puzzles/cube3/scrambler.d.ts.map +1 -1
  13. package/dist/puzzles/cube3/scrambler.js +1 -1
  14. package/dist/puzzles/cube3/solver/browser-loader.js +6 -2
  15. package/dist/puzzles/cube3/solver/index.d.ts +5 -3
  16. package/dist/puzzles/cube3/solver/index.d.ts.map +1 -1
  17. package/dist/puzzles/cube3/solver/index.js +2 -1
  18. package/dist/puzzles/cube3/solver/node-loader.js +9 -2
  19. package/dist/puzzles/cube3/solver/phase1.d.ts +18 -0
  20. package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
  21. package/dist/puzzles/cube3/solver/phase1.js +132 -14
  22. package/dist/puzzles/cube3/solver/phase2.d.ts +47 -0
  23. package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
  24. package/dist/puzzles/cube3/solver/phase2.js +236 -66
  25. package/dist/puzzles/cube3/solver/search-utils.d.ts +1 -1
  26. package/dist/puzzles/cube3/solver/search-utils.d.ts.map +1 -1
  27. package/dist/puzzles/cube3/solver/symmetry.d.ts +24 -0
  28. package/dist/puzzles/cube3/solver/symmetry.d.ts.map +1 -0
  29. package/dist/puzzles/cube3/solver/symmetry.js +74 -0
  30. package/dist/puzzles/cube3/solver/tables.d.ts +6 -1
  31. package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
  32. package/dist/puzzles/cube3/validation.d.ts.map +1 -1
  33. package/dist/puzzles/cube3/validation.js +15 -7
  34. package/dist/puzzles/cube4/index.d.ts +3 -2
  35. package/dist/puzzles/cube4/index.d.ts.map +1 -1
  36. package/dist/puzzles/cube4/index.js +2 -2
  37. package/dist/puzzles/cube4/moves.d.ts +5 -0
  38. package/dist/puzzles/cube4/moves.d.ts.map +1 -1
  39. package/dist/puzzles/cube4/moves.js +120 -11
  40. package/dist/puzzles/cube4/reduction/browser-loader.d.ts +3 -0
  41. package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
  42. package/dist/puzzles/cube4/reduction/browser-loader.js +34 -3
  43. package/dist/puzzles/cube4/reduction/center2.d.ts +20 -3
  44. package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
  45. package/dist/puzzles/cube4/reduction/center2.js +53 -9
  46. package/dist/puzzles/cube4/reduction/edge3.d.ts +4 -0
  47. package/dist/puzzles/cube4/reduction/edge3.d.ts.map +1 -1
  48. package/dist/puzzles/cube4/reduction/edge3.js +23 -3
  49. package/dist/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
  50. package/dist/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
  51. package/dist/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
  52. package/dist/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
  53. package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +38 -0
  54. package/dist/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
  55. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +50 -0
  56. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -0
  57. package/dist/puzzles/cube4/reduction/hc-pdb.js +202 -0
  58. package/dist/puzzles/cube4/reduction/node-loader.d.ts +5 -0
  59. package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
  60. package/dist/puzzles/cube4/reduction/node-loader.js +48 -8
  61. package/dist/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
  62. package/dist/puzzles/cube4/reduction/phase3-search.d.ts +111 -5
  63. package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
  64. package/dist/puzzles/cube4/reduction/phase3-search.js +341 -22
  65. package/dist/puzzles/cube4/reduction/solve.d.ts +112 -1
  66. package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
  67. package/dist/puzzles/cube4/reduction/solve.js +920 -181
  68. package/dist/puzzles/cube4/scrambler.d.ts +105 -4
  69. package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
  70. package/dist/puzzles/cube4/scrambler.js +216 -166
  71. package/dist/puzzles/cube4/state.d.ts.map +1 -1
  72. package/dist/puzzles/cube4/state.js +4 -1
  73. package/dist/puzzles/pyraminx/solver/search.d.ts.map +1 -1
  74. package/dist/puzzles/pyraminx/solver/search.js +3 -6
  75. package/dist/puzzles/pyraminx/solver/tables.d.ts +6 -0
  76. package/dist/puzzles/pyraminx/solver/tables.d.ts.map +1 -1
  77. package/dist/puzzles/pyraminx/solver/tables.js +17 -0
  78. package/dist/puzzles/skewb/solver/search.js +3 -3
  79. package/dist/puzzles/skewb/solver/tables.d.ts +6 -0
  80. package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -1
  81. package/dist/puzzles/skewb/solver/tables.js +17 -0
  82. package/dist-cjs/puzzles/cube2/solver/search.js +2 -2
  83. package/dist-cjs/puzzles/cube2/solver/tables.js +17 -3
  84. package/dist-cjs/puzzles/cube3/moves.js +104 -18
  85. package/dist-cjs/puzzles/cube3/scrambler.js +1 -1
  86. package/dist-cjs/puzzles/cube3/solver/browser-loader.js +6 -2
  87. package/dist-cjs/puzzles/cube3/solver/index.js +8 -1
  88. package/dist-cjs/puzzles/cube3/solver/node-loader.js +9 -2
  89. package/dist-cjs/puzzles/cube3/solver/phase1.js +133 -14
  90. package/dist-cjs/puzzles/cube3/solver/phase2.js +233 -63
  91. package/dist-cjs/puzzles/cube3/solver/symmetry.js +80 -0
  92. package/dist-cjs/puzzles/cube3/validation.js +15 -7
  93. package/dist-cjs/puzzles/cube4/index.js +3 -1
  94. package/dist-cjs/puzzles/cube4/moves.js +121 -11
  95. package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +34 -3
  96. package/dist-cjs/puzzles/cube4/reduction/center2.js +54 -9
  97. package/dist-cjs/puzzles/cube4/reduction/edge3.js +24 -3
  98. package/dist-cjs/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
  99. package/dist-cjs/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
  100. package/dist-cjs/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
  101. package/dist-cjs/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
  102. package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +38 -0
  103. package/dist-cjs/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
  104. package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +213 -0
  105. package/dist-cjs/puzzles/cube4/reduction/node-loader.js +48 -8
  106. package/dist-cjs/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
  107. package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +343 -23
  108. package/dist-cjs/puzzles/cube4/reduction/solve.js +918 -173
  109. package/dist-cjs/puzzles/cube4/scrambler.js +214 -163
  110. package/dist-cjs/puzzles/cube4/state.js +4 -1
  111. package/dist-cjs/puzzles/pyraminx/solver/search.js +2 -5
  112. package/dist-cjs/puzzles/pyraminx/solver/tables.js +18 -1
  113. package/dist-cjs/puzzles/skewb/solver/search.js +2 -2
  114. package/dist-cjs/puzzles/skewb/solver/tables.js +18 -1
  115. 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
- // Without the pair tables (the default npm package), keep the exact
173
- // reference traversal zero overhead. With them (turbo tier), collect the
174
- // children with the pair-distance bound and explore in ascending bound
175
- // order (stable: ties keep the reference move order): same depth-minimal
176
- // solutions, far fewer nodes.
177
- if (context.pairTables === undefined) {
178
- for (let i = 0; i < validMoves.length; i += 1) {
179
- const moveIndex = validMoves[i];
180
- const nextCornerPermutation = cpTable[cpOffset + moveIndex];
181
- const nextUdEdgePermutation = epTable[epOffset + moveIndex];
182
- const nextSlicePermutation = spTable[spOffset + moveIndex];
183
- const combinedCPIndex = nextCornerPermutation * spRange + nextSlicePermutation;
184
- const combinedEPIndex = nextUdEdgePermutation * spRange + nextSlicePermutation;
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
- const pair = (0, pair_tables_js_1.pairBound)(pairTables, nextCornerPermutation, nextUdEdgePermutation);
218
- if (pair > nextLowerBound) {
219
- nextLowerBound = pair;
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
- if (nextLowerBound > remainingDepth - 1) {
222
- continue;
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
- children.push({
225
- moveIndex,
226
- lowerBound: nextLowerBound,
227
- cornerPermutation: nextCornerPermutation,
228
- udEdgePermutation: nextUdEdgePermutation,
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
- children.sort((left, right) => left.lowerBound - right.lowerBound);
233
- for (const child of children) {
234
- path.push(context.tables.phase2Moves[child.moveIndex]);
235
- if (searchPhase2(child.cornerPermutation, child.udEdgePermutation, child.slicePermutation, PHASE2_MOVE_FACES[child.moveIndex], previousFace, remainingDepth - 1, path, context)) {
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
- let inversions = 0;
51
- for (let left = 0; left < permutation.length; left += 1) {
52
- for (let right = left + 1; right < permutation.length; right += 1) {
53
- if (permutation[left] > permutation[right]) {
54
- inversions += 1;
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 (inversions % 2);
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
- const { face, turns } = parseMove(move);
208
- const effect = FACE_MOVE_EFFECTS[face];
209
- let current = state;
210
- for (let i = 0; i < turns; i++) {
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
- let current = state;
217
- for (const move of moves) {
218
- current = applyCube4Move(current, move);
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 current;
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
+ }