@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,12 +1,40 @@
1
1
  "use strict";
2
2
  /**
3
3
  * The 4x4 scramble generator: sample a uniform random state, solve it with
4
- * the reduction solver (phases 1-4), and invert the solution. The scramble
5
- * always reproduces the sampled target state and facelets exactly.
4
+ * the reduction solver (phases 1-4), and invert the solution. The emitted
5
+ * scramble must reproduce the sampled target state EXACTLY — enforced by
6
+ * `assertCube4ScrambleReproducesTarget` on every generated result (unless
7
+ * `skipValidation` is set, which is only for the independent verification
8
+ * harness in `scripts/verify-scrambles.ts`).
6
9
  *
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.
10
+ * On a target mismatch the error is retryable, so the existing retry loop
11
+ * re-runs the solve over the SAME sampled target (deep attempt) and throws
12
+ * once attempts are exhausted. Re-sampling a different target to "make it
13
+ * fit" and degrading to random moves are forbidden: failing loudly (WCA 4g)
14
+ * is strictly preferable to emitting a scramble that lies about its seed.
15
+ *
16
+ * ── Determinism contract: what defines the output ──────────────────────
17
+ * `generateCube4Scramble` / `generateCube4Attempt` are pure functions of
18
+ * (seed, complete options): the emitted `moves` do not depend on wall-clock
19
+ * time, CPU load, warm-up, batch order, or process identity. This holds
20
+ * because every budget that can change the search outcome is a
21
+ * deterministic work counter (nodes visited), never a wall deadline:
22
+ * - reduction phases: node budgets inside `reduction/solve.ts`;
23
+ * - the 3x3 finish: node caps below (`finish.phase2MaxNodes`,
24
+ * `finish.improveMaxNodes`, per-candidate cap), which replace the old
25
+ * 150/50 ms wall budgets that made the same seed yield different move
26
+ * counts under CPU contention.
27
+ * Options are PART of the key: `restarts`/`bestOf`, `threeAxes`, `deep`,
28
+ * the beam knobs and `finish.*` legitimately change the result for the same
29
+ * seed — the output is reproducible within each configuration (documented
30
+ * behavior, not nondeterminism).
31
+ *
32
+ * Residual wall-clock backstop (hang guard only; never observed to bind —
33
+ * max measured solve 36.5 s): the 90 s
34
+ * `Promise.race` below. It cannot preempt the synchronous search — it
35
+ * can only fire at an event-loop tick — so if one ever fired, that attempt
36
+ * fails loudly (as a retryable error) instead of hanging. Making that bound
37
+ * exact deadline support would require cooperative search checkpoints.
10
38
  */
11
39
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
40
  if (k2 === undefined) k2 = k;
@@ -42,107 +70,59 @@ var __importStar = (this && this.__importStar) || (function () {
42
70
  };
43
71
  })();
44
72
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.cube4ScrambleGenerator = void 0;
73
+ exports.Cube4WallTimeoutError = exports.Cube4TargetMismatchError = exports.cube4ScrambleGenerator = void 0;
74
+ exports.assertCube4ScrambleReproducesTarget = assertCube4ScrambleReproducesTarget;
46
75
  exports.generateCube4Scramble = generateCube4Scramble;
76
+ exports.generateCube4Attempt = generateCube4Attempt;
47
77
  const moves_js_1 = require("./moves.js");
48
78
  const random_state_js_1 = require("./random-state.js");
49
79
  const state_js_1 = require("./state.js");
50
80
  const facelets_js_1 = require("./facelets.js");
51
81
  const solve_js_1 = require("./reduction/solve.js");
52
82
  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)
83
+ const tables_js_1 = require("../cube3/solver/tables.js");
84
+ const finish3_js_1 = require("./reduction/finish3.js");
85
+ const rotation_fix_js_1 = require("./reduction/rotation-fix.js");
56
86
  // ────────────────────────────────────────────────────────────
57
87
  /**
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.
88
+ * Deterministic work caps for the 3x3 finish — nodes visited, never
89
+ * wall-clock. Calibrated against the former wall budgets (phase-2 50 ms,
90
+ * improvement 150 ms at a measured ~1.75k nodes/ms warm) so typical
91
+ * latency and finish length are unchanged, while the stopping point
92
+ * becomes independent of CPU load: same (seed, options) ⇒ same moves.
93
+ * A failing first finish call retries once with every cap doubled
94
+ * (equivalent of the former 150→300 / 50→100 ms retry).
61
95
  */
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
- };
96
+ const FINISH_PHASE2_MAX_NODES = 90_000;
97
+ const FINISH_IMPROVE_MAX_NODES = 260_000;
98
+ const FINISH_RETRY_SCALE = 2;
99
+ // Continuous finish defaults (the production finish). Measured on cc:0..15:
100
+ // 6 views × 200k nodes = 44.88 moves @ 1257 ms vs legacy 46.06 @ 1288 ms
101
+ // (50k: 45.50 @ 1102; 100k: 45.31; 300k: 44.88 @ 1371 — no gain past 200k).
102
+ const FINISH3_MAX_NODES = 200_000;
103
+ const FINISH3_HARD_MAX_NODES = 20_000_000;
104
+ // Screening budget for the two-stage finish (every candidate reduction gets
105
+ // this; only the best `finishScreenTop` get FINISH3_MAX_NODES).
106
+ const FINISH3_SCREEN_NODES = 30_000;
107
+ const FINISH3_VIEWS = 6;
108
+ const FINISH3_MAX_PHASE2_LENGTH = 12;
109
+ let cube4TablesPromise;
110
+ /**
111
+ * Loads the 4x4 reduction tables with a promise-deduplicating cache: the
112
+ * cache stores the in-flight PROMISE (not a value), so concurrent first
113
+ * calls share one load instead of racing; a rejected load is purged so the
114
+ * next call can retry instead of caching the failure forever.
115
+ */
116
+ function loadCube4Tables(baseUrl) {
117
+ if (cube4TablesPromise !== undefined)
118
+ return cube4TablesPromise;
119
+ cube4TablesPromise = loadCube4TablesOnce(baseUrl).catch((error) => {
120
+ cube4TablesPromise = undefined;
121
+ throw error;
122
+ });
123
+ return cube4TablesPromise;
140
124
  }
141
- // ────────────────────────────────────────────────────────────
142
- let cachedTables;
143
- async function loadCube4Tables(baseUrl) {
144
- if (cachedTables)
145
- return cachedTables;
125
+ async function loadCube4TablesOnce(baseUrl) {
146
126
  const isBrowser = typeof window !== "undefined" ||
147
127
  typeof document !== "undefined" ||
148
128
  (typeof self !== "undefined" && typeof self.importScripts === "function") ||
@@ -155,20 +135,15 @@ async function loadCube4Tables(baseUrl) {
155
135
  "./reduction/browser-loader.js")));
156
136
  const cached = loader.getCachedCube4Tables();
157
137
  if (cached) {
158
- cachedTables = cached;
159
138
  return cached;
160
139
  }
161
- const tables = await loader.loadCube4ReductionTablesBrowser(baseUrl);
162
- cachedTables = tables;
163
- return tables;
140
+ return await loader.loadCube4ReductionTablesBrowser(baseUrl);
164
141
  }
165
142
  const loader = await Promise.resolve().then(() => __importStar(require(
166
143
  /* webpackIgnore: true */
167
144
  /* @vite-ignore */
168
145
  "./reduction/node-loader.js")));
169
- const tables = await loader.loadCube4ReductionTables();
170
- cachedTables = tables;
171
- return tables;
146
+ return await loader.loadCube4ReductionTables();
172
147
  }
173
148
  exports.cube4ScrambleGenerator = {
174
149
  info: {
@@ -177,98 +152,365 @@ exports.cube4ScrambleGenerator = {
177
152
  puzzle: "cube-4x4",
178
153
  quality: "random-state",
179
154
  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."
155
+ description: "Random-state 4x4 scramble via the reduction solver (centers + pairing + 3x3 finish)."
181
156
  },
182
157
  generate(options = {}) {
183
158
  return generateCube4Scramble(options);
184
159
  }
185
160
  };
161
+ /**
162
+ * Thrown by the target gate when the emitted scramble does not reproduce
163
+ * the independently sampled state. Retryable: the retry loop re-solves the
164
+ * SAME sampled target (deep attempt) — never re-samples.
165
+ */
166
+ class Cube4TargetMismatchError extends Error {
167
+ code = "cube4-target-mismatch";
168
+ constructor(message) {
169
+ super(message);
170
+ this.name = "Cube4TargetMismatchError";
171
+ }
172
+ }
173
+ exports.Cube4TargetMismatchError = Cube4TargetMismatchError;
174
+ /** Thrown by the 90 s hang guard (wall-clock, fires only at a tick). */
175
+ class Cube4WallTimeoutError extends Error {
176
+ code = "cube4-wall-timeout";
177
+ constructor(message) {
178
+ super(message);
179
+ this.name = "Cube4WallTimeoutError";
180
+ }
181
+ }
182
+ exports.Cube4WallTimeoutError = Cube4WallTimeoutError;
183
+ /**
184
+ * Message patterns for errors thrown by `reduction/solve.ts`, which cannot
185
+ * be classified by type/code without editing that file (reported: it should
186
+ * grow error codes). Every other retryable path is typed below.
187
+ */
188
+ const SOLVER_RETRYABLE_MESSAGE_PATTERNS = [
189
+ "reduction failed", // no candidates / node budget exhausted in a reduction phase
190
+ "no valid phase 2" // empty phase-2 candidate set
191
+ ];
192
+ /**
193
+ * Retry classification. Typed first (error class / `code`), message
194
+ * patterns last and only for the reduction solver's own errors. Semantics:
195
+ * a retry re-runs the solve over the SAME sampled target with deeper work
196
+ * caps; it never changes the seed's target state.
197
+ */
198
+ function isFinish3Cap(error) {
199
+ return error instanceof Error &&
200
+ error.code === "cube4-finish3-cap" &&
201
+ (error instanceof finish3_js_1.Finish3SearchError || error.name === "Finish3SearchError");
202
+ }
203
+ function isRetryableCube4Error(error) {
204
+ if (!(error instanceof Error))
205
+ return false;
206
+ const code = error.code;
207
+ if (code === "cube4-target-mismatch" || code === "cube4-wall-timeout")
208
+ return true;
209
+ if (isFinish3Cap(error))
210
+ return true;
211
+ if (error instanceof solve_js_1.Cube4Phase2CapError ||
212
+ error instanceof solve_js_1.Cube4Phase3CapError ||
213
+ error instanceof solve_js_1.Cube4FinishCapError)
214
+ return true;
215
+ // Budget/depth failures of the 3x3 finish phase 2: a deeper attempt may
216
+ // succeed (historical behavior: any Cube3Phase2* error was retryable).
217
+ if (error instanceof index_js_1.Cube3Phase2SearchError || error.name === "Cube3Phase2SearchError") {
218
+ return true;
219
+ }
220
+ // Phase-1 failures are deterministic depth failures (phase 1 runs
221
+ // uncapped in the 444 finish): retrying the same target cannot help.
222
+ if (error instanceof index_js_1.Cube3Phase1SearchError || error.name === "Cube3Phase1SearchError") {
223
+ return false;
224
+ }
225
+ return SOLVER_RETRYABLE_MESSAGE_PATTERNS.some((pattern) => error.message.includes(pattern));
226
+ }
227
+ /**
228
+ * The real random-state contract for 444 (non-tautological): replaying the
229
+ * emitted scramble on the solved cube must reproduce the exact state that
230
+ * was sampled for the seed — the reference the sampler produced, never a
231
+ * state derived from the scramble itself. Throws when it does not.
232
+ *
233
+ * Used by the production validation path in `solveCube4Attempt` and
234
+ * exported so tests can prove the gate rejects corrupted scrambles.
235
+ */
236
+ function assertCube4ScrambleReproducesTarget(scrambleMoves, targetState, seed) {
237
+ const replayed = (0, moves_js_1.applyCube4Algorithm)((0, state_js_1.createSolvedCube4State)(), scrambleMoves);
238
+ if (!(0, state_js_1.cube4StatesEqual)(replayed, targetState)) {
239
+ throw new Cube4TargetMismatchError("[cube4-scrambler] scramble does not reproduce the sampled target state" +
240
+ (seed !== undefined ? ` (seed=${seed})` : "") +
241
+ ": applying the emitted scramble to the solved cube must yield the state " +
242
+ "sampled for this seed. Refusing to emit it (no re-sampling, no random-move fallback).");
243
+ }
244
+ }
186
245
  async function generateCube4Scramble(options = {}) {
187
246
  let tables;
247
+ // Optional pair tables are unnecessary for the 444 finish. In browsers,
248
+ // probing for absent tables on every generation adds a network dependency
249
+ // before the search even starts (outside its timeout).
250
+ const pairTables = undefined;
188
251
  try {
189
- tables = await loadCube4Tables();
252
+ const continuousFinish = options.finish?.mode !== "legacy";
253
+ const [loadedTables, cube3P1Tables, cube3P2Tables] = await Promise.all([
254
+ loadCube4Tables(),
255
+ (0, tables_js_1.loadCube3Phase1Tables)(),
256
+ continuousFinish ? (0, tables_js_1.loadCube3Phase2Tables)() : Promise.resolve(undefined)
257
+ ]);
258
+ tables = loadedTables;
259
+ tables.cube3Phase1 = cube3P1Tables;
260
+ if (cube3P2Tables !== undefined)
261
+ tables.cube3Phase2 = cube3P2Tables;
190
262
  }
191
263
  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);
264
+ throw new Error("[cube4-scrambler] Failed to load reduction tables; run pnpm cube4:center1:tables etc. " +
265
+ `(${error instanceof Error ? error.message : String(error)})`);
197
266
  }
267
+ const totalRestarts = Math.max(1, Math.floor(options.restarts ?? options.bestOf ?? 1) || 1);
268
+ const targetState = (0, random_state_js_1.generateRandomCube4State)(options);
198
269
  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;
270
+ try {
271
+ return await solveCube4Attempt(tables, pairTables, {
272
+ ...options,
273
+ restarts: totalRestarts
274
+ }, targetState);
275
+ }
276
+ catch (error) {
277
+ if (!isRetryableCube4Error(error))
278
+ throw error;
279
+ lastError = error;
280
+ }
281
+ if (!options.deep) {
282
+ try {
283
+ return await solveCube4Attempt(tables, pairTables, {
284
+ ...options,
285
+ restarts: totalRestarts,
286
+ deep: true
287
+ }, targetState);
206
288
  }
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
- }
289
+ catch (error) {
290
+ if (isRetryableCube4Error(error))
291
+ lastError = error;
292
+ else
293
+ throw error;
215
294
  }
216
- const attemptState = (0, random_state_js_1.generateRandomCube4State)(attemptOptions);
295
+ }
296
+ throw lastError instanceof Error
297
+ ? lastError
298
+ : new Error(`[cube4-scrambler] Reduction solver failed for restarts=${totalRestarts}: ${String(lastError)}`);
299
+ }
300
+ /**
301
+ * Single-attempt 444 solve: samples ONE state (from `options.seed`) and
302
+ * solves it with the reduction solver. Throws on failure — including a
303
+ * target mismatch ("scramble does not reproduce the sampled target state")
304
+ * — no resampling; hosts may retry with the same seed, which re-uses the
305
+ * same target.
306
+ * Fan-out primitive for hosts: run with seeds `seed`, `seed:bestN`, ...
307
+ * across workers and keep the shortest valid result.
308
+ */
309
+ async function generateCube4Attempt(tables, pairTables, options = {}) {
310
+ return solveCube4Attempt(tables, pairTables, options, (0, random_state_js_1.generateRandomCube4State)(options));
311
+ }
312
+ async function solveCube4Attempt(tables, pairTables, options, attemptState) {
313
+ const axes = options.threeAxes ? [[], ["z"], ["y'"]] : [[]];
314
+ let bestScrambleMoves = null;
315
+ let bestSolutionMoves = null;
316
+ let bestPhases;
317
+ let bestNodes;
318
+ let bestTimings;
319
+ let bestReductionLen;
320
+ let lastError;
321
+ const tAttemptStart = performance.now();
322
+ const finishOpts = options.finish ?? {};
323
+ for (const rot of axes) {
217
324
  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.");
325
+ const sRot = rot.length === 0 ? attemptState : (0, solve_js_1.applyCubeRotation)(attemptState, rot);
326
+ // The reduction search is bounded by deterministic work caps (see
327
+ // solve.ts), so these timeouts are only wall-clock backstops for
328
+ // pathological cases.
329
+ const solvePromise = (0, solve_js_1.solveCube4State)(sRot, tables, async (cube3State, effort) => {
330
+ // Legacy batch two-phase finish. Budgets are deterministic node
331
+ // caps (see FINISH_* constants); `capScale` doubles every cap on
332
+ // the one flaky-finish retry.
333
+ const solveLegacyFinish = async (capScale) => {
334
+ const solution = await (0, index_js_1.solveCube3State)(cube3State, {
335
+ orientations: finishOpts.orientations ?? 3,
336
+ phase1ExtraDepth: finishOpts.phase1ExtraDepth ?? 1,
337
+ phase1SolutionLimit: finishOpts.phase1SolutionLimit ?? 24,
338
+ // 18 (not 20): the improve loop only runs when the first
339
+ // answer exceeds the target, so 20 skipped it for 19-20 move
340
+ // answers. Measured: -0.13 moves at ~+3% time.
341
+ targetLength: finishOpts.targetLength ?? 18,
342
+ improveMaxNodes: Math.round((finishOpts.improveMaxNodes ?? FINISH_IMPROVE_MAX_NODES) * capScale),
343
+ ...(finishOpts.improvePhase2MaxNodes !== undefined
344
+ ? {
345
+ improvePhase2MaxNodes: Math.round(finishOpts.improvePhase2MaxNodes * capScale)
346
+ }
347
+ : {}),
348
+ // The 54MB pair tables are skipped for the 444 finish: pair
349
+ // pruning changes nothing measurable here (identical lengths
350
+ // on a 44-state corpus, zero extra finish retries), and
351
+ // loading them dominates cold-start I/O. Hosts that already
352
+ // hold pairTables can still pass them via generateCube4Attempt.
353
+ phase2: {
354
+ pairTables,
355
+ skipPairTables: pairTables === undefined,
356
+ maxNodes: Math.round((finishOpts.phase2MaxNodes ?? FINISH_PHASE2_MAX_NODES) * capScale)
357
+ }
358
+ });
359
+ return { moves: solution.moves };
360
+ };
361
+ if (finishOpts.mode !== "legacy") {
362
+ // Callers of generateCube4Attempt pass their own table bag,
363
+ // which may predate the continuous finish: load what is missing.
364
+ tables.cube3Phase1 ??= await (0, tables_js_1.loadCube3Phase1Tables)();
365
+ tables.cube3Phase2 ??= await (0, tables_js_1.loadCube3Phase2Tables)();
366
+ const views = finishOpts.continuousViews ?? FINISH3_VIEWS;
367
+ const runContinuous = (capScale) => {
368
+ if (tables.cube3Phase1 === undefined || tables.cube3Phase2 === undefined) {
369
+ throw new Error("[cube4-scrambler] continuous finish requires cube3 phase-1 and phase-2 tables");
370
+ }
371
+ return (0, finish3_js_1.solveFinish3)(cube3State, { phase1: tables.cube3Phase1, phase2: tables.cube3Phase2 }, {
372
+ targetLength: finishOpts.targetLength ?? 18,
373
+ maxNodes: Math.round((effort === "screen"
374
+ ? finishOpts.continuousScreenNodes ?? FINISH3_SCREEN_NODES
375
+ : finishOpts.continuousMaxNodes ?? FINISH3_MAX_NODES) * capScale),
376
+ hardMaxNodes: Math.round((finishOpts.continuousHardMaxNodes ?? FINISH3_HARD_MAX_NODES) * capScale),
377
+ maxPhase2Length: finishOpts.continuousMaxPhase2Length ?? FINISH3_MAX_PHASE2_LENGTH,
378
+ views
379
+ });
380
+ };
381
+ try {
382
+ return runContinuous(1);
383
+ }
384
+ catch (error) {
385
+ if (!isFinish3Cap(error))
386
+ throw error;
387
+ }
388
+ try {
389
+ return runContinuous(FINISH_RETRY_SCALE);
390
+ }
391
+ catch (error) {
392
+ if (!isFinish3Cap(error))
393
+ throw error;
394
+ return solveLegacyFinish(1);
395
+ }
396
+ }
397
+ // The first legacy call seeds everything, so a flaky finish gets
398
+ // one retry with doubled caps before the whole finish is declared
399
+ // failed (formerly 150→300 / 50→100 wall budgets).
400
+ try {
401
+ return await solveLegacyFinish(1);
225
402
  }
403
+ catch {
404
+ return solveLegacyFinish(FINISH_RETRY_SCALE);
405
+ }
406
+ }, {
407
+ deep: options.deep === true,
408
+ deepPolish: options.deepPolish ?? options.threeAxes,
409
+ ...(options.finishTries !== undefined ? { finishTries: options.finishTries } : {}),
410
+ ...(options.centersBeam !== undefined ? { centersBeam: options.centersBeam } : {}),
411
+ ...(options.edgesPerCenter !== undefined ? { edgesPerCenter: options.edgesPerCenter } : {}),
412
+ ...(options.phase1ExtraDepths !== undefined ? { phase1ExtraDepths: options.phase1ExtraDepths } : {}),
413
+ ...(options.phase1Cap !== undefined ? { phase1Cap: options.phase1Cap } : {}),
414
+ ...(options.center2Wide !== undefined ? { center2Wide: options.center2Wide } : {}),
415
+ ...(options.restarts !== undefined ? { restarts: options.restarts } : {}),
416
+ finishScreenTop: finishOpts.mode === "legacy" ? 0 : finishOpts.screenTop ?? 0,
417
+ ...(options.maxFixMoves !== undefined ? { maxFixMoves: options.maxFixMoves } : {}),
418
+ ...(options.prepCap !== undefined ? { prepCap: options.prepCap } : {}),
419
+ ...(options.p1CandCap !== undefined ? { p1CandCap: options.p1CandCap } : {}),
420
+ ...(options.stagnationCut !== undefined ? { stagnationCut: options.stagnationCut } : {}),
421
+ maxReductionLen: bestReductionLen
422
+ });
423
+ let timeoutHandle;
424
+ // Honest scope: this race cannot interrupt the synchronous CPU-bound
425
+ // search — a setTimeout callback only runs at an event-loop tick, so
426
+ // the guard can fire only if the async solve chain yields for 90 s
427
+ // total. It exists solely so a pathological hang fails loudly
428
+ // (retryable Cube4WallTimeoutError) instead of wedging the host; it
429
+ // has never bound (max measured solve 36.5 s).
430
+ const timeoutPromise = new Promise((_, reject) => {
431
+ timeoutHandle = setTimeout(() => reject(new Cube4WallTimeoutError("4x4 solve: timeout")), 90000);
432
+ });
433
+ let solveOutcome;
434
+ try {
435
+ solveOutcome = await Promise.race([solvePromise, timeoutPromise]);
436
+ }
437
+ finally {
438
+ // The search is work-capped and always settles first: clearing the
439
+ // backstop avoids leaking a live 90s timer per axis (which kept the
440
+ // event loop — and CLI/test processes — alive needlessly).
441
+ clearTimeout(timeoutHandle);
226
442
  }
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
443
+ const { scramble: scrambleMoves, moves: solutionMoves, phases, timings, nodes } = solveOutcome;
444
+ const fullSolution = rot.length === 0
445
+ ? solutionMoves
446
+ : (0, rotation_fix_js_1.placeRotationFix)([], rot, solutionMoves);
447
+ const fullScramble = rot.length === 0
448
+ ? scrambleMoves
449
+ : (0, moves_js_1.invertCube4Algorithm)(fullSolution);
450
+ if (!bestScrambleMoves || fullScramble.length < bestScrambleMoves.length) {
451
+ bestScrambleMoves = fullScramble;
452
+ bestSolutionMoves = fullSolution;
453
+ bestPhases = phases;
454
+ bestTimings = timings;
455
+ bestNodes = nodes;
456
+ if (phases) {
457
+ bestReductionLen = phases.length12 + phases.length3;
241
458
  }
242
- };
243
- if (options.seed !== undefined) {
244
- result.seed = options.seed;
245
459
  }
246
- return result;
247
460
  }
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;
461
+ catch (err) {
462
+ lastError = err;
463
+ }
464
+ }
465
+ if (!bestScrambleMoves || !bestSolutionMoves) {
466
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
467
+ }
468
+ const scrambleMoves = bestScrambleMoves;
469
+ const scrambledState = (0, moves_js_1.applyCube4Algorithm)((0, state_js_1.createSolvedCube4State)(), scrambleMoves);
470
+ if (!options.skipValidation) {
471
+ // Non-tautological gate: compare against `attemptState` — the state
472
+ // sampled for this seed (the same reference the sampler produced),
473
+ // NOT a state derived from the scramble. The former check
474
+ // (`apply(scramble, invert(scramble)) === solved`) passed by
475
+ // construction and hid frame bugs. A mismatch throws an error the
476
+ // retry loop classifies as retryable: `generateCube4Scramble` then
477
+ // re-runs the solve over this SAME `attemptState` (deep) and finally
478
+ // throws — it never re-samples another target.
479
+ assertCube4ScrambleReproducesTarget(scrambleMoves, attemptState, options.seed);
480
+ }
481
+ const facelets = (0, facelets_js_1.toCube4Facelets)(scrambledState);
482
+ const result = {
483
+ eventId: "444",
484
+ scramble: (0, moves_js_1.formatCube4Algorithm)(scrambleMoves),
485
+ moves: scrambleMoves,
486
+ quality: "random-state",
487
+ generator: "cube4-reduction",
488
+ metadata: {
489
+ generator: "cube4-reduction",
490
+ // The sampled target — the independent reference the seed stands for
491
+ // (consistent with the 222/333/pymx/skwb/fto scramblers). When
492
+ // validation ran, this is exactly `apply(solved, moves)`; with
493
+ // `skipValidation` it stays the sampled truth so external checks can
494
+ // compare the emitted scramble against it instead of a derived state.
495
+ state: attemptState,
496
+ facelets,
497
+ faceletString: (0, facelets_js_1.formatCube4Facelets)(facelets),
498
+ distribution: "random-state",
499
+ // The scramble's inverse: by construction it solves the state the
500
+ // scramble actually produces, and (validation on) that state is
501
+ // `attemptState`.
502
+ solutionMoves: (0, moves_js_1.invertCube4Algorithm)(scrambleMoves),
503
+ // Winning-reduction breakdown (unsimplified lengths) + wall-clock
504
+ // split, for tuning and A/B comparisons.
505
+ phases: bestPhases,
506
+ phaseTimings: bestTimings,
507
+ nodes: bestNodes,
508
+ attemptMs: performance.now() - tAttemptStart,
509
+ ...options.metadata
265
510
  }
511
+ };
512
+ if (options.seed !== undefined) {
513
+ result.seed = options.seed;
266
514
  }
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);
515
+ return result;
274
516
  }