@cubingcenter/scrambler 0.6.3 → 0.8.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 (191) hide show
  1. package/README.md +17 -4
  2. package/dist/core/api.d.ts.map +1 -1
  3. package/dist/core/api.js +10 -2
  4. package/dist/puzzles/cube2/solver/browser-loader.d.ts +4 -7
  5. package/dist/puzzles/cube2/solver/browser-loader.d.ts.map +1 -1
  6. package/dist/puzzles/cube2/solver/browser-loader.js +42 -14
  7. package/dist/puzzles/cube2/solver/generated-assets.d.ts.map +1 -1
  8. package/dist/puzzles/cube2/solver/generated-assets.js +8 -3
  9. package/dist/puzzles/cube2/solver/search.d.ts.map +1 -1
  10. package/dist/puzzles/cube2/solver/search.js +3 -3
  11. package/dist/puzzles/cube2/solver/tables.d.ts +6 -0
  12. package/dist/puzzles/cube2/solver/tables.d.ts.map +1 -1
  13. package/dist/puzzles/cube2/solver/tables.js +16 -2
  14. package/dist/puzzles/cube3/index.d.ts +1 -0
  15. package/dist/puzzles/cube3/index.d.ts.map +1 -1
  16. package/dist/puzzles/cube3/moves.d.ts.map +1 -1
  17. package/dist/puzzles/cube3/moves.js +104 -18
  18. package/dist/puzzles/cube3/scrambler.d.ts +16 -1
  19. package/dist/puzzles/cube3/scrambler.d.ts.map +1 -1
  20. package/dist/puzzles/cube3/scrambler.js +1 -1
  21. package/dist/puzzles/cube3/solver/browser-loader.d.ts.map +1 -1
  22. package/dist/puzzles/cube3/solver/browser-loader.js +48 -22
  23. package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
  24. package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
  25. package/dist/puzzles/cube3/solver/index.d.ts +5 -3
  26. package/dist/puzzles/cube3/solver/index.d.ts.map +1 -1
  27. package/dist/puzzles/cube3/solver/index.js +2 -1
  28. package/dist/puzzles/cube3/solver/node-loader.js +9 -2
  29. package/dist/puzzles/cube3/solver/phase1.d.ts +37 -0
  30. package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
  31. package/dist/puzzles/cube3/solver/phase1.js +156 -15
  32. package/dist/puzzles/cube3/solver/phase2.d.ts +75 -0
  33. package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
  34. package/dist/puzzles/cube3/solver/phase2.js +279 -68
  35. package/dist/puzzles/cube3/solver/search-utils.d.ts +1 -1
  36. package/dist/puzzles/cube3/solver/search-utils.d.ts.map +1 -1
  37. package/dist/puzzles/cube3/solver/symmetry.d.ts +24 -0
  38. package/dist/puzzles/cube3/solver/symmetry.d.ts.map +1 -0
  39. package/dist/puzzles/cube3/solver/symmetry.js +74 -0
  40. package/dist/puzzles/cube3/solver/tables.d.ts +6 -1
  41. package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
  42. package/dist/puzzles/cube3/solver/tables.js +14 -12
  43. package/dist/puzzles/cube3/validation.d.ts.map +1 -1
  44. package/dist/puzzles/cube3/validation.js +15 -7
  45. package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
  46. package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
  47. package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
  48. package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
  49. package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
  50. package/dist/puzzles/cube4/browser-worker.js +31 -0
  51. package/dist/puzzles/cube4/index.d.ts +10 -2
  52. package/dist/puzzles/cube4/index.d.ts.map +1 -1
  53. package/dist/puzzles/cube4/index.js +6 -2
  54. package/dist/puzzles/cube4/moves.d.ts +5 -0
  55. package/dist/puzzles/cube4/moves.d.ts.map +1 -1
  56. package/dist/puzzles/cube4/moves.js +212 -11
  57. package/dist/puzzles/cube4/reduction/browser-loader.d.ts +29 -1
  58. package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
  59. package/dist/puzzles/cube4/reduction/browser-loader.js +227 -32
  60. package/dist/puzzles/cube4/reduction/center2.d.ts +44 -3
  61. package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
  62. package/dist/puzzles/cube4/reduction/center2.js +152 -10
  63. package/dist/puzzles/cube4/reduction/edge3.d.ts +4 -0
  64. package/dist/puzzles/cube4/reduction/edge3.d.ts.map +1 -1
  65. package/dist/puzzles/cube4/reduction/edge3.js +23 -3
  66. package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
  67. package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
  68. package/dist/puzzles/cube4/reduction/finish3.js +393 -0
  69. package/dist/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
  70. package/dist/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
  71. package/dist/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
  72. package/dist/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
  73. package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +44 -0
  74. package/dist/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
  75. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +52 -0
  76. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -0
  77. package/dist/puzzles/cube4/reduction/hc-pdb.js +220 -0
  78. package/dist/puzzles/cube4/reduction/node-loader.d.ts +15 -1
  79. package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
  80. package/dist/puzzles/cube4/reduction/node-loader.js +199 -25
  81. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
  82. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
  83. package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
  84. package/dist/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
  85. package/dist/puzzles/cube4/reduction/phase3-search.d.ts +116 -7
  86. package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
  87. package/dist/puzzles/cube4/reduction/phase3-search.js +458 -32
  88. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
  89. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
  90. package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
  91. package/dist/puzzles/cube4/reduction/solve.d.ts +172 -2
  92. package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
  93. package/dist/puzzles/cube4/reduction/solve.js +1201 -180
  94. package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
  95. package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
  96. package/dist/puzzles/cube4/scramble-cache.js +150 -0
  97. package/dist/puzzles/cube4/scrambler.d.ts +201 -6
  98. package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
  99. package/dist/puzzles/cube4/scrambler.js +414 -176
  100. package/dist/puzzles/cube4/state.d.ts.map +1 -1
  101. package/dist/puzzles/cube4/state.js +4 -1
  102. package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
  103. package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
  104. package/dist/puzzles/cube4/worker-pool.js +405 -0
  105. package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
  106. package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
  107. package/dist/puzzles/cube4/worker-protocol.js +58 -0
  108. package/dist/puzzles/cube4/worker.d.ts +26 -0
  109. package/dist/puzzles/cube4/worker.d.ts.map +1 -0
  110. package/dist/puzzles/cube4/worker.js +49 -0
  111. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
  112. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
  113. package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
  114. package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
  115. package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
  116. package/dist/puzzles/pyraminx/solver/search.d.ts.map +1 -1
  117. package/dist/puzzles/pyraminx/solver/search.js +3 -6
  118. package/dist/puzzles/pyraminx/solver/tables.d.ts +6 -0
  119. package/dist/puzzles/pyraminx/solver/tables.d.ts.map +1 -1
  120. package/dist/puzzles/pyraminx/solver/tables.js +17 -0
  121. package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
  122. package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
  123. package/dist/puzzles/shared/compressed-fetch.js +226 -31
  124. package/dist/puzzles/shared/load-once.d.ts +16 -0
  125. package/dist/puzzles/shared/load-once.d.ts.map +1 -0
  126. package/dist/puzzles/shared/load-once.js +11 -0
  127. package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
  128. package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
  129. package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
  130. package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
  131. package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
  132. package/dist/puzzles/skewb/solver/search.js +3 -3
  133. package/dist/puzzles/skewb/solver/tables.d.ts +6 -0
  134. package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -1
  135. package/dist/puzzles/skewb/solver/tables.js +17 -0
  136. package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
  137. package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
  138. package/dist-cjs/core/api.js +10 -2
  139. package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
  140. package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
  141. package/dist-cjs/puzzles/cube2/solver/search.js +2 -2
  142. package/dist-cjs/puzzles/cube2/solver/tables.js +17 -3
  143. package/dist-cjs/puzzles/cube3/moves.js +104 -18
  144. package/dist-cjs/puzzles/cube3/scrambler.js +1 -1
  145. package/dist-cjs/puzzles/cube3/solver/browser-loader.js +48 -22
  146. package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
  147. package/dist-cjs/puzzles/cube3/solver/index.js +8 -1
  148. package/dist-cjs/puzzles/cube3/solver/node-loader.js +9 -2
  149. package/dist-cjs/puzzles/cube3/solver/phase1.js +157 -15
  150. package/dist-cjs/puzzles/cube3/solver/phase2.js +276 -65
  151. package/dist-cjs/puzzles/cube3/solver/symmetry.js +80 -0
  152. package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
  153. package/dist-cjs/puzzles/cube3/validation.js +15 -7
  154. package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
  155. package/dist-cjs/puzzles/cube4/index.js +23 -1
  156. package/dist-cjs/puzzles/cube4/moves.js +213 -11
  157. package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +227 -32
  158. package/dist-cjs/puzzles/cube4/reduction/center2.js +155 -10
  159. package/dist-cjs/puzzles/cube4/reduction/edge3.js +24 -3
  160. package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
  161. package/dist-cjs/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
  162. package/dist-cjs/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
  163. package/dist-cjs/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
  164. package/dist-cjs/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
  165. package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +44 -0
  166. package/dist-cjs/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
  167. package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +232 -0
  168. package/dist-cjs/puzzles/cube4/reduction/node-loader.js +200 -25
  169. package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
  170. package/dist-cjs/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
  171. package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +460 -33
  172. package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
  173. package/dist-cjs/puzzles/cube4/reduction/solve.js +1203 -172
  174. package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
  175. package/dist-cjs/puzzles/cube4/scrambler.js +416 -174
  176. package/dist-cjs/puzzles/cube4/state.js +4 -1
  177. package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
  178. package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
  179. package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
  180. package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
  181. package/dist-cjs/puzzles/pyraminx/solver/search.js +2 -5
  182. package/dist-cjs/puzzles/pyraminx/solver/tables.js +18 -1
  183. package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
  184. package/dist-cjs/puzzles/shared/load-once.js +14 -0
  185. package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
  186. package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
  187. package/dist-cjs/puzzles/skewb/solver/search.js +2 -2
  188. package/dist-cjs/puzzles/skewb/solver/tables.js +18 -1
  189. package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
  190. package/docs/development.md +19 -7
  191. package/package.json +17 -7
@@ -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, options.maxNodes);
64
59
  const spRange = tables.moveTables.slicePermutation.range;
65
60
  const cpspTable = tables.pruningTables.cornerPermutationSlicePermutation.values;
66
61
  const epspTable = tables.pruningTables.udEdgePermutationSlicePermutation.values;
@@ -91,6 +86,9 @@ async function solveCube3Phase2(state, options = {}) {
91
86
  visitedNodes: context.visitedNodes
92
87
  };
93
88
  }
89
+ if (context.nodesExceeded) {
90
+ throw new Cube3Phase2SearchError(`3x3 Phase 2 node budget exceeded (${options.maxNodes} nodes).`, context.visitedNodes);
91
+ }
94
92
  if (context.timedOut) {
95
93
  throw new Cube3Phase2SearchError(`3x3 Phase 2 search timed out after ${options.timeoutMs}ms.`, context.visitedNodes);
96
94
  }
@@ -98,6 +96,11 @@ async function solveCube3Phase2(state, options = {}) {
98
96
  throw new Cube3Phase2SearchError(`3x3 Phase 2 solution not found within depth ${maxDepth}.`, context.visitedNodes);
99
97
  }
100
98
  async function solveCube3State(state, options = {}) {
99
+ const quality = options.quality ?? "short";
100
+ const orientations = options.orientations ?? (quality === "best" ? 3 : 1);
101
+ if (orientations >= 3) {
102
+ return solveCube3TripleOrientation(state, { ...options, orientations });
103
+ }
101
104
  const phase1 = await (0, phase1_js_1.solveCube3Phase1)(state, options.phase1);
102
105
  let previousFace = -1;
103
106
  let previousPreviousFace = -1;
@@ -116,6 +119,154 @@ async function solveCube3State(state, options = {}) {
116
119
  };
117
120
  const phase2 = await solveCube3Phase2(phase1.state, phase2Options);
118
121
  const moves = [...phase1.moves, ...phase2.moves];
122
+ const targetLength = options.targetLength;
123
+ const extraDepth = options.phase1ExtraDepth ?? (quality === "fast" ? 0 : 1);
124
+ const solutionLimit = options.phase1SolutionLimit ?? (targetLength !== undefined ? 32 : 8);
125
+ const improveDepths = targetLength !== undefined ? Math.max(4, extraDepth) : extraDepth;
126
+ const needsTarget = targetLength !== undefined && moves.length > targetLength;
127
+ if (needsTarget || (targetLength === undefined && extraDepth > 0)) {
128
+ const p1Tables = options.phase1?.tables ?? await (0, tables_js_1.loadCube3Phase1Tables)();
129
+ const p2Tables = options.phase2?.tables ?? await (0, tables_js_1.loadCube3Phase2Tables)();
130
+ const scoredPairTables = options.phase2?.pairTables ??
131
+ (options.phase2?.skipPairTables ? undefined : await (0, pair_tables_js_1.loadCube3PairTables)());
132
+ // Work budget for the improvement loop. Preferred: `improveMaxNodes`
133
+ // (deterministic node counter — the wall deadline stays unarmed so the
134
+ // outcome cannot depend on CPU load/clock). Legacy: `timeoutMs ?? 800`
135
+ // wall deadline, kept for existing callers.
136
+ const nodeMode = options.improveMaxNodes !== undefined;
137
+ let remainingNodes = options.improveMaxNodes ?? 0;
138
+ const deadline = nodeMode ? undefined : performance.now() + (options.timeoutMs ?? 800);
139
+ const budgetExhausted = () => nodeMode ? remainingNodes <= 0 : performance.now() > deadline;
140
+ const spendNodes = (used) => {
141
+ if (nodeMode) {
142
+ remainingNodes = Math.max(0, remainingNodes - used);
143
+ }
144
+ };
145
+ const p2SpRange = p2Tables.moveTables.slicePermutation.range;
146
+ const p2cpsp = p2Tables.pruningTables.cornerPermutationSlicePermutation.values;
147
+ const p2uepsp = p2Tables.pruningTables.udEdgePermutationSlicePermutation.values;
148
+ const seenG1 = new Set();
149
+ let bestP1 = phase1;
150
+ let bestP2 = phase2;
151
+ let bestMoves = moves;
152
+ let improvementNodes = 0;
153
+ const minDepth = phase1.depth;
154
+ const scored = [];
155
+ for (let d = minDepth; d <= minDepth + improveDepths && d < bestMoves.length; d++) {
156
+ if (budgetExhausted())
157
+ break;
158
+ const { solutions: p1Solutions, visitedNodes } = await (0, phase1_js_1.collectCube3Phase1Solutions)(state, d, {
159
+ tables: p1Tables,
160
+ limit: solutionLimit,
161
+ ...(nodeMode
162
+ ? { maxNodes: Math.max(1, remainingNodes) }
163
+ : { timeoutMs: Math.max(1, deadline - performance.now()) })
164
+ });
165
+ improvementNodes += visitedNodes;
166
+ spendNodes(visitedNodes);
167
+ if (budgetExhausted())
168
+ break;
169
+ for (const p1Moves of p1Solutions) {
170
+ if (budgetExhausted())
171
+ break;
172
+ if (p1Moves.length >= bestMoves.length)
173
+ continue;
174
+ // Skip the phase-1 solution the initial search already completed.
175
+ if (p1Moves.length === phase1.moves.length &&
176
+ p1Moves.every((move, index) => move === phase1.moves[index])) {
177
+ continue;
178
+ }
179
+ const s1 = (0, moves_js_1.applyCube3Algorithm)(state, p1Moves);
180
+ const coords = encodeCube3Phase2Coordinates(s1);
181
+ const estKey = `${coords.cornerPermutation},${coords.udEdgePermutation},${coords.slicePermutation}`;
182
+ if (seenG1.has(estKey))
183
+ continue;
184
+ seenG1.add(estKey);
185
+ const cpIndex = coords.cornerPermutation * p2SpRange + coords.slicePermutation;
186
+ const epIndex = coords.udEdgePermutation * p2SpRange + coords.slicePermutation;
187
+ const cpDist = (p2cpsp[cpIndex >> 1] >> ((cpIndex & 1) << 2)) & 0x0f;
188
+ const epDist = (p2uepsp[epIndex >> 1] >> ((epIndex & 1) << 2)) & 0x0f;
189
+ let est = p1Moves.length + (cpDist > epDist ? cpDist : epDist);
190
+ if (scoredPairTables !== undefined) {
191
+ const pair = (0, pair_tables_js_1.pairBound)(scoredPairTables, coords.cornerPermutation, coords.udEdgePermutation);
192
+ const pairEst = p1Moves.length + pair;
193
+ if (pairEst > est)
194
+ est = pairEst;
195
+ }
196
+ // A lower bound at or above the best total can never strictly improve it.
197
+ if (est >= bestMoves.length)
198
+ continue;
199
+ const pFace = p1Moves.length > 0 ? (0, search_utils_js_1.getMoveFace)(p1Moves[p1Moves.length - 1]) : -1;
200
+ const ppFace = p1Moves.length > 1 ? (0, search_utils_js_1.getMoveFace)(p1Moves[p1Moves.length - 2]) : -1;
201
+ scored.push({ moves: p1Moves, state: s1, est, pFace, ppFace });
202
+ }
203
+ }
204
+ // Sort ALL candidates globally across depths by admissible total lower bound
205
+ scored.sort((left, right) => left.est - right.est);
206
+ for (const cand of scored) {
207
+ if (budgetExhausted())
208
+ break;
209
+ // Prune: since candidates are sorted by est, no subsequent candidate can beat bestMoves.length
210
+ if (cand.est >= bestMoves.length)
211
+ break;
212
+ const maxP2 = Math.min(bestMoves.length - cand.moves.length - 1, 15);
213
+ if (maxP2 <= 0)
214
+ continue;
215
+ try {
216
+ // Strip any legacy wall `timeoutMs` from the spread: in node mode
217
+ // the wall deadline must not be armed at all (pure function of
218
+ // budgets); in wall mode the per-candidate cap below re-adds it.
219
+ const phase2Source = options.phase2 ?? {};
220
+ const { timeoutMs: legacyPhase2Wall, ...phase2CandidateBase } = phase2Source;
221
+ // Per-candidate budget: node cap (deterministic) when the loop runs
222
+ // on `improveMaxNodes`, else the legacy wall cap.
223
+ const candidateCap = nodeMode
224
+ ? {
225
+ maxNodes: Math.max(1, Math.min(options.improvePhase2MaxNodes ?? phase2CandidateBase.maxNodes ?? remainingNodes, remainingNodes))
226
+ }
227
+ : {
228
+ timeoutMs: Math.min(options.improvePhase2TimeoutMs ?? legacyPhase2Wall ?? 25, Math.max(1, deadline - performance.now()))
229
+ };
230
+ const candP2 = await solveCube3Phase2(cand.state, {
231
+ ...phase2CandidateBase,
232
+ pairTables: scoredPairTables,
233
+ tables: p2Tables,
234
+ maxDepth: maxP2,
235
+ previousFace: cand.pFace,
236
+ previousPreviousFace: cand.ppFace,
237
+ ...candidateCap
238
+ });
239
+ improvementNodes += candP2.visitedNodes;
240
+ spendNodes(candP2.visitedNodes);
241
+ const candTotal = cand.moves.length + candP2.moves.length;
242
+ if (candTotal < bestMoves.length) {
243
+ bestMoves = [...cand.moves, ...candP2.moves];
244
+ bestP1 = {
245
+ moves: cand.moves,
246
+ depth: cand.moves.length,
247
+ state: cand.state,
248
+ coordinates: (0, phase1_js_1.encodeCube3Phase1Coordinates)(cand.state),
249
+ visitedNodes: 0
250
+ };
251
+ bestP2 = candP2;
252
+ if (targetLength !== undefined && bestMoves.length <= targetLength) {
253
+ break;
254
+ }
255
+ }
256
+ }
257
+ catch {
258
+ // continue
259
+ }
260
+ }
261
+ return {
262
+ phase1: bestP1,
263
+ phase2: bestP2,
264
+ moves: bestMoves,
265
+ depth: bestMoves.length,
266
+ state: bestP2.state,
267
+ visitedNodes: phase1.visitedNodes + phase2.visitedNodes + improvementNodes
268
+ };
269
+ }
119
270
  return {
120
271
  phase1,
121
272
  phase2,
@@ -125,6 +276,39 @@ async function solveCube3State(state, options = {}) {
125
276
  visitedNodes: phase1.visitedNodes + phase2.visitedNodes
126
277
  };
127
278
  }
279
+ /**
280
+ * Best-of-N-orientations solve: UD plus rotated copies, mapped back with
281
+ * the inverse rotation. Rotating a solution conjugates the whole picture,
282
+ * so the mapped-back moves solve the original state with their length
283
+ * preserved. Deterministic: ties keep the earliest orientation (UD first).
284
+ */
285
+ async function solveCube3TripleOrientation(state, options) {
286
+ const count = Math.min(Math.max(options.orientations ?? 1, 1), symmetry_js_1.CUBE3_ORIENTATIONS_24.length);
287
+ const singleOptions = { ...options, orientations: 1 };
288
+ let best = await solveCube3State(state, singleOptions);
289
+ let visitedNodes = best.visitedNodes;
290
+ const scramble = (0, moves_js_1.invertCube3Algorithm)(best.moves);
291
+ const sequences = symmetry_js_1.CUBE3_ORIENTATIONS_24.slice(1, count);
292
+ for (const sequence of sequences) {
293
+ try {
294
+ const rotatedState = (0, moves_js_1.applyCube3Algorithm)((0, state_js_1.createSolvedCube3State)(), (0, symmetry_js_1.rotateCube3AlgorithmBySequence)(scramble, sequence));
295
+ const candidate = await solveCube3State(rotatedState, singleOptions);
296
+ visitedNodes += candidate.visitedNodes;
297
+ if (candidate.moves.length < best.moves.length) {
298
+ best = {
299
+ ...candidate,
300
+ moves: (0, symmetry_js_1.rotateCube3AlgorithmBySequence)(candidate.moves, (0, symmetry_js_1.invertCube3RotationSequence)(sequence)),
301
+ depth: candidate.moves.length,
302
+ visitedNodes
303
+ };
304
+ }
305
+ }
306
+ catch {
307
+ // A flaky orientation must never fail the whole solve.
308
+ }
309
+ }
310
+ return { ...best, visitedNodes };
311
+ }
128
312
  function encodeCube3Phase2Coordinates(state) {
129
313
  return {
130
314
  cornerPermutation: encodeCornerPermutationForPhase2(state),
@@ -147,10 +331,38 @@ const PHASE2_MOVE_FACES = tables_js_1.CUBE3_PHASE2_MOVES.map((move) => {
147
331
  const char = move[0];
148
332
  return char === "U" ? 0 : char === "D" ? 1 : char === "R" ? 2 : char === "L" ? 3 : char === "F" ? 4 : 5;
149
333
  });
334
+ function createPhase2SearchContext(tables, pairTables, maxDepth, deadline, nodeCap = undefined) {
335
+ const scratchLevels = maxDepth + 1;
336
+ const scratchSize = scratchLevels * tables_js_1.CUBE3_PHASE2_MOVES.length;
337
+ return {
338
+ tables,
339
+ pairTables,
340
+ maxDepth,
341
+ deadline,
342
+ nodeCap,
343
+ visitedNodes: 0,
344
+ timedOut: false,
345
+ nodesExceeded: false,
346
+ scratchMove: new Int32Array(scratchSize),
347
+ scratchBound: new Int32Array(scratchSize),
348
+ scratchCp: new Int32Array(scratchSize),
349
+ scratchUep: new Int32Array(scratchSize),
350
+ scratchSp: new Int32Array(scratchSize)
351
+ };
352
+ }
150
353
  function searchPhase2(cornerPermutation, udEdgePermutation, slicePermutation, previousFace, previousPreviousFace, remainingDepth, path, context) {
151
354
  context.visitedNodes += 1;
152
- if (context.deadline !== undefined && (context.visitedNodes & 1023) === 0 && performance.now() > context.deadline) {
355
+ if (context.nodeCap !== undefined &&
356
+ context.visitedNodes > context.nodeCap) {
357
+ context.nodesExceeded = true;
358
+ }
359
+ if (!context.nodesExceeded &&
360
+ context.deadline !== undefined &&
361
+ (context.visitedNodes & 1023) === 0 &&
362
+ performance.now() > context.deadline) {
153
363
  context.timedOut = true;
364
+ }
365
+ if (context.timedOut || context.nodesExceeded) {
154
366
  return false;
155
367
  }
156
368
  if (remainingDepth === 0) {
@@ -166,41 +378,23 @@ function searchPhase2(cornerPermutation, udEdgePermutation, slicePermutation, pr
166
378
  const spRange = context.tables.moveTables.slicePermutation.range;
167
379
  const cpspTable = context.tables.pruningTables.cornerPermutationSlicePermutation.values;
168
380
  const epspTable = context.tables.pruningTables.udEdgePermutationSlicePermutation.values;
381
+ const pairTables = context.pairTables;
169
382
  const cpOffset = cornerPermutation * moveCount;
170
383
  const epOffset = udEdgePermutation * moveCount;
171
384
  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 = [];
385
+ // Collect the surviving children with the pair-tightened lower bound and
386
+ // explore them in ascending bound order (stable: ties keep the reference
387
+ // move order). Without the pair tables this reduces to ordering by
388
+ // max(cpsp, uepsp) — same depth-minimal solutions, far fewer nodes than
389
+ // the reference traversal. With them, same set and order as before but
390
+ // without per-node allocation.
391
+ const slot = remainingDepth * moveCount;
392
+ const scratchMove = context.scratchMove;
393
+ const scratchBound = context.scratchBound;
394
+ const scratchCp = context.scratchCp;
395
+ const scratchUep = context.scratchUep;
396
+ const scratchSp = context.scratchSp;
397
+ let childCount = 0;
204
398
  for (let i = 0; i < validMoves.length; i += 1) {
205
399
  const moveIndex = validMoves[i];
206
400
  const nextCornerPermutation = cpTable[cpOffset + moveIndex];
@@ -214,29 +408,52 @@ function searchPhase2(cornerPermutation, udEdgePermutation, slicePermutation, pr
214
408
  if (nextLowerBound > remainingDepth - 1) {
215
409
  continue;
216
410
  }
217
- const pair = (0, pair_tables_js_1.pairBound)(pairTables, nextCornerPermutation, nextUdEdgePermutation);
218
- if (pair > nextLowerBound) {
219
- nextLowerBound = pair;
411
+ if (pairTables !== undefined) {
412
+ const pair = (0, pair_tables_js_1.pairBound)(pairTables, nextCornerPermutation, nextUdEdgePermutation);
413
+ if (pair > nextLowerBound) {
414
+ nextLowerBound = pair;
415
+ }
416
+ if (nextLowerBound > remainingDepth - 1) {
417
+ continue;
418
+ }
220
419
  }
221
- if (nextLowerBound > remainingDepth - 1) {
222
- continue;
420
+ scratchMove[slot + childCount] = moveIndex;
421
+ scratchBound[slot + childCount] = nextLowerBound;
422
+ scratchCp[slot + childCount] = nextCornerPermutation;
423
+ scratchUep[slot + childCount] = nextUdEdgePermutation;
424
+ scratchSp[slot + childCount] = nextSlicePermutation;
425
+ childCount += 1;
426
+ }
427
+ // Stable insertion sort by bound (n <= 10; ties keep reference order).
428
+ for (let i = 1; i < childCount; i += 1) {
429
+ const move = scratchMove[slot + i];
430
+ const bound = scratchBound[slot + i];
431
+ const cp = scratchCp[slot + i];
432
+ const uep = scratchUep[slot + i];
433
+ const sp = scratchSp[slot + i];
434
+ let j = i - 1;
435
+ while (j >= 0 && scratchBound[slot + j] > bound) {
436
+ scratchMove[slot + j + 1] = scratchMove[slot + j];
437
+ scratchBound[slot + j + 1] = scratchBound[slot + j];
438
+ scratchCp[slot + j + 1] = scratchCp[slot + j];
439
+ scratchUep[slot + j + 1] = scratchUep[slot + j];
440
+ scratchSp[slot + j + 1] = scratchSp[slot + j];
441
+ j -= 1;
223
442
  }
224
- children.push({
225
- moveIndex,
226
- lowerBound: nextLowerBound,
227
- cornerPermutation: nextCornerPermutation,
228
- udEdgePermutation: nextUdEdgePermutation,
229
- slicePermutation: nextSlicePermutation
230
- });
443
+ scratchMove[slot + j + 1] = move;
444
+ scratchBound[slot + j + 1] = bound;
445
+ scratchCp[slot + j + 1] = cp;
446
+ scratchUep[slot + j + 1] = uep;
447
+ scratchSp[slot + j + 1] = sp;
231
448
  }
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)) {
449
+ for (let i = 0; i < childCount; i += 1) {
450
+ const moveIndex = scratchMove[slot + i];
451
+ path.push(context.tables.phase2Moves[moveIndex]);
452
+ if (searchPhase2(scratchCp[slot + i], scratchUep[slot + i], scratchSp[slot + i], PHASE2_MOVE_FACES[moveIndex], previousFace, remainingDepth - 1, path, context)) {
236
453
  return true;
237
454
  }
238
455
  path.pop();
239
- if (context.timedOut) {
456
+ if (context.timedOut || context.nodesExceeded) {
240
457
  return false;
241
458
  }
242
459
  }
@@ -281,13 +498,7 @@ function searchPhase1Interleaved(cornerOrientation, edgeOrientation, udSlice, pr
281
498
  const g1State = applyCube3Phase2Moves(context.originalState, path);
282
499
  const start2 = encodeCube3Phase2Coordinates(g1State);
283
500
  const maxPhase2Depth = 6;
284
- const phase2Context = {
285
- tables: context.phase2Tables,
286
- pairTables: undefined,
287
- deadline: context.deadline,
288
- visitedNodes: 0,
289
- timedOut: false
290
- };
501
+ const phase2Context = createPhase2SearchContext(context.phase2Tables, undefined, maxPhase2Depth, context.deadline);
291
502
  for (let d2 = 0; d2 <= maxPhase2Depth; d2 += 1) {
292
503
  const path2 = [];
293
504
  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
+ }
@@ -16,6 +16,7 @@ const types_js_1 = require("../types.js");
16
16
  const coordinates_js_1 = require("./coordinates.js");
17
17
  const state_js_1 = require("../state.js");
18
18
  const generated_assets_js_1 = require("./generated-assets.js");
19
+ const load_once_js_1 = require("../../shared/load-once.js");
19
20
  exports.CUBE3_PHASE1_MOVES = types_js_1.CUBE3_MOVES;
20
21
  exports.CUBE3_PHASE2_MOVES = [
21
22
  "U",
@@ -29,20 +30,21 @@ exports.CUBE3_PHASE2_MOVES = [
29
30
  "F2",
30
31
  "B2"
31
32
  ];
32
- let cachedTables;
33
- let cachedPhase1Tables;
34
- let cachedPhase2Tables;
35
- async function loadCube3SolverTables() {
36
- cachedTables ??= await (0, generated_assets_js_1.loadGeneratedCube3SolverTables)();
37
- return cachedTables;
33
+ // Promise slots (shared helper, see shared/load-once.ts): concurrent cold
34
+ // calls share ONE in-flight load instead of racing several loads and
35
+ // clobbering each other's results; a rejection purges the slot so the next
36
+ // call can retry.
37
+ const solverTablesSlot = { promise: undefined };
38
+ const phase1TablesSlot = { promise: undefined };
39
+ const phase2TablesSlot = { promise: undefined };
40
+ function loadCube3SolverTables() {
41
+ return (0, load_once_js_1.loadOnce)(solverTablesSlot, generated_assets_js_1.loadGeneratedCube3SolverTables);
38
42
  }
39
- async function loadCube3Phase1Tables() {
40
- cachedPhase1Tables ??= await (0, generated_assets_js_1.loadGeneratedCube3Phase1Tables)();
41
- return cachedPhase1Tables;
43
+ function loadCube3Phase1Tables() {
44
+ return (0, load_once_js_1.loadOnce)(phase1TablesSlot, generated_assets_js_1.loadGeneratedCube3Phase1Tables);
42
45
  }
43
- async function loadCube3Phase2Tables() {
44
- cachedPhase2Tables ??= await (0, generated_assets_js_1.loadGeneratedCube3Phase2Tables)();
45
- return cachedPhase2Tables;
46
+ function loadCube3Phase2Tables() {
47
+ return (0, load_once_js_1.loadOnce)(phase2TablesSlot, generated_assets_js_1.loadGeneratedCube3Phase2Tables);
46
48
  }
47
49
  function generateCube3Phase1Tables() {
48
50
  const cornerOrientation = generateCube3MoveTable("cornerOrientation", coordinates_js_1.CUBE3_COORDINATE_RANGES.cornerOrientation, exports.CUBE3_PHASE1_MOVES);
@@ -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 = [];