@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
@@ -1,10 +1,11 @@
1
- import { applyCube3Algorithm } from "../moves.js";
2
- import { isSolvedCube3State } from "../state.js";
1
+ import { applyCube3Algorithm, invertCube3Algorithm } from "../moves.js";
2
+ import { createSolvedCube3State, isSolvedCube3State } from "../state.js";
3
3
  import { encodePermutation, encodeSlicePermutation, encodeUdEdgePermutation } from "./coordinates.js";
4
- import { isCube3Phase1Solved, solveCube3Phase1 } from "./phase1.js";
4
+ import { isCube3Phase1Solved, solveCube3Phase1, collectCube3Phase1Solutions, encodeCube3Phase1Coordinates } from "./phase1.js";
5
5
  import { loadCube3Phase1Tables, loadCube3Phase2Tables, CUBE3_PHASE1_MOVES, CUBE3_PHASE2_MOVES } from "./tables.js";
6
6
  import { getMoveFace, getMoveFaceByIndex, shouldSkipCube3SearchMove } from "./search-utils.js";
7
7
  import { loadCube3PairTables, pairBound } from "./pair-tables.js";
8
+ import { CUBE3_ORIENTATIONS_24, invertCube3RotationSequence, rotateCube3AlgorithmBySequence } from "./symmetry.js";
8
9
  export class Cube3Phase2SearchError extends Error {
9
10
  visitedNodes;
10
11
  constructor(message, visitedNodes) {
@@ -41,17 +42,11 @@ export async function solveCube3Phase2(state, options = {}) {
41
42
  throw new Cube3Phase2SearchError("3x3 Phase 2 requires a state already in G1.", 0);
42
43
  }
43
44
  const tables = options.tables ?? await loadCube3Phase2Tables();
44
- const pairTables = options.pairTables ?? await loadCube3PairTables();
45
+ const pairTables = options.pairTables ?? (options.skipPairTables ? undefined : await loadCube3PairTables());
45
46
  const maxDepth = options.maxDepth ?? 18;
46
47
  const start = encodeCube3Phase2Coordinates(state);
47
48
  const deadline = options.timeoutMs === undefined ? undefined : performance.now() + options.timeoutMs;
48
- const context = {
49
- tables,
50
- pairTables,
51
- deadline,
52
- visitedNodes: 0,
53
- timedOut: false
54
- };
49
+ const context = createPhase2SearchContext(tables, pairTables, maxDepth, deadline, options.maxNodes);
55
50
  const spRange = tables.moveTables.slicePermutation.range;
56
51
  const cpspTable = tables.pruningTables.cornerPermutationSlicePermutation.values;
57
52
  const epspTable = tables.pruningTables.udEdgePermutationSlicePermutation.values;
@@ -82,6 +77,9 @@ export async function solveCube3Phase2(state, options = {}) {
82
77
  visitedNodes: context.visitedNodes
83
78
  };
84
79
  }
80
+ if (context.nodesExceeded) {
81
+ throw new Cube3Phase2SearchError(`3x3 Phase 2 node budget exceeded (${options.maxNodes} nodes).`, context.visitedNodes);
82
+ }
85
83
  if (context.timedOut) {
86
84
  throw new Cube3Phase2SearchError(`3x3 Phase 2 search timed out after ${options.timeoutMs}ms.`, context.visitedNodes);
87
85
  }
@@ -89,6 +87,11 @@ export async function solveCube3Phase2(state, options = {}) {
89
87
  throw new Cube3Phase2SearchError(`3x3 Phase 2 solution not found within depth ${maxDepth}.`, context.visitedNodes);
90
88
  }
91
89
  export async function solveCube3State(state, options = {}) {
90
+ const quality = options.quality ?? "short";
91
+ const orientations = options.orientations ?? (quality === "best" ? 3 : 1);
92
+ if (orientations >= 3) {
93
+ return solveCube3TripleOrientation(state, { ...options, orientations });
94
+ }
92
95
  const phase1 = await solveCube3Phase1(state, options.phase1);
93
96
  let previousFace = -1;
94
97
  let previousPreviousFace = -1;
@@ -107,6 +110,154 @@ export async function solveCube3State(state, options = {}) {
107
110
  };
108
111
  const phase2 = await solveCube3Phase2(phase1.state, phase2Options);
109
112
  const moves = [...phase1.moves, ...phase2.moves];
113
+ const targetLength = options.targetLength;
114
+ const extraDepth = options.phase1ExtraDepth ?? (quality === "fast" ? 0 : 1);
115
+ const solutionLimit = options.phase1SolutionLimit ?? (targetLength !== undefined ? 32 : 8);
116
+ const improveDepths = targetLength !== undefined ? Math.max(4, extraDepth) : extraDepth;
117
+ const needsTarget = targetLength !== undefined && moves.length > targetLength;
118
+ if (needsTarget || (targetLength === undefined && extraDepth > 0)) {
119
+ const p1Tables = options.phase1?.tables ?? await loadCube3Phase1Tables();
120
+ const p2Tables = options.phase2?.tables ?? await loadCube3Phase2Tables();
121
+ const scoredPairTables = options.phase2?.pairTables ??
122
+ (options.phase2?.skipPairTables ? undefined : await loadCube3PairTables());
123
+ // Work budget for the improvement loop. Preferred: `improveMaxNodes`
124
+ // (deterministic node counter — the wall deadline stays unarmed so the
125
+ // outcome cannot depend on CPU load/clock). Legacy: `timeoutMs ?? 800`
126
+ // wall deadline, kept for existing callers.
127
+ const nodeMode = options.improveMaxNodes !== undefined;
128
+ let remainingNodes = options.improveMaxNodes ?? 0;
129
+ const deadline = nodeMode ? undefined : performance.now() + (options.timeoutMs ?? 800);
130
+ const budgetExhausted = () => nodeMode ? remainingNodes <= 0 : performance.now() > deadline;
131
+ const spendNodes = (used) => {
132
+ if (nodeMode) {
133
+ remainingNodes = Math.max(0, remainingNodes - used);
134
+ }
135
+ };
136
+ const p2SpRange = p2Tables.moveTables.slicePermutation.range;
137
+ const p2cpsp = p2Tables.pruningTables.cornerPermutationSlicePermutation.values;
138
+ const p2uepsp = p2Tables.pruningTables.udEdgePermutationSlicePermutation.values;
139
+ const seenG1 = new Set();
140
+ let bestP1 = phase1;
141
+ let bestP2 = phase2;
142
+ let bestMoves = moves;
143
+ let improvementNodes = 0;
144
+ const minDepth = phase1.depth;
145
+ const scored = [];
146
+ for (let d = minDepth; d <= minDepth + improveDepths && d < bestMoves.length; d++) {
147
+ if (budgetExhausted())
148
+ break;
149
+ const { solutions: p1Solutions, visitedNodes } = await collectCube3Phase1Solutions(state, d, {
150
+ tables: p1Tables,
151
+ limit: solutionLimit,
152
+ ...(nodeMode
153
+ ? { maxNodes: Math.max(1, remainingNodes) }
154
+ : { timeoutMs: Math.max(1, deadline - performance.now()) })
155
+ });
156
+ improvementNodes += visitedNodes;
157
+ spendNodes(visitedNodes);
158
+ if (budgetExhausted())
159
+ break;
160
+ for (const p1Moves of p1Solutions) {
161
+ if (budgetExhausted())
162
+ break;
163
+ if (p1Moves.length >= bestMoves.length)
164
+ continue;
165
+ // Skip the phase-1 solution the initial search already completed.
166
+ if (p1Moves.length === phase1.moves.length &&
167
+ p1Moves.every((move, index) => move === phase1.moves[index])) {
168
+ continue;
169
+ }
170
+ const s1 = applyCube3Algorithm(state, p1Moves);
171
+ const coords = encodeCube3Phase2Coordinates(s1);
172
+ const estKey = `${coords.cornerPermutation},${coords.udEdgePermutation},${coords.slicePermutation}`;
173
+ if (seenG1.has(estKey))
174
+ continue;
175
+ seenG1.add(estKey);
176
+ const cpIndex = coords.cornerPermutation * p2SpRange + coords.slicePermutation;
177
+ const epIndex = coords.udEdgePermutation * p2SpRange + coords.slicePermutation;
178
+ const cpDist = (p2cpsp[cpIndex >> 1] >> ((cpIndex & 1) << 2)) & 0x0f;
179
+ const epDist = (p2uepsp[epIndex >> 1] >> ((epIndex & 1) << 2)) & 0x0f;
180
+ let est = p1Moves.length + (cpDist > epDist ? cpDist : epDist);
181
+ if (scoredPairTables !== undefined) {
182
+ const pair = pairBound(scoredPairTables, coords.cornerPermutation, coords.udEdgePermutation);
183
+ const pairEst = p1Moves.length + pair;
184
+ if (pairEst > est)
185
+ est = pairEst;
186
+ }
187
+ // A lower bound at or above the best total can never strictly improve it.
188
+ if (est >= bestMoves.length)
189
+ continue;
190
+ const pFace = p1Moves.length > 0 ? getMoveFace(p1Moves[p1Moves.length - 1]) : -1;
191
+ const ppFace = p1Moves.length > 1 ? getMoveFace(p1Moves[p1Moves.length - 2]) : -1;
192
+ scored.push({ moves: p1Moves, state: s1, est, pFace, ppFace });
193
+ }
194
+ }
195
+ // Sort ALL candidates globally across depths by admissible total lower bound
196
+ scored.sort((left, right) => left.est - right.est);
197
+ for (const cand of scored) {
198
+ if (budgetExhausted())
199
+ break;
200
+ // Prune: since candidates are sorted by est, no subsequent candidate can beat bestMoves.length
201
+ if (cand.est >= bestMoves.length)
202
+ break;
203
+ const maxP2 = Math.min(bestMoves.length - cand.moves.length - 1, 15);
204
+ if (maxP2 <= 0)
205
+ continue;
206
+ try {
207
+ // Strip any legacy wall `timeoutMs` from the spread: in node mode
208
+ // the wall deadline must not be armed at all (pure function of
209
+ // budgets); in wall mode the per-candidate cap below re-adds it.
210
+ const phase2Source = options.phase2 ?? {};
211
+ const { timeoutMs: legacyPhase2Wall, ...phase2CandidateBase } = phase2Source;
212
+ // Per-candidate budget: node cap (deterministic) when the loop runs
213
+ // on `improveMaxNodes`, else the legacy wall cap.
214
+ const candidateCap = nodeMode
215
+ ? {
216
+ maxNodes: Math.max(1, Math.min(options.improvePhase2MaxNodes ?? phase2CandidateBase.maxNodes ?? remainingNodes, remainingNodes))
217
+ }
218
+ : {
219
+ timeoutMs: Math.min(options.improvePhase2TimeoutMs ?? legacyPhase2Wall ?? 25, Math.max(1, deadline - performance.now()))
220
+ };
221
+ const candP2 = await solveCube3Phase2(cand.state, {
222
+ ...phase2CandidateBase,
223
+ pairTables: scoredPairTables,
224
+ tables: p2Tables,
225
+ maxDepth: maxP2,
226
+ previousFace: cand.pFace,
227
+ previousPreviousFace: cand.ppFace,
228
+ ...candidateCap
229
+ });
230
+ improvementNodes += candP2.visitedNodes;
231
+ spendNodes(candP2.visitedNodes);
232
+ const candTotal = cand.moves.length + candP2.moves.length;
233
+ if (candTotal < bestMoves.length) {
234
+ bestMoves = [...cand.moves, ...candP2.moves];
235
+ bestP1 = {
236
+ moves: cand.moves,
237
+ depth: cand.moves.length,
238
+ state: cand.state,
239
+ coordinates: encodeCube3Phase1Coordinates(cand.state),
240
+ visitedNodes: 0
241
+ };
242
+ bestP2 = candP2;
243
+ if (targetLength !== undefined && bestMoves.length <= targetLength) {
244
+ break;
245
+ }
246
+ }
247
+ }
248
+ catch {
249
+ // continue
250
+ }
251
+ }
252
+ return {
253
+ phase1: bestP1,
254
+ phase2: bestP2,
255
+ moves: bestMoves,
256
+ depth: bestMoves.length,
257
+ state: bestP2.state,
258
+ visitedNodes: phase1.visitedNodes + phase2.visitedNodes + improvementNodes
259
+ };
260
+ }
110
261
  return {
111
262
  phase1,
112
263
  phase2,
@@ -116,6 +267,39 @@ export async function solveCube3State(state, options = {}) {
116
267
  visitedNodes: phase1.visitedNodes + phase2.visitedNodes
117
268
  };
118
269
  }
270
+ /**
271
+ * Best-of-N-orientations solve: UD plus rotated copies, mapped back with
272
+ * the inverse rotation. Rotating a solution conjugates the whole picture,
273
+ * so the mapped-back moves solve the original state with their length
274
+ * preserved. Deterministic: ties keep the earliest orientation (UD first).
275
+ */
276
+ async function solveCube3TripleOrientation(state, options) {
277
+ const count = Math.min(Math.max(options.orientations ?? 1, 1), CUBE3_ORIENTATIONS_24.length);
278
+ const singleOptions = { ...options, orientations: 1 };
279
+ let best = await solveCube3State(state, singleOptions);
280
+ let visitedNodes = best.visitedNodes;
281
+ const scramble = invertCube3Algorithm(best.moves);
282
+ const sequences = CUBE3_ORIENTATIONS_24.slice(1, count);
283
+ for (const sequence of sequences) {
284
+ try {
285
+ const rotatedState = applyCube3Algorithm(createSolvedCube3State(), rotateCube3AlgorithmBySequence(scramble, sequence));
286
+ const candidate = await solveCube3State(rotatedState, singleOptions);
287
+ visitedNodes += candidate.visitedNodes;
288
+ if (candidate.moves.length < best.moves.length) {
289
+ best = {
290
+ ...candidate,
291
+ moves: rotateCube3AlgorithmBySequence(candidate.moves, invertCube3RotationSequence(sequence)),
292
+ depth: candidate.moves.length,
293
+ visitedNodes
294
+ };
295
+ }
296
+ }
297
+ catch {
298
+ // A flaky orientation must never fail the whole solve.
299
+ }
300
+ }
301
+ return { ...best, visitedNodes };
302
+ }
119
303
  export function encodeCube3Phase2Coordinates(state) {
120
304
  return {
121
305
  cornerPermutation: encodeCornerPermutationForPhase2(state),
@@ -138,10 +322,38 @@ const PHASE2_MOVE_FACES = CUBE3_PHASE2_MOVES.map((move) => {
138
322
  const char = move[0];
139
323
  return char === "U" ? 0 : char === "D" ? 1 : char === "R" ? 2 : char === "L" ? 3 : char === "F" ? 4 : 5;
140
324
  });
325
+ function createPhase2SearchContext(tables, pairTables, maxDepth, deadline, nodeCap = undefined) {
326
+ const scratchLevels = maxDepth + 1;
327
+ const scratchSize = scratchLevels * CUBE3_PHASE2_MOVES.length;
328
+ return {
329
+ tables,
330
+ pairTables,
331
+ maxDepth,
332
+ deadline,
333
+ nodeCap,
334
+ visitedNodes: 0,
335
+ timedOut: false,
336
+ nodesExceeded: false,
337
+ scratchMove: new Int32Array(scratchSize),
338
+ scratchBound: new Int32Array(scratchSize),
339
+ scratchCp: new Int32Array(scratchSize),
340
+ scratchUep: new Int32Array(scratchSize),
341
+ scratchSp: new Int32Array(scratchSize)
342
+ };
343
+ }
141
344
  function searchPhase2(cornerPermutation, udEdgePermutation, slicePermutation, previousFace, previousPreviousFace, remainingDepth, path, context) {
142
345
  context.visitedNodes += 1;
143
- if (context.deadline !== undefined && (context.visitedNodes & 1023) === 0 && performance.now() > context.deadline) {
346
+ if (context.nodeCap !== undefined &&
347
+ context.visitedNodes > context.nodeCap) {
348
+ context.nodesExceeded = true;
349
+ }
350
+ if (!context.nodesExceeded &&
351
+ context.deadline !== undefined &&
352
+ (context.visitedNodes & 1023) === 0 &&
353
+ performance.now() > context.deadline) {
144
354
  context.timedOut = true;
355
+ }
356
+ if (context.timedOut || context.nodesExceeded) {
145
357
  return false;
146
358
  }
147
359
  if (remainingDepth === 0) {
@@ -157,41 +369,23 @@ function searchPhase2(cornerPermutation, udEdgePermutation, slicePermutation, pr
157
369
  const spRange = context.tables.moveTables.slicePermutation.range;
158
370
  const cpspTable = context.tables.pruningTables.cornerPermutationSlicePermutation.values;
159
371
  const epspTable = context.tables.pruningTables.udEdgePermutationSlicePermutation.values;
372
+ const pairTables = context.pairTables;
160
373
  const cpOffset = cornerPermutation * moveCount;
161
374
  const epOffset = udEdgePermutation * moveCount;
162
375
  const spOffset = slicePermutation * moveCount;
163
- // Without the pair tables (the default npm package), keep the exact
164
- // reference traversal — zero overhead. With them (turbo tier), collect the
165
- // children with the pair-distance bound and explore in ascending bound
166
- // order (stable: ties keep the reference move order): same depth-minimal
167
- // solutions, far fewer nodes.
168
- if (context.pairTables === undefined) {
169
- for (let i = 0; i < validMoves.length; i += 1) {
170
- const moveIndex = validMoves[i];
171
- const nextCornerPermutation = cpTable[cpOffset + moveIndex];
172
- const nextUdEdgePermutation = epTable[epOffset + moveIndex];
173
- const nextSlicePermutation = spTable[spOffset + moveIndex];
174
- const combinedCPIndex = nextCornerPermutation * spRange + nextSlicePermutation;
175
- const combinedEPIndex = nextUdEdgePermutation * spRange + nextSlicePermutation;
176
- const cpDist = (cpspTable[combinedCPIndex >> 1] >> ((combinedCPIndex & 1) << 2)) & 0x0f;
177
- const epDist = (epspTable[combinedEPIndex >> 1] >> ((combinedEPIndex & 1) << 2)) & 0x0f;
178
- const nextLowerBound = cpDist > epDist ? cpDist : epDist;
179
- if (nextLowerBound > remainingDepth - 1) {
180
- continue;
181
- }
182
- path.push(context.tables.phase2Moves[moveIndex]);
183
- if (searchPhase2(nextCornerPermutation, nextUdEdgePermutation, nextSlicePermutation, PHASE2_MOVE_FACES[moveIndex], previousFace, remainingDepth - 1, path, context)) {
184
- return true;
185
- }
186
- path.pop();
187
- if (context.timedOut) {
188
- return false;
189
- }
190
- }
191
- return false;
192
- }
193
- const pairTables = context.pairTables;
194
- const children = [];
376
+ // Collect the surviving children with the pair-tightened lower bound and
377
+ // explore them in ascending bound order (stable: ties keep the reference
378
+ // move order). Without the pair tables this reduces to ordering by
379
+ // max(cpsp, uepsp) — same depth-minimal solutions, far fewer nodes than
380
+ // the reference traversal. With them, same set and order as before but
381
+ // without per-node allocation.
382
+ const slot = remainingDepth * moveCount;
383
+ const scratchMove = context.scratchMove;
384
+ const scratchBound = context.scratchBound;
385
+ const scratchCp = context.scratchCp;
386
+ const scratchUep = context.scratchUep;
387
+ const scratchSp = context.scratchSp;
388
+ let childCount = 0;
195
389
  for (let i = 0; i < validMoves.length; i += 1) {
196
390
  const moveIndex = validMoves[i];
197
391
  const nextCornerPermutation = cpTable[cpOffset + moveIndex];
@@ -205,29 +399,52 @@ function searchPhase2(cornerPermutation, udEdgePermutation, slicePermutation, pr
205
399
  if (nextLowerBound > remainingDepth - 1) {
206
400
  continue;
207
401
  }
208
- const pair = pairBound(pairTables, nextCornerPermutation, nextUdEdgePermutation);
209
- if (pair > nextLowerBound) {
210
- nextLowerBound = pair;
402
+ if (pairTables !== undefined) {
403
+ const pair = pairBound(pairTables, nextCornerPermutation, nextUdEdgePermutation);
404
+ if (pair > nextLowerBound) {
405
+ nextLowerBound = pair;
406
+ }
407
+ if (nextLowerBound > remainingDepth - 1) {
408
+ continue;
409
+ }
211
410
  }
212
- if (nextLowerBound > remainingDepth - 1) {
213
- continue;
411
+ scratchMove[slot + childCount] = moveIndex;
412
+ scratchBound[slot + childCount] = nextLowerBound;
413
+ scratchCp[slot + childCount] = nextCornerPermutation;
414
+ scratchUep[slot + childCount] = nextUdEdgePermutation;
415
+ scratchSp[slot + childCount] = nextSlicePermutation;
416
+ childCount += 1;
417
+ }
418
+ // Stable insertion sort by bound (n <= 10; ties keep reference order).
419
+ for (let i = 1; i < childCount; i += 1) {
420
+ const move = scratchMove[slot + i];
421
+ const bound = scratchBound[slot + i];
422
+ const cp = scratchCp[slot + i];
423
+ const uep = scratchUep[slot + i];
424
+ const sp = scratchSp[slot + i];
425
+ let j = i - 1;
426
+ while (j >= 0 && scratchBound[slot + j] > bound) {
427
+ scratchMove[slot + j + 1] = scratchMove[slot + j];
428
+ scratchBound[slot + j + 1] = scratchBound[slot + j];
429
+ scratchCp[slot + j + 1] = scratchCp[slot + j];
430
+ scratchUep[slot + j + 1] = scratchUep[slot + j];
431
+ scratchSp[slot + j + 1] = scratchSp[slot + j];
432
+ j -= 1;
214
433
  }
215
- children.push({
216
- moveIndex,
217
- lowerBound: nextLowerBound,
218
- cornerPermutation: nextCornerPermutation,
219
- udEdgePermutation: nextUdEdgePermutation,
220
- slicePermutation: nextSlicePermutation
221
- });
434
+ scratchMove[slot + j + 1] = move;
435
+ scratchBound[slot + j + 1] = bound;
436
+ scratchCp[slot + j + 1] = cp;
437
+ scratchUep[slot + j + 1] = uep;
438
+ scratchSp[slot + j + 1] = sp;
222
439
  }
223
- children.sort((left, right) => left.lowerBound - right.lowerBound);
224
- for (const child of children) {
225
- path.push(context.tables.phase2Moves[child.moveIndex]);
226
- if (searchPhase2(child.cornerPermutation, child.udEdgePermutation, child.slicePermutation, PHASE2_MOVE_FACES[child.moveIndex], previousFace, remainingDepth - 1, path, context)) {
440
+ for (let i = 0; i < childCount; i += 1) {
441
+ const moveIndex = scratchMove[slot + i];
442
+ path.push(context.tables.phase2Moves[moveIndex]);
443
+ if (searchPhase2(scratchCp[slot + i], scratchUep[slot + i], scratchSp[slot + i], PHASE2_MOVE_FACES[moveIndex], previousFace, remainingDepth - 1, path, context)) {
227
444
  return true;
228
445
  }
229
446
  path.pop();
230
- if (context.timedOut) {
447
+ if (context.timedOut || context.nodesExceeded) {
231
448
  return false;
232
449
  }
233
450
  }
@@ -272,13 +489,7 @@ function searchPhase1Interleaved(cornerOrientation, edgeOrientation, udSlice, pr
272
489
  const g1State = applyCube3Phase2Moves(context.originalState, path);
273
490
  const start2 = encodeCube3Phase2Coordinates(g1State);
274
491
  const maxPhase2Depth = 6;
275
- const phase2Context = {
276
- tables: context.phase2Tables,
277
- pairTables: undefined,
278
- deadline: context.deadline,
279
- visitedNodes: 0,
280
- timedOut: false
281
- };
492
+ const phase2Context = createPhase2SearchContext(context.phase2Tables, undefined, maxPhase2Depth, context.deadline);
282
493
  for (let d2 = 0; d2 <= maxPhase2Depth; d2 += 1) {
283
494
  const path2 = [];
284
495
  const solved2 = searchPhase2(start2.cornerPermutation, start2.udEdgePermutation, start2.slicePermutation, -1, -1, d2, path2, phase2Context);
@@ -5,5 +5,5 @@ export declare function getMoveFaceByIndex(moveIndex: number, moves: readonly Cu
5
5
  export declare const ARE_OPPOSITE_FACES: readonly boolean[];
6
6
  export declare function areOppositeFaces(left: Cube3SearchFace, right: Cube3SearchFace): boolean;
7
7
  export declare function shouldSkipCube3SearchMove(face: Cube3SearchFace, previousFace: Cube3SearchFace | undefined, previousPreviousFace: Cube3SearchFace | undefined): boolean;
8
- export declare function lookupMoveTableByIndex(values: Int32Array, coordinate: number, moveIndex: number, moveCount: number): number;
8
+ export declare function lookupMoveTableByIndex(values: Int32Array | Uint16Array, coordinate: number, moveIndex: number, moveCount: number): number;
9
9
  //# sourceMappingURL=search-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search-utils.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/search-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAYrC,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAE5D;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,eAAe,CAElG;AAED,eAAO,MAAM,kBAAkB,EAAE,SAAS,OAAO,EAOhD,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAEvF;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,eAAe,EACrB,YAAY,EAAE,eAAe,GAAG,SAAS,EACzC,oBAAoB,EAAE,eAAe,GAAG,SAAS,GAChD,OAAO,CAcT;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,MAAM,CAER"}
1
+ {"version":3,"file":"search-utils.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/search-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAYrC,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAE5D;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,eAAe,CAElG;AAED,eAAO,MAAM,kBAAkB,EAAE,SAAS,OAAO,EAOhD,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAEvF;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,eAAe,EACrB,YAAY,EAAE,eAAe,GAAG,SAAS,EACzC,oBAAoB,EAAE,eAAe,GAAG,SAAS,GAChD,OAAO,CAcT;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,UAAU,GAAG,WAAW,EAChC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,MAAM,CAER"}
@@ -0,0 +1,24 @@
1
+ import type { Cube3Move } from "../types.js";
2
+ /**
3
+ * Whole-cube rotations for 3x3 move sequences.
4
+ *
5
+ * Face maps ported from the 4x4 solver's `ROT_MAP`
6
+ * (`puzzles/cube4/reduction/solve.ts`), restricted to the six basic faces —
7
+ * kept cube3-local so `solver/` never imports from `cube4` (which itself
8
+ * depends on this solver).
9
+ */
10
+ export type Cube3Rotation = "x" | "x'" | "z" | "z'";
11
+ export declare const CUBE3_ROTATION_INVERSE: Record<Cube3Rotation, Cube3Rotation>;
12
+ export type Cube3RotationSequence = readonly string[];
13
+ /**
14
+ * 24 whole-cube orientations as rotation sequences, discovered by BFS over
15
+ * the group generated by x/y/z (canonicalized by face map, so all 24 are
16
+ * geometrically distinct by construction).
17
+ * The first three are identity, x, z — so `orientations: 3` keeps the
18
+ * legacy UD/x/z triple exactly.
19
+ */
20
+ export declare const CUBE3_ORIENTATIONS_24: readonly Cube3RotationSequence[];
21
+ export declare function invertCube3RotationSequence(sequence: Cube3RotationSequence): string[];
22
+ export declare function rotateCube3AlgorithmBySequence(moves: readonly Cube3Move[], sequence: Cube3RotationSequence): Cube3Move[];
23
+ export declare function rotateCube3Algorithm(moves: readonly Cube3Move[], rotation: Cube3Rotation): Cube3Move[];
24
+ //# sourceMappingURL=symmetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symmetry.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/symmetry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC;AAqBpD,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,aAAa,EAAE,aAAa,CAKvE,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,SAAS,MAAM,EAAE,CAAC;AAEtD;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,qBAAqB,EA4B9D,CAAC;AAEL,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,EAAE,CAErF;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,QAAQ,EAAE,qBAAqB,GAC9B,SAAS,EAAE,CAOb;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,QAAQ,EAAE,aAAa,GACtB,SAAS,EAAE,CAGb"}
@@ -0,0 +1,74 @@
1
+ const ROT_FACE = {
2
+ "x": { "U": "B", "R": "R", "F": "U", "D": "F", "L": "L", "B": "D" },
3
+ "x'": { "U": "F", "R": "R", "F": "D", "D": "B", "L": "L", "B": "U" },
4
+ "y": { "U": "U", "R": "F", "F": "L", "D": "D", "L": "B", "B": "R" },
5
+ "y'": { "U": "U", "R": "B", "F": "R", "D": "D", "L": "F", "B": "L" },
6
+ "z": { "U": "R", "R": "D", "F": "F", "D": "L", "L": "U", "B": "B" },
7
+ "z'": { "U": "L", "R": "U", "F": "F", "D": "R", "L": "D", "B": "B" },
8
+ "x2": { "U": "D", "R": "R", "F": "B", "D": "U", "L": "L", "B": "F" },
9
+ "y2": { "U": "U", "R": "L", "F": "B", "D": "D", "L": "R", "B": "F" },
10
+ "z2": { "U": "D", "R": "L", "F": "F", "D": "U", "L": "R", "B": "B" }
11
+ };
12
+ const INVERT_ROTATION = {
13
+ "x": "x'", "x'": "x",
14
+ "y": "y'", "y'": "y",
15
+ "z": "z'", "z'": "z",
16
+ "x2": "x2", "y2": "y2", "z2": "z2"
17
+ };
18
+ export const CUBE3_ROTATION_INVERSE = {
19
+ "x": "x'",
20
+ "x'": "x",
21
+ "z": "z'",
22
+ "z'": "z"
23
+ };
24
+ /**
25
+ * 24 whole-cube orientations as rotation sequences, discovered by BFS over
26
+ * the group generated by x/y/z (canonicalized by face map, so all 24 are
27
+ * geometrically distinct by construction).
28
+ * The first three are identity, x, z — so `orientations: 3` keeps the
29
+ * legacy UD/x/z triple exactly.
30
+ */
31
+ export const CUBE3_ORIENTATIONS_24 = (() => {
32
+ const faces = ["U", "R", "F", "D", "L", "B"];
33
+ const identity = "URFDLB";
34
+ const composeKey = (key, gen) => {
35
+ const map = ROT_FACE[gen];
36
+ return faces.map((face, index) => map[key[index]]).join("");
37
+ };
38
+ const seen = new Map([[identity, []]]);
39
+ const queue = [identity];
40
+ while (queue.length > 0) {
41
+ const key = queue.shift();
42
+ const sequence = seen.get(key);
43
+ for (const gen of ["x", "y", "z"]) {
44
+ const next = composeKey(key, gen);
45
+ if (!seen.has(next)) {
46
+ seen.set(next, [...sequence, gen]);
47
+ queue.push(next);
48
+ }
49
+ }
50
+ }
51
+ const isLegacyTriple = (sequence) => sequence.length === 0 ||
52
+ (sequence.length === 1 && (sequence[0] === "x" || sequence[0] === "z"));
53
+ const rest = [];
54
+ for (const sequence of seen.values()) {
55
+ if (!isLegacyTriple(sequence))
56
+ rest.push(sequence);
57
+ }
58
+ return [[], ["x"], ["z"], ...rest];
59
+ })();
60
+ export function invertCube3RotationSequence(sequence) {
61
+ return [...sequence].reverse().map((rotation) => INVERT_ROTATION[rotation]);
62
+ }
63
+ export function rotateCube3AlgorithmBySequence(moves, sequence) {
64
+ let result = [...moves];
65
+ for (const rotation of sequence) {
66
+ const map = ROT_FACE[rotation];
67
+ result = result.map((move) => `${map[move[0]]}${move.slice(1)}`);
68
+ }
69
+ return result;
70
+ }
71
+ export function rotateCube3Algorithm(moves, rotation) {
72
+ const map = ROT_FACE[rotation];
73
+ return moves.map((move) => `${map[move[0]]}${move.slice(1)}`);
74
+ }
@@ -7,7 +7,12 @@ export interface Cube3MoveTable {
7
7
  moves: readonly Cube3Move[];
8
8
  range: number;
9
9
  solvedCoordinate: number;
10
- values: Int32Array;
10
+ /**
11
+ * Move-table values. Every coordinate fits in 16 bits (max range 40320),
12
+ * so loaders store `Uint16Array` (half the RAM/cache of the int32 wire
13
+ * format); table generation still builds `Int32Array`.
14
+ */
15
+ values: Int32Array | Uint16Array;
11
16
  checksum: number;
12
17
  }
13
18
  export interface Cube3PruningTable {
@@ -1 +1 @@
1
- {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/tables.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/tables.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,7 @@ import { CUBE3_MOVES } from "../types.js";
2
2
  import { CUBE3_COORDINATE_RANGES, encodeCube3Coordinates, moveCoordinate } from "./coordinates.js";
3
3
  import { createSolvedCube3State } from "../state.js";
4
4
  import { loadGeneratedCube3Phase1Tables, loadGeneratedCube3Phase2Tables, loadGeneratedCube3SolverTables } from "./generated-assets.js";
5
+ import { loadOnce } from "../../shared/load-once.js";
5
6
  export const CUBE3_PHASE1_MOVES = CUBE3_MOVES;
6
7
  export const CUBE3_PHASE2_MOVES = [
7
8
  "U",
@@ -15,20 +16,21 @@ export const CUBE3_PHASE2_MOVES = [
15
16
  "F2",
16
17
  "B2"
17
18
  ];
18
- let cachedTables;
19
- let cachedPhase1Tables;
20
- let cachedPhase2Tables;
21
- export async function loadCube3SolverTables() {
22
- cachedTables ??= await loadGeneratedCube3SolverTables();
23
- return cachedTables;
19
+ // Promise slots (shared helper, see shared/load-once.ts): concurrent cold
20
+ // calls share ONE in-flight load instead of racing several loads and
21
+ // clobbering each other's results; a rejection purges the slot so the next
22
+ // call can retry.
23
+ const solverTablesSlot = { promise: undefined };
24
+ const phase1TablesSlot = { promise: undefined };
25
+ const phase2TablesSlot = { promise: undefined };
26
+ export function loadCube3SolverTables() {
27
+ return loadOnce(solverTablesSlot, loadGeneratedCube3SolverTables);
24
28
  }
25
- export async function loadCube3Phase1Tables() {
26
- cachedPhase1Tables ??= await loadGeneratedCube3Phase1Tables();
27
- return cachedPhase1Tables;
29
+ export function loadCube3Phase1Tables() {
30
+ return loadOnce(phase1TablesSlot, loadGeneratedCube3Phase1Tables);
28
31
  }
29
- export async function loadCube3Phase2Tables() {
30
- cachedPhase2Tables ??= await loadGeneratedCube3Phase2Tables();
31
- return cachedPhase2Tables;
32
+ export function loadCube3Phase2Tables() {
33
+ return loadOnce(phase2TablesSlot, loadGeneratedCube3Phase2Tables);
32
34
  }
33
35
  export function generateCube3Phase1Tables() {
34
36
  const cornerOrientation = generateCube3MoveTable("cornerOrientation", CUBE3_COORDINATE_RANGES.cornerOrientation, CUBE3_PHASE1_MOVES);
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/puzzles/cube3/validation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAIpE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,qBAAqB,CA+B3E;AASD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAExE;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAIpE;AAED,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAYvE"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/puzzles/cube3/validation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAIpE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,qBAAqB,CA+B3E;AASD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAExE;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAIpE;AAED,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAmBvE"}