@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
@@ -4,9 +4,9 @@
4
4
  * the reduction solver (phases 1-4), and invert the solution. The scramble
5
5
  * always reproduces the sampled target state and facelets exactly.
6
6
  *
7
- * If the solver fails after 5 attempts it falls back to cubing.js
8
- * `randomScrambleForEvent("444")` (also random-state). If that also fails,
9
- * a WCA-compatible random-moves sequence (40 moves) is returned.
7
+ * Reduction-only, no resampling: each call runs one round of up to `bestOf`
8
+ * independent attempts and returns the shortest success (ties keep the
9
+ * lowest job index). If every attempt fails, the last error is thrown.
10
10
  */
11
11
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
12
  if (k2 === undefined) k2 = k;
@@ -44,100 +44,15 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.cube4ScrambleGenerator = void 0;
46
46
  exports.generateCube4Scramble = generateCube4Scramble;
47
+ exports.generateCube4Attempt = generateCube4Attempt;
47
48
  const moves_js_1 = require("./moves.js");
48
49
  const random_state_js_1 = require("./random-state.js");
49
50
  const state_js_1 = require("./state.js");
50
51
  const facelets_js_1 = require("./facelets.js");
51
52
  const solve_js_1 = require("./reduction/solve.js");
52
53
  const index_js_1 = require("../cube3/solver/index.js");
53
- const index_js_2 = require("../../random/index.js");
54
- // ────────────────────────────────────────────────────────────
55
- // cubing.js fallback (random-state, WCA-compliant)
56
- // ────────────────────────────────────────────────────────────
57
- /**
58
- * Try to generate a 4x4 scramble using cubing.js `randomScrambleForEvent`.
59
- * Uses a dynamic import so browser bundles can tree-shake it when unused.
60
- * Returns null if cubing.js is unavailable.
61
- */
62
- async function generateCube4WithCubingJs() {
63
- try {
64
- const { randomScrambleForEvent } = await Promise.resolve().then(() => __importStar(require(
65
- /* webpackIgnore: true */
66
- /* @vite-ignore */
67
- "cubing/scramble")));
68
- const alg = await randomScrambleForEvent("444");
69
- const scramble = alg.toString();
70
- const moves = scramble.trim().split(/\s+/);
71
- return {
72
- eventId: "444",
73
- scramble,
74
- moves,
75
- quality: "random-state",
76
- generator: "cubing-js",
77
- metadata: {
78
- generator: "cubing-js",
79
- distribution: "random-state",
80
- note: "Generated by cubing.js randomScrambleForEvent as fallback."
81
- }
82
- };
83
- }
84
- catch {
85
- return null;
86
- }
87
- }
88
- // ────────────────────────────────────────────────────────────
89
- // Last-resort: random-moves (no external dep, always works)
90
- // ────────────────────────────────────────────────────────────
91
- const CUBE4_FACES = ["U", "R", "F", "D", "L", "B"];
92
- // Opposite face indices (U↔D, R↔L, F↔B)
93
- const CUBE4_OPPOSITE = { U: "D", D: "U", R: "L", L: "R", F: "B", B: "F" };
94
- const CUBE4_SUFFIXES = ["", "2", "'"];
95
- const CUBE4_WIDE_FACES = ["Uw", "Rw", "Fw", "Dw", "Lw", "Bw"];
96
- // Opposite wide-move faces
97
- const CUBE4_WIDE_OPPOSITE = {
98
- Uw: "Dw", Dw: "Uw", Rw: "Lw", Lw: "Rw", Fw: "Bw", Bw: "Fw"
99
- };
100
- // All available move bases (outer + wide). Outer and wide turns are treated
101
- // independently for the same-face/opposite-face check using their base letter.
102
- const ALL_MOVE_BASES = [...CUBE4_FACES, ...CUBE4_WIDE_FACES];
103
- function generateCube4RandomMoves(options = {}) {
104
- const rng = (0, index_js_2.createRandomSource)(options.seed);
105
- const moveCount = 40;
106
- const moves = [];
107
- let lastFace = null;
108
- let secondLastFace = null;
109
- const getBaseLetter = (face) => face.length > 1 ? face[0] : face;
110
- for (let i = 0; i < moveCount; i++) {
111
- let face;
112
- // Reject same face as last or opposite face when the last two moves
113
- // share the same axis (to avoid redundant move pairs like R L R).
114
- do {
115
- face = ALL_MOVE_BASES[rng.nextInt(ALL_MOVE_BASES.length)];
116
- } while (face === lastFace ||
117
- (lastFace !== null &&
118
- (CUBE4_OPPOSITE[face] === lastFace || CUBE4_WIDE_OPPOSITE[face] === lastFace) &&
119
- secondLastFace !== null &&
120
- getBaseLetter(secondLastFace) === getBaseLetter(face)));
121
- const suffix = CUBE4_SUFFIXES[rng.nextInt(3)];
122
- moves.push(`${face}${suffix}`);
123
- secondLastFace = lastFace;
124
- lastFace = face;
125
- }
126
- const scramble = moves.join(" ");
127
- return {
128
- eventId: "444",
129
- scramble,
130
- moves,
131
- quality: "random-moves",
132
- generator: "cube4-random-moves-fallback",
133
- metadata: {
134
- generator: "cube4-random-moves-fallback",
135
- distribution: "random-moves",
136
- note: "Reduction solver and cubing.js both failed; using random-moves.",
137
- ...options.metadata
138
- }
139
- };
140
- }
54
+ const tables_js_1 = require("../cube3/solver/tables.js");
55
+ const pair_tables_js_1 = require("../cube3/solver/pair-tables.js");
141
56
  // ────────────────────────────────────────────────────────────
142
57
  let cachedTables;
143
58
  async function loadCube4Tables(baseUrl) {
@@ -177,98 +92,234 @@ exports.cube4ScrambleGenerator = {
177
92
  puzzle: "cube-4x4",
178
93
  quality: "random-state",
179
94
  generator: "cube4-reduction",
180
- description: "Random-state 4x4 scramble via the reduction solver (centers + pairing + 3x3 finish). Falls back to random-moves if the solver fails."
95
+ description: "Random-state 4x4 scramble via the reduction solver (centers + pairing + 3x3 finish)."
181
96
  },
182
97
  generate(options = {}) {
183
98
  return generateCube4Scramble(options);
184
99
  }
185
100
  };
101
+ function invertRotationSequence(rotSeq) {
102
+ return rotSeq.slice().reverse().map((r) => (r.endsWith("'") ? r.slice(0, -1) : r.endsWith("2") ? r : `${r}'`));
103
+ }
104
+ /** Shortest-first pick (ties keep `a`, i.e. the lowest job index). */
105
+ function shorterScramble(a, b) {
106
+ if (!a || b.moves.length < a.moves.length)
107
+ return b;
108
+ return a;
109
+ }
110
+ function seedForBestJob(options, job) {
111
+ if (job === 0)
112
+ return options;
113
+ const next = { ...options };
114
+ if (options.seed !== undefined) {
115
+ next.seed = `${options.seed}:best${job}`;
116
+ }
117
+ else {
118
+ delete next.seed;
119
+ }
120
+ return next;
121
+ }
122
+ function isRetryableCube4Error(error) {
123
+ const msg = error instanceof Error ? error.message : String(error);
124
+ const isPhase2Error = typeof error?.name === "string" &&
125
+ error.name.includes("Cube3Phase2");
126
+ return (error instanceof Error &&
127
+ (msg.includes("reduction failed") ||
128
+ msg.includes("failed to convert") ||
129
+ msg.includes("no valid phase 2") ||
130
+ msg.includes("timeout") ||
131
+ isPhase2Error ||
132
+ msg.includes("Phase 2 solution not found")));
133
+ }
186
134
  async function generateCube4Scramble(options = {}) {
135
+ const bestOf = Math.max(1, Math.floor(options.bestOf ?? 1) || 1);
187
136
  let tables;
137
+ let pairTables;
188
138
  try {
189
- tables = await loadCube4Tables();
139
+ const [loadedTables, cube3P1Tables, loadedPairTables] = await Promise.all([
140
+ loadCube4Tables(),
141
+ (0, tables_js_1.loadCube3Phase1Tables)(),
142
+ (0, pair_tables_js_1.loadCube3PairTables)()
143
+ ]);
144
+ tables = loadedTables;
145
+ tables.cube3Phase1 = cube3P1Tables;
146
+ tables.pairTables = loadedPairTables;
147
+ pairTables = loadedPairTables;
190
148
  }
191
149
  catch (error) {
192
- console.warn("[cube4-scrambler] Failed to load reduction tables; using fallback.", error);
193
- const cubingJsResult = await generateCube4WithCubingJs();
194
- if (cubingJsResult)
195
- return cubingJsResult;
196
- return generateCube4RandomMoves(options);
150
+ throw new Error("[cube4-scrambler] Failed to load reduction tables; run pnpm cube4:center1:tables etc. " +
151
+ `(${error instanceof Error ? error.message : String(error)})`);
197
152
  }
153
+ const totalRestarts = Math.max(1, Math.floor(options.restarts ?? options.bestOf ?? 1) || 1);
198
154
  let lastError;
199
- // Retry with different states if the solver fails to find a physical
200
- // reduction (some random states are outside the solver's effective
201
- // horizon with the current candidate/length caps).
202
- for (let attempt = 0; attempt < 5; attempt++) {
203
- let attemptOptions;
204
- if (attempt === 0) {
205
- attemptOptions = options;
155
+ try {
156
+ return await generateCube4Attempt(tables, pairTables, {
157
+ ...options,
158
+ restarts: totalRestarts
159
+ });
160
+ }
161
+ catch (error) {
162
+ if (!isRetryableCube4Error(error))
163
+ throw error;
164
+ lastError = error;
165
+ }
166
+ if (!options.deep) {
167
+ try {
168
+ return await generateCube4Attempt(tables, pairTables, {
169
+ ...options,
170
+ restarts: totalRestarts,
171
+ deep: true
172
+ });
206
173
  }
207
- else {
208
- attemptOptions = { ...options };
209
- if (options.seed !== undefined) {
210
- attemptOptions.seed = `${options.seed}:retry${attempt}`;
211
- }
212
- else {
213
- delete attemptOptions.seed;
214
- }
174
+ catch (error) {
175
+ if (isRetryableCube4Error(error))
176
+ lastError = error;
177
+ else
178
+ throw error;
215
179
  }
216
- const attemptState = (0, random_state_js_1.generateRandomCube4State)(attemptOptions);
180
+ }
181
+ throw lastError instanceof Error
182
+ ? lastError
183
+ : new Error(`[cube4-scrambler] Reduction solver failed for restarts=${totalRestarts}: ${String(lastError)}`);
184
+ }
185
+ /**
186
+ * Single-attempt 444 solve: samples ONE state (from `options.seed`) and
187
+ * solves it with the reduction solver. Throws on failure — no resampling.
188
+ * Fan-out primitive for hosts: run with seeds `seed`, `seed:bestN`, ...
189
+ * across workers and keep the shortest valid result.
190
+ */
191
+ async function generateCube4Attempt(tables, pairTables, options = {}) {
192
+ const attemptState = (0, random_state_js_1.generateRandomCube4State)(options);
193
+ const axes = options.threeAxes ? [[], ["z"], ["y'"]] : [[]];
194
+ let bestScrambleMoves = null;
195
+ let bestSolutionMoves = null;
196
+ let bestPhases;
197
+ let bestNodes;
198
+ let bestTimings;
199
+ let bestReductionLen;
200
+ let lastError;
201
+ const tAttemptStart = performance.now();
202
+ const finishOpts = options.finish ?? {};
203
+ for (const rot of axes) {
217
204
  try {
218
- const solvePromise = (0, solve_js_1.solveCube4State)(attemptState, tables, (cube3State) => (0, index_js_1.solveCube3State)(cube3State));
219
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("4x4 solve: timeout")), 20000));
220
- const { scramble: scrambleMoves } = await Promise.race([solvePromise, timeoutPromise]);
221
- if (!options.skipValidation) {
222
- const scrambledState = (0, moves_js_1.applyCube4Algorithm)((0, state_js_1.createSolvedCube4State)(), scrambleMoves);
223
- if (!(0, state_js_1.cube4StatesEqual)(scrambledState, attemptState)) {
224
- throw new Error("Generated 4x4 scramble does not reproduce the target random state.");
205
+ const sRot = rot.length === 0 ? attemptState : (0, solve_js_1.applyCubeRotation)(attemptState, rot);
206
+ // The reduction search is bounded by deterministic work caps (see
207
+ // solve.ts), so these timeouts are only wall-clock backstops for
208
+ // pathological cases.
209
+ const solvePromise = (0, solve_js_1.solveCube4State)(sRot, tables, async (cube3State) => {
210
+ // Single solver-side best-of-orientations finish call with
211
+ // extra phase-1 improvement: same lengths as the former
212
+ // hand-rolled UD/X/Z triple at a fraction of the cost.
213
+ const solveFinish = async (timeBudget, phase2Budget) => {
214
+ const solution = await (0, index_js_1.solveCube3State)(cube3State, {
215
+ orientations: finishOpts.orientations ?? 3,
216
+ phase1ExtraDepth: finishOpts.phase1ExtraDepth ?? 1,
217
+ phase1SolutionLimit: finishOpts.phase1SolutionLimit ?? 24,
218
+ targetLength: finishOpts.targetLength ?? 20,
219
+ timeoutMs: timeBudget,
220
+ // The 54MB pair tables are skipped for the 444 finish: pair
221
+ // pruning changes nothing measurable here (identical lengths
222
+ // on a 44-state corpus, zero extra finish retries), and
223
+ // loading them dominates cold-start I/O. Hosts that already
224
+ // hold pairTables can still pass them via generateCube4Attempt.
225
+ phase2: { pairTables, skipPairTables: pairTables === undefined, timeoutMs: phase2Budget }
226
+ });
227
+ return { moves: solution.moves };
228
+ };
229
+ // The first call seeds everything, so a flaky finish gets one
230
+ // budgeted retry before the whole finish is declared failed.
231
+ try {
232
+ return await solveFinish(finishOpts.timeoutMs ?? 150, finishOpts.phase2TimeoutMs ?? 50);
233
+ }
234
+ catch {
235
+ return await solveFinish(finishOpts.retryTimeoutMs ?? 300, finishOpts.retryPhase2TimeoutMs ?? 100);
225
236
  }
237
+ }, {
238
+ timeoutMs: 75000,
239
+ deep: options.deep === true,
240
+ deepPolish: options.deepPolish ?? options.threeAxes,
241
+ ...(options.finishTries !== undefined ? { finishTries: options.finishTries } : {}),
242
+ ...(options.centersBeam !== undefined ? { centersBeam: options.centersBeam } : {}),
243
+ ...(options.edgesPerCenter !== undefined ? { edgesPerCenter: options.edgesPerCenter } : {}),
244
+ ...(options.phase1ExtraDepths !== undefined ? { phase1ExtraDepths: options.phase1ExtraDepths } : {}),
245
+ ...(options.phase1Cap !== undefined ? { phase1Cap: options.phase1Cap } : {}),
246
+ ...(options.center2Wide !== undefined ? { center2Wide: options.center2Wide } : {}),
247
+ ...(options.restarts !== undefined ? { restarts: options.restarts } : {}),
248
+ ...(options.prepCap !== undefined ? { prepCap: options.prepCap } : {}),
249
+ ...(options.p1CandCap !== undefined ? { p1CandCap: options.p1CandCap } : {}),
250
+ ...(options.stagnationCut !== undefined ? { stagnationCut: options.stagnationCut } : {}),
251
+ maxReductionLen: bestReductionLen
252
+ });
253
+ let timeoutHandle;
254
+ const timeoutPromise = new Promise((_, reject) => {
255
+ timeoutHandle = setTimeout(() => reject(new Error("4x4 solve: timeout")), 90000);
256
+ });
257
+ let solveOutcome;
258
+ try {
259
+ solveOutcome = await Promise.race([solvePromise, timeoutPromise]);
260
+ }
261
+ finally {
262
+ // The search is work-capped and always settles first: clearing the
263
+ // backstop avoids leaking a live 90s timer per axis (which kept the
264
+ // event loop — and CLI/test processes — alive needlessly).
265
+ clearTimeout(timeoutHandle);
226
266
  }
227
- const facelets = (0, facelets_js_1.toCube4Facelets)(attemptState);
228
- const result = {
229
- eventId: "444",
230
- scramble: (0, moves_js_1.formatCube4Algorithm)(scrambleMoves),
231
- moves: scrambleMoves,
232
- quality: "random-state",
233
- generator: "cube4-reduction",
234
- metadata: {
235
- generator: "cube4-reduction",
236
- state: attemptState,
237
- facelets,
238
- faceletString: (0, facelets_js_1.formatCube4Facelets)(facelets),
239
- distribution: "random-state",
240
- ...options.metadata
267
+ const { scramble: scrambleMoves, moves: solutionMoves, phases, timings, nodes } = solveOutcome;
268
+ const invRot = rot.length === 0 ? [] : invertRotationSequence(rot);
269
+ const fullScramble = rot.length === 0 ? scrambleMoves : (0, solve_js_1.rotateAlgorithm)(scrambleMoves, invRot);
270
+ const fullSolution = rot.length === 0 ? solutionMoves : (0, solve_js_1.rotateAlgorithm)(solutionMoves, invRot);
271
+ if (!bestScrambleMoves || fullScramble.length < bestScrambleMoves.length) {
272
+ bestScrambleMoves = fullScramble;
273
+ bestSolutionMoves = fullSolution;
274
+ bestPhases = phases;
275
+ bestTimings = timings;
276
+ bestNodes = nodes;
277
+ if (phases) {
278
+ bestReductionLen = phases.length12 + phases.length3;
241
279
  }
242
- };
243
- if (options.seed !== undefined) {
244
- result.seed = options.seed;
245
280
  }
246
- return result;
247
281
  }
248
- catch (error) {
249
- lastError = error;
250
- // Retry on deterministic reduction / 3x3 failures (some random states are
251
- // outside the solver's horizon with the current caps).
252
- const msg = error instanceof Error ? error.message : String(error);
253
- const isPhase2Error = typeof error?.name === "string" &&
254
- error.name.includes("Cube3Phase2");
255
- if (error instanceof Error &&
256
- (msg.includes("reduction failed") ||
257
- msg.includes("failed to convert") ||
258
- msg.includes("no valid phase 2") ||
259
- msg.includes("timeout") ||
260
- isPhase2Error ||
261
- msg.includes("Phase 2 solution not found"))) {
262
- continue;
263
- }
264
- throw error;
282
+ catch (err) {
283
+ lastError = err;
284
+ }
285
+ }
286
+ if (!bestScrambleMoves || !bestSolutionMoves) {
287
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
288
+ }
289
+ const scrambleMoves = bestScrambleMoves;
290
+ const solutionMoves = bestSolutionMoves;
291
+ const scrambledState = (0, moves_js_1.applyCube4Algorithm)((0, state_js_1.createSolvedCube4State)(), scrambleMoves);
292
+ if (!options.skipValidation) {
293
+ const solvedCheck = (0, moves_js_1.applyCube4Algorithm)(scrambledState, solutionMoves);
294
+ if (!(0, state_js_1.isSolvedCube4State)(solvedCheck)) {
295
+ throw new Error("Generated 4x4 scramble does not solve back to solved state.");
296
+ }
297
+ }
298
+ const facelets = (0, facelets_js_1.toCube4Facelets)(scrambledState);
299
+ const result = {
300
+ eventId: "444",
301
+ scramble: (0, moves_js_1.formatCube4Algorithm)(scrambleMoves),
302
+ moves: scrambleMoves,
303
+ quality: "random-state",
304
+ generator: "cube4-reduction",
305
+ metadata: {
306
+ generator: "cube4-reduction",
307
+ state: scrambledState,
308
+ facelets,
309
+ faceletString: (0, facelets_js_1.formatCube4Facelets)(facelets),
310
+ distribution: "random-state",
311
+ solutionMoves: (0, moves_js_1.invertCube4Algorithm)(scrambleMoves),
312
+ // Winning-reduction breakdown (unsimplified lengths) + wall-clock
313
+ // split, for tuning and A/B comparisons.
314
+ phases: bestPhases,
315
+ phaseTimings: bestTimings,
316
+ nodes: bestNodes,
317
+ attemptMs: performance.now() - tAttemptStart,
318
+ ...options.metadata
265
319
  }
320
+ };
321
+ if (options.seed !== undefined) {
322
+ result.seed = options.seed;
266
323
  }
267
- // All solver attempts exhausted — try cubing.js first, then random-moves.
268
- console.warn("[cube4-scrambler] Reduction solver failed after 5 attempts; trying cubing.js fallback.", lastError);
269
- const cubingJsResult = await generateCube4WithCubingJs();
270
- if (cubingJsResult)
271
- return cubingJsResult;
272
- console.warn("[cube4-scrambler] cubing.js fallback also failed; using random-moves.");
273
- return generateCube4RandomMoves(options);
324
+ return result;
274
325
  }
@@ -44,8 +44,11 @@ function cube4StatesEqual(a, b) {
44
44
  typedArrayEqual(a.wp, b.wp) &&
45
45
  typedArrayEqual(a.centers, b.centers));
46
46
  }
47
+ // Read-only canonical solved state for comparisons (never mutated, so a
48
+ // single shared instance is safe and avoids allocating 4 arrays per call).
49
+ const SOLVED_STATE = createSolvedCube4State();
47
50
  function isSolvedCube4State(state) {
48
- return cube4StatesEqual(state, createSolvedCube4State());
51
+ return cube4StatesEqual(state, SOLVED_STATE);
49
52
  }
50
53
  // ---------------------------------------------------------------------------
51
54
  // Helpers
@@ -38,7 +38,7 @@ function solveWithTablesIndices(state) {
38
38
  const centersTable = tables_js_1.CENTERS_TABLE;
39
39
  const permTable = tables_js_1.PERMUTATION_TABLE;
40
40
  const orientTable = tables_js_1.ORIENTATION_TABLE;
41
- const policy = tables_js_1.POLICY_TABLE;
41
+ const policy = tables_js_1.POLICY_TABLE_EXPANDED;
42
42
  let c = (0, tables_js_1.getCentersIndex)(state.centers);
43
43
  let p = (0, tables_js_1.getPermutationCompactIndex)(state.edgesPerm);
44
44
  let o = (0, tables_js_1.getOrientationIndex)(state.edgesOrient);
@@ -47,10 +47,7 @@ function solveWithTablesIndices(state) {
47
47
  return [];
48
48
  const moves = [];
49
49
  while (stateIndex !== 0) {
50
- const bits = stateIndex * 3;
51
- const byteIndex = bits >> 3;
52
- const offset = bits & 7;
53
- const m = ((policy[byteIndex] | ((policy[byteIndex + 1] ?? 0) << 8)) >> offset) & 0x07;
50
+ const m = policy[stateIndex];
54
51
  moves.push(m);
55
52
  c = centersTable[c * tables_js_1.MOVE_COUNT + m];
56
53
  p = permTable[p * tables_js_1.MOVE_COUNT + m];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.POLICY_TABLE = exports.ORIENTATION_TABLE = exports.PERMUTATION_TABLE = exports.CENTERS_TABLE = exports.PYRAMINX_MOVES = exports.POLICY_PACKED_ENTRIES = exports.STATE_COUNT = exports.MOVE_COUNT = exports.ORIENTATION_COUNT = exports.CENTERS_COUNT = exports.PERM_360_TO_720 = exports.PERM_720_TO_360 = exports.PERMUTATION_COUNT = exports.PERMUTATION_FULL_COUNT = void 0;
3
+ exports.POLICY_TABLE_EXPANDED = exports.POLICY_TABLE = exports.ORIENTATION_TABLE = exports.PERMUTATION_TABLE = exports.CENTERS_TABLE = exports.PYRAMINX_MOVES = exports.POLICY_PACKED_ENTRIES = exports.STATE_COUNT = exports.MOVE_COUNT = exports.ORIENTATION_COUNT = exports.CENTERS_COUNT = exports.PERM_360_TO_720 = exports.PERM_720_TO_360 = exports.PERMUTATION_COUNT = exports.PERMUTATION_FULL_COUNT = void 0;
4
4
  exports.getPermutationIndex = getPermutationIndex;
5
5
  exports.getPermutationFromIndex = getPermutationFromIndex;
6
6
  exports.getOrientationIndex = getOrientationIndex;
@@ -289,6 +289,12 @@ exports.CENTERS_TABLE = null;
289
289
  exports.PERMUTATION_TABLE = null;
290
290
  exports.ORIENTATION_TABLE = null;
291
291
  exports.POLICY_TABLE = null;
292
+ /**
293
+ * Byte-expanded policy (one optimal move per state, no bit-unpacking in
294
+ * the hot walk). Built at load from the 3-bit packed table: +0.9 MB RAM,
295
+ * zero wire/format change.
296
+ */
297
+ exports.POLICY_TABLE_EXPANDED = null;
292
298
  async function ensureTablesLoaded() {
293
299
  if (tablesLoaded)
294
300
  return;
@@ -297,8 +303,18 @@ async function ensureTablesLoaded() {
297
303
  exports.PERMUTATION_TABLE = tables.permutationTable;
298
304
  exports.ORIENTATION_TABLE = tables.orientationTable;
299
305
  exports.POLICY_TABLE = tables.policyTable;
306
+ exports.POLICY_TABLE_EXPANDED = expandPolicy3Bit(tables.policyTable, exports.STATE_COUNT);
300
307
  tablesLoaded = true;
301
308
  }
309
+ function expandPolicy3Bit(packed, count) {
310
+ const expanded = new Uint8Array(count);
311
+ for (let index = 0; index < count; index += 1) {
312
+ const bits = index * 3;
313
+ const byteIndex = bits >> 3;
314
+ expanded[index] = (((packed[byteIndex] | ((packed[byteIndex + 1] ?? 0) << 8)) >> (bits & 7)) & 0x07);
315
+ }
316
+ return expanded;
317
+ }
302
318
  function arePyraminxTablesLoaded() {
303
319
  return tablesLoaded;
304
320
  }
@@ -328,4 +344,5 @@ function resetTablesForTests() {
328
344
  exports.PERMUTATION_TABLE = null;
329
345
  exports.ORIENTATION_TABLE = null;
330
346
  exports.POLICY_TABLE = null;
347
+ exports.POLICY_TABLE_EXPANDED = null;
331
348
  }
@@ -43,7 +43,7 @@ function solveWithTables(state) {
43
43
  return { moves, depth: moves.length };
44
44
  }
45
45
  function solveWithTablesIndices(state) {
46
- const policy = tables_js_1.POLICY_TABLE;
46
+ const policy = tables_js_1.POLICY_TABLE_EXPANDED;
47
47
  const permTable = tables_js_1.PERMUTATION_TABLE;
48
48
  const orientTable = tables_js_1.ORIENTATION_TABLE;
49
49
  const centersTable = tables_js_1.CENTERS_TABLE;
@@ -60,7 +60,7 @@ function solveWithTablesIndices(state) {
60
60
  }
61
61
  const moves = [];
62
62
  while (rank !== tables_js_1.SOLVED_RANK) {
63
- const m = (0, tables_js_1.getPolicyMoveFromPacked)(policy, rank);
63
+ const m = policy[rank];
64
64
  moves.push(m);
65
65
  r7 = permTable[r7 * tables_js_1.MOVE_COUNT + m];
66
66
  rO = orientTable[rO * tables_js_1.MOVE_COUNT + m];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SOLVED_RANK = exports.ORIENT_UNRANK = exports.CENTER_UNRANK = exports.PERM7_UNRANK = exports.R7_SETS = exports.ORIENT_CLASS = exports.EVEN_CENTER_FULL = exports.EVEN_CENTER_RANK = exports.POLICY_TABLE = exports.CENTERS_TABLE = exports.ORIENTATION_TABLE = exports.PERMUTATION_TABLE = exports.SKEWB_MOVES = exports.POLICY_PACKED_ENTRIES = exports.STATE_COUNT = exports.MOVE_COUNT = exports.PER_SET_SIZE = exports.ORIENT_CLASS_COUNT = exports.EVEN_CENTER_COUNT = exports.CENTER_COUNT = exports.ORIENTATION_COUNT = exports.PERMUTATION_COUNT = void 0;
3
+ exports.SOLVED_RANK = exports.ORIENT_UNRANK = exports.CENTER_UNRANK = exports.PERM7_UNRANK = exports.R7_SETS = exports.ORIENT_CLASS = exports.EVEN_CENTER_FULL = exports.EVEN_CENTER_RANK = exports.POLICY_TABLE_EXPANDED = exports.POLICY_TABLE = exports.CENTERS_TABLE = exports.ORIENTATION_TABLE = exports.PERMUTATION_TABLE = exports.SKEWB_MOVES = exports.POLICY_PACKED_ENTRIES = exports.STATE_COUNT = exports.MOVE_COUNT = exports.PER_SET_SIZE = exports.ORIENT_CLASS_COUNT = exports.EVEN_CENTER_COUNT = exports.CENTER_COUNT = exports.ORIENTATION_COUNT = exports.PERMUTATION_COUNT = void 0;
4
4
  exports.getPermutationIndex = getPermutationIndex;
5
5
  exports.getPermutationFromIndex = getPermutationFromIndex;
6
6
  exports.getOrientationIndex = getOrientationIndex;
@@ -175,6 +175,12 @@ exports.PERMUTATION_TABLE = null; // r7 * MOVE_COUNT + m
175
175
  exports.ORIENTATION_TABLE = null; // rO * MOVE_COUNT + m
176
176
  exports.CENTERS_TABLE = null; // rC * MOVE_COUNT + m
177
177
  exports.POLICY_TABLE = null; // optimal move per rank, 3-bit packed
178
+ /**
179
+ * Byte-expanded policy (one optimal move per rank, no bit-unpacking in
180
+ * the hot walk). Built at load from the 3-bit packed table: +3.1 MB RAM,
181
+ * zero wire/format change.
182
+ */
183
+ exports.POLICY_TABLE_EXPANDED = null;
178
184
  exports.EVEN_CENTER_RANK = null; // rC -> compact 0..359 (65535 = odd)
179
185
  exports.EVEN_CENTER_FULL = null; // compact 0..359 -> rC (derived at load)
180
186
  exports.ORIENT_CLASS = null; // rO -> class 0..8
@@ -224,9 +230,19 @@ function assignTables(tables) {
224
230
  for (let i = 0; i < 7; i++)
225
231
  exports.ORIENT_UNRANK[rO * 7 + i] = orient[i + 1];
226
232
  }
233
+ exports.POLICY_TABLE_EXPANDED = expandPolicy3Bit(tables.policyTable, exports.STATE_COUNT);
227
234
  exports.SOLVED_RANK = rankFromComponents(0, 0, 0);
228
235
  tablesLoaded = true;
229
236
  }
237
+ function expandPolicy3Bit(packed, count) {
238
+ const expanded = new Uint8Array(count);
239
+ for (let index = 0; index < count; index += 1) {
240
+ const bits = index * 3;
241
+ const byteIndex = bits >> 3;
242
+ expanded[index] = (((packed[byteIndex] | ((packed[byteIndex + 1] ?? 0) << 8)) >> (bits & 7)) & 0x07);
243
+ }
244
+ return expanded;
245
+ }
230
246
  function resetTablesForTests() {
231
247
  tablesLoaded = false;
232
248
  exports.PERMUTATION_TABLE = null;
@@ -240,6 +256,7 @@ function resetTablesForTests() {
240
256
  exports.PERM7_UNRANK = null;
241
257
  exports.CENTER_UNRANK = null;
242
258
  exports.ORIENT_UNRANK = null;
259
+ exports.POLICY_TABLE_EXPANDED = null;
243
260
  }
244
261
  // ---------------------------------------------------------------------------
245
262
  // Compact rank helpers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubingcenter/scrambler",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Extensible scramble generation toolkit for CubingCenter projects.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -57,12 +57,19 @@
57
57
  "tsx": "^4.20.0",
58
58
  "typescript": "^5.9.0"
59
59
  },
60
- "dependencies": {
61
- "cubing": "^0.63.4"
62
- },
63
60
  "scripts": {
64
61
  "build": "node scripts/clean.cjs && tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node scripts/write-cjs-package.cjs && node scripts/copy-assets.cjs",
65
- "test": "node --test --import tsx test/*.test.ts",
62
+ "test": "node --test --import tsx",
63
+ "test:all": "node --test --import tsx test/*.test.ts",
64
+ "test:skewb": "node --test --import tsx test/skewb*.test.ts",
65
+ "test:pyraminx": "node --test --import tsx test/pyraminx*.test.ts",
66
+ "test:sq1": "node --test --import tsx test/sq1*.test.ts",
67
+ "test:fto": "node --test --import tsx test/fto*.test.ts",
68
+ "test:cube2": "node --test --import tsx test/cube2*.test.ts",
69
+ "test:cube3": "node --test --import tsx test/cube3*.test.ts",
70
+ "test:cube4": "node --test --import tsx test/cube4*.test.ts test/center*.test.ts test/edge3.test.ts test/pairing-coordinate.test.ts test/phase*.test.ts",
71
+ "test:big": "node --test --import tsx test/big-cubes*.test.ts",
72
+ "test:watch": "node --test --watch --import tsx",
66
73
  "lint": "tsc -p tsconfig.json --noEmit",
67
74
  "package:check": "pnpm build && node scripts/check-package.cjs",
68
75
  "verify": "pnpm lint && pnpm test && pnpm build && pnpm cube3:tables:check && pnpm cube2:tables:check && pnpm pyraminx:tables:check && pnpm skewb:tables:check && pnpm sq1:tables:check && pnpm package:check",
@@ -83,6 +90,8 @@
83
90
  "cube4:center2:tables:check": "node --import tsx scripts/check-center2-solver-tables.ts",
84
91
  "cube4:center3:tables": "node --import tsx scripts/generate-center3-solver-tables.ts",
85
92
  "cube4:center3:tables:check": "node --import tsx scripts/check-center3-solver-tables.ts",
93
+ "cube4:hc:tables": "node --import tsx scripts/generate-hc-pdb-table.ts",
94
+ "cube4:hc:tables:check": "node --import tsx scripts/check-hc-pdb-table.ts",
86
95
  "cube4:edge3:tables": "node --import tsx scripts/generate-edge3-solver-tables.ts",
87
96
  "cube4:edge3:tables:check": "node --import tsx scripts/check-edge3-solver-tables.ts",
88
97
  "tables:web:sync": "node --import tsx scripts/sync-web-tables.ts",