@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
@@ -5,17 +5,143 @@
5
5
  * The scramble for a sampled state is the inverted solution.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.rotStats = exports.finishStats = exports.stagnationStats = exports.f2Stats = exports.P2_CANCEL = exports.Cube4FinishCapError = exports.Cube4Phase3CapError = exports.Cube4Phase2CapError = void 0;
9
+ exports.rotateAlgorithm = rotateAlgorithm;
10
+ exports.cubeRotationMoves = cubeRotationMoves;
11
+ exports.invertRotationSequence = invertRotationSequence;
12
+ exports.applyCubeRotation = applyCubeRotation;
13
+ exports.resetF2Stats = resetF2Stats;
14
+ exports.resetStagnationStats = resetStagnationStats;
8
15
  exports.solveCube4State = solveCube4State;
9
16
  const moves_js_1 = require("../moves.js");
10
17
  const center1_js_1 = require("./center1.js");
11
18
  const center2_js_1 = require("./center2.js");
12
19
  const center3_js_1 = require("./center3.js");
13
20
  const pairing_coordinate_js_1 = require("./pairing-coordinate.js");
21
+ const hc_pdb_js_1 = require("./hc-pdb.js");
14
22
  const edge3_js_1 = require("./edge3.js");
15
23
  const phase3_search_js_1 = require("./phase3-search.js");
24
+ const rotation_fix_js_1 = require("./rotation-fix.js");
25
+ const pair_transversal_js_1 = require("./pair-transversal.js");
16
26
  const phase4_js_1 = require("./phase4.js");
17
- const index_js_1 = require("../../cube3/solver/index.js");
27
+ const coordinates_js_1 = require("../../cube3/solver/coordinates.js");
18
28
  const state_js_1 = require("../state.js");
29
+ function mulberry32(seed) {
30
+ let a = seed >>> 0;
31
+ return () => {
32
+ a |= 0;
33
+ a = (a + 0x6d2b79f5) | 0;
34
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
35
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
36
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
37
+ };
38
+ }
39
+ function hashCube4State(state) {
40
+ let h = 0x811c9dc5;
41
+ for (const arr of [state.cp, state.co, state.wp, state.centers]) {
42
+ for (let i = 0; i < arr.length; i++) {
43
+ h ^= arr[i] & 0xff;
44
+ h = Math.imul(h, 0x01000193);
45
+ }
46
+ }
47
+ return h >>> 0;
48
+ }
49
+ /**
50
+ * Fisher-Yates order, or identity when rng is null (restarts=1 keeps the
51
+ * legacy deterministic traversal bit-identical).
52
+ */
53
+ function restartOrder(n, rng) {
54
+ const order = Array.from({ length: n }, (_, i) => i);
55
+ if (rng) {
56
+ for (let i = n - 1; i > 0; i--) {
57
+ const j = Math.floor(rng() * (i + 1));
58
+ const t = order[i];
59
+ order[i] = order[j];
60
+ order[j] = t;
61
+ }
62
+ }
63
+ return order;
64
+ }
65
+ /**
66
+ * Fisher-Yates within equal-key runs of a key-sorted array, so the
67
+ * caller's sorted early-break (`value > length123`) stays valid.
68
+ */
69
+ function shuffleTies(arr, key, rng) {
70
+ if (!rng)
71
+ return;
72
+ let run = 0;
73
+ for (let i = 1; i <= arr.length; i++) {
74
+ if (i < arr.length && key(arr[i]) === key(arr[run]))
75
+ continue;
76
+ for (let k = i - 1; k > run; k--) {
77
+ const j = run + Math.floor(rng() * (k - run + 1));
78
+ const t = arr[k];
79
+ arr[k] = arr[j];
80
+ arr[j] = t;
81
+ }
82
+ run = i;
83
+ }
84
+ }
85
+ function getCube3Phase1Heuristic(cube3State, tables) {
86
+ const co = (0, coordinates_js_1.encodeCornerOrientation)(cube3State);
87
+ const eo = (0, coordinates_js_1.encodeEdgeOrientation)(cube3State);
88
+ const ud = (0, coordinates_js_1.encodeUdSlice)(cube3State);
89
+ const combined = (co << 11) + eo;
90
+ const coeo = (tables.pruningTables.cornerOrientationEdgeOrientation.values[combined >> 1] >>
91
+ ((combined & 1) << 2)) &
92
+ 0x0f;
93
+ const udd = (tables.pruningTables.udSlice.values[ud >> 1] >> ((ud & 1) << 2)) & 0x0f;
94
+ return Math.max(coeo, udd);
95
+ }
96
+ function getTriAxialCube3Heuristic(standardReduced, tables) {
97
+ const s0 = (0, phase4_js_1.toCube3State)(standardReduced);
98
+ if (!s0)
99
+ return 99;
100
+ let minH = getCube3Phase1Heuristic(s0, tables);
101
+ if (minH <= 3)
102
+ return minH;
103
+ const sx = (0, phase4_js_1.toCube3State)(applyCubeRotation(standardReduced, ["x"]));
104
+ if (sx) {
105
+ const hx = getCube3Phase1Heuristic(sx, tables);
106
+ if (hx < minH)
107
+ minH = hx;
108
+ if (minH <= 3)
109
+ return minH;
110
+ }
111
+ const sz = (0, phase4_js_1.toCube3State)(applyCubeRotation(standardReduced, ["z"]));
112
+ if (sz) {
113
+ const hz = getCube3Phase1Heuristic(sz, tables);
114
+ if (hz < minH)
115
+ minH = hz;
116
+ }
117
+ return minH;
118
+ }
119
+ class Cube4SolveError extends Error {
120
+ }
121
+ class Cube4Phase2CapError extends Cube4SolveError {
122
+ code = "cube4-phase2-cap";
123
+ constructor(message) {
124
+ super(message);
125
+ this.name = "Cube4Phase2CapError";
126
+ }
127
+ }
128
+ exports.Cube4Phase2CapError = Cube4Phase2CapError;
129
+ class Cube4Phase3CapError extends Cube4SolveError {
130
+ code = "cube4-phase3-cap";
131
+ constructor(message) {
132
+ super(message);
133
+ this.name = "Cube4Phase3CapError";
134
+ }
135
+ }
136
+ exports.Cube4Phase3CapError = Cube4Phase3CapError;
137
+ class Cube4FinishCapError extends Cube4SolveError {
138
+ code = "cube4-finish-cap";
139
+ constructor(message) {
140
+ super(message);
141
+ this.name = "Cube4FinishCapError";
142
+ }
143
+ }
144
+ exports.Cube4FinishCapError = Cube4FinishCapError;
19
145
  function cornerPermutationParity(cp) {
20
146
  let parity = 0;
21
147
  const seen = new Array(8).fill(false);
@@ -33,13 +159,48 @@ function cornerPermutationParity(cp) {
33
159
  }
34
160
  return parity;
35
161
  }
162
+ /**
163
+ * Computes the spatial determinant (+1 for valid 3D rotations, -1 for reflections)
164
+ * of the 3 axes defined by the center colors on faces U, R, F.
165
+ */
166
+ function getOrientationDet(uColor, rColor, fColor) {
167
+ const vec = (c) => {
168
+ switch (c) {
169
+ case 0: return [0, 0, 1];
170
+ case 3: return [0, 0, -1];
171
+ case 1: return [1, 0, 0];
172
+ case 4: return [-1, 0, 0];
173
+ case 2: return [0, 1, 0];
174
+ case 5: return [0, -1, 0];
175
+ default: return [0, 0, 0];
176
+ }
177
+ };
178
+ const [ux, uy, uz] = vec(uColor);
179
+ const [rx, ry, rz] = vec(rColor);
180
+ const [fx, fy, fz] = vec(fColor);
181
+ const cx = ry * fz - rz * fy;
182
+ const cy = rz * fx - rx * fz;
183
+ const cz = rx * fy - ry * fx;
184
+ return cx * ux + cy * uy + cz * uz;
185
+ }
186
+ function axisDet(uAxis, rAxis, fAxis) {
187
+ if ((uAxis === 0 && rAxis === 1 && fAxis === 2) ||
188
+ (uAxis === 1 && rAxis === 2 && fAxis === 0) ||
189
+ (uAxis === 2 && rAxis === 0 && fAxis === 1)) {
190
+ return 1;
191
+ }
192
+ return -1;
193
+ }
36
194
  const ROT_MOVES = {
37
195
  "x": ["Rw", "Lw'"],
38
196
  "y": ["Uw", "Dw'"],
39
197
  "z": ["Fw", "Bw'"],
40
198
  "x'": ["Rw'", "Lw"],
41
199
  "y'": ["Uw'", "Dw"],
42
- "z'": ["Fw'", "Bw"]
200
+ "z'": ["Fw'", "Bw"],
201
+ "x2": ["Rw2", "Lw2"],
202
+ "y2": ["Uw2", "Dw2"],
203
+ "z2": ["Fw2", "Bw2"]
43
204
  };
44
205
  const VALID_24_ORIENTATIONS = (() => {
45
206
  const map = new Map();
@@ -60,18 +221,24 @@ const VALID_24_ORIENTATIONS = (() => {
60
221
  }
61
222
  return map;
62
223
  })();
224
+ /** Centers of the solved state — used to verify a full (not just partial) standard orientation. */
225
+ const SOLVED_CENTERS = (0, state_js_1.createSolvedCube4State)().centers;
63
226
  const ROT_MAP = {
64
227
  "x": { "U": "B", "R": "R", "F": "U", "D": "F", "L": "L", "B": "D" },
65
228
  "x'": { "U": "F", "R": "R", "F": "D", "D": "B", "L": "L", "B": "U" },
66
229
  "y": { "U": "U", "R": "F", "F": "L", "D": "D", "L": "B", "B": "R" },
67
230
  "y'": { "U": "U", "R": "B", "F": "R", "D": "D", "L": "F", "B": "L" },
68
231
  "z": { "U": "R", "R": "D", "F": "F", "D": "L", "L": "U", "B": "B" },
69
- "z'": { "U": "L", "R": "U", "F": "F", "D": "R", "L": "D", "B": "B" }
232
+ "z'": { "U": "L", "R": "U", "F": "F", "D": "R", "L": "D", "B": "B" },
233
+ "x2": { "U": "D", "R": "R", "F": "B", "D": "U", "L": "L", "B": "F" },
234
+ "y2": { "U": "U", "R": "L", "F": "B", "D": "D", "L": "R", "B": "F" },
235
+ "z2": { "U": "D", "R": "L", "F": "F", "D": "U", "L": "R", "B": "B" }
70
236
  };
71
237
  const INVERT_ROT = {
72
238
  "x": "x'", "x'": "x",
73
239
  "y": "y'", "y'": "y",
74
- "z": "z'", "z'": "z"
240
+ "z": "z'", "z'": "z",
241
+ "x2": "x2", "y2": "y2", "z2": "z2"
75
242
  };
76
243
  function rotateMove(move, rot) {
77
244
  const map = ROT_MAP[rot];
@@ -80,11 +247,20 @@ function rotateMove(move, rot) {
80
247
  return (newFace + move.slice(1));
81
248
  }
82
249
  function rotateAlgorithm(alg, rotSeq) {
83
- let res = [...alg];
84
- for (const r of rotSeq) {
85
- res = res.map((m) => rotateMove(m, r));
86
- }
87
- return res;
250
+ // Single pass over the algorithm (one allocation): applying the rotation
251
+ // sequence per move instead of re-mapping the whole array per rotation.
252
+ if (rotSeq.length === 0)
253
+ return [...alg];
254
+ return alg.map((move) => {
255
+ let current = move;
256
+ for (const rot of rotSeq)
257
+ current = rotateMove(current, rot);
258
+ return current;
259
+ });
260
+ }
261
+ /** Physical wide-move sequence implementing a whole-cube rotation. */
262
+ function cubeRotationMoves(rotSeq) {
263
+ return rotSeq.flatMap((rotation) => ROT_MOVES[rotation]);
88
264
  }
89
265
  function invertRotationSequence(rotSeq) {
90
266
  return [...rotSeq].reverse().map((r) => INVERT_ROT[r]);
@@ -119,196 +295,1051 @@ function isCancelled(previous, current) {
119
295
  }
120
296
  return false;
121
297
  }
122
- function solveCenter1Diverse(centers, urf, tables, cap = 80) {
123
- const startMask = (0, center1_js_1.center1Mask)(centers, urf);
124
- const solutions = [];
125
- const path = [];
126
- const minH = (0, center1_js_1.getCenter1Distance)(startMask, tables);
127
- for (let maxD = minH; maxD <= minH + 1; maxD++) {
128
- const dfs = (mask, depth, lm) => {
129
- if (solutions.length >= cap)
130
- return true;
131
- const h = (0, center1_js_1.getCenter1Distance)(mask, tables);
132
- if (h === 0) {
133
- solutions.push({
134
- moves: path.slice(0, depth).map((m) => center1_js_1.CENTER1_MOVES[m]),
135
- length: depth
136
- });
137
- return false;
138
- }
139
- if (depth + h > maxD)
140
- return false;
141
- for (let m = 0; m < center1_js_1.CENTER1_MOVES.length; m++) {
142
- if (lm >= 0 && Math.floor(lm / 3) === Math.floor(m / 3))
143
- continue;
144
- const next = (0, center1_js_1.applyCenter1Move)(mask, tables.moveMaps, m);
145
- path[depth] = m;
146
- if (dfs(next, depth + 1, m))
147
- return true;
148
- }
149
- return false;
150
- };
151
- if (dfs(startMask, 0, -1))
298
+ // Precomputed phase-2 cancellation matrix over CENTER2_SEARCH_MOVES: the
299
+ // search evaluates isCancelled per child per node (~0.3M nodes × 23 moves
300
+ // per attempt with regex + Map lookups); the table makes it one boolean
301
+ // read with a bit-identical traversal.
302
+ exports.P2_CANCEL = center2_js_1.CENTER2_SEARCH_MOVES.map((previous) => center2_js_1.CENTER2_SEARCH_MOVES.map((current) => isCancelled(previous, current)));
303
+ // Same over the wide 28-move set (used when `center2Wide` is set).
304
+ const P2_CANCEL_WIDE = center2_js_1.CENTER2_SEARCH_MOVES_WIDE.map((previous) => center2_js_1.CENTER2_SEARCH_MOVES_WIDE.map((current) => isCancelled(previous, current)));
305
+ /** Flat (row-major) cancel tables for the hot DFS loop; same semantics. */
306
+ function flattenCancel(table) {
307
+ const n = table.length;
308
+ const flat = new Uint8Array(n * n);
309
+ for (let i = 0; i < n; i++) {
310
+ const row = table[i];
311
+ for (let j = 0; j < n; j++)
312
+ flat[i * n + j] = row[j] ? 1 : 0;
313
+ }
314
+ return flat;
315
+ }
316
+ const P2_CANCEL_FLAT = flattenCancel(exports.P2_CANCEL);
317
+ const P2_CANCEL_WIDE_FLAT = flattenCancel(P2_CANCEL_WIDE);
318
+ /**
319
+ * Wing transversal mask: bit k set iff pair k occupies a second-half slot.
320
+ * `hasPairingTransversal(wp)` is equivalent to `pairingMaskOf(wp) === 0xfff`
321
+ * (12 slots, 12 pair ids in 0..11: all distinct ⟺ all 12 bits set). The mask
322
+ * form is O(1) at the millions of phase-2 goal leaves.
323
+ */
324
+ function pairingMaskOf(wp) {
325
+ let mask = 0;
326
+ for (let i = 0; i < 12; i++) {
327
+ mask |= 1 << (wp[pairing_coordinate_js_1.PAIRING_SECOND_HALVES[i]] >> 1);
328
+ }
329
+ return mask;
330
+ }
331
+ /** rl side of the phase-2 goal test (same flags as `isCenter2Goal`). */
332
+ const P2_GOAL_RL = (() => {
333
+ const flags = new Uint8Array(center2_js_1.CENTER2_RL_STATES);
334
+ for (const rl of center2_js_1.CENTER2_GOAL_RL)
335
+ flags[rl] = 1;
336
+ return flags;
337
+ })();
338
+ /** Chiral phase-2 tables, generated once per loaded center2 table set. */
339
+ const chiralTablesCache = new WeakMap();
340
+ function getCenter2ChiralTables(center2) {
341
+ let chiral = chiralTablesCache.get(center2);
342
+ if (!chiral) {
343
+ chiral = (0, center2_js_1.generateCenter2ChiralTables)(center2.rlMove);
344
+ chiralTablesCache.set(center2, chiral);
345
+ }
346
+ return chiral;
347
+ }
348
+ const SKIP_AXIS_2 = new Int32Array(center2_js_1.CENTER2_SEARCH_MOVES.length);
349
+ for (let i = 0; i < center2_js_1.CENTER2_SEARCH_MOVES.length; i++) {
350
+ const m1 = center2_js_1.CENTER2_SEARCH_MOVES[i];
351
+ const face1 = m1.replace(/2|'|w/g, "");
352
+ const isWide1 = m1.includes("w");
353
+ SKIP_AXIS_2[i] = center2_js_1.CENTER2_SEARCH_MOVES.length;
354
+ for (let j = i + 1; j < center2_js_1.CENTER2_SEARCH_MOVES.length; j++) {
355
+ const m2 = center2_js_1.CENTER2_SEARCH_MOVES[j];
356
+ const face2 = m2.replace(/2|'|w/g, "");
357
+ const isWide2 = m2.includes("w");
358
+ if (face1 !== face2 || isWide1 !== isWide2) {
359
+ SKIP_AXIS_2[i] = j;
152
360
  break;
361
+ }
153
362
  }
154
- return solutions;
155
363
  }
156
- const phase3Solver = new phase3_search_js_1.FastPhase3Solver();
364
+ /** Temporary instrumentation for F2 goal statistics (CUBE4_F2_STATS=1). */
365
+ exports.f2Stats = { goals: 0, transversalPass: 0, axisRejects: 0, minTransversalL2: 99, minChiralL2: 99 };
366
+ function resetF2Stats() {
367
+ exports.f2Stats.goals = 0;
368
+ exports.f2Stats.transversalPass = 0;
369
+ exports.f2Stats.axisRejects = 0;
370
+ exports.f2Stats.minTransversalL2 = 99;
371
+ exports.f2Stats.minChiralL2 = 99;
372
+ }
373
+ /** Trigger rate for the stagnationCut early-stop (checks vs actual cuts). */
374
+ exports.stagnationStats = { checks: 0, cuts: 0 };
375
+ function resetStagnationStats() {
376
+ exports.stagnationStats.checks = 0;
377
+ exports.stagnationStats.cuts = 0;
378
+ }
157
379
  /**
158
- * Solve a 4x4 state with the reduction solver + the cube3 two-phase finish.
159
- * `solveCube3` is injected (the cube3 solver loads its tables lazily).
380
+ * Temporary instrumentation for the finish-skip bound (CUBE4_FINISH_STATS=1):
381
+ * `gap = totalEst - finalLen` per finished reduction. A skip
382
+ * `totalEst - SLACK >= minTotalMoves` is only sound while SLACK covers the
383
+ * observed gap maximum (junction cancellation + finish-vs-h3 slack).
160
384
  */
161
- async function solveCube4State(state, tables, solveCube3) {
385
+ exports.finishStats = {
386
+ gapMin: 99,
387
+ gapMax: -99,
388
+ gapSum: 0,
389
+ gapN: 0,
390
+ finishes: 0,
391
+ skipped: 0
392
+ };
393
+ /**
394
+ * Temporary instrumentation for the rotMoves cost (CUBE4_ROT_STATS=1):
395
+ * per built reduction records the prep frame rotation, the derived
396
+ * normalization rotation and the emitted fix length.
397
+ */
398
+ exports.rotStats = [];
399
+ async function solveCube4State(state, tables, solveCube3, options) {
400
+ // NOTE: no wall-clock deadline inside the search — all loops are bounded
401
+ // by deterministic work caps (node counts, candidate counts, length caps),
402
+ // so the same state always takes the same path.
162
403
  const sym = (0, edge3_js_1.getSymTables)();
163
- const cornerParity = cornerPermutationParity(state.cp);
164
- const p1Sols = solveCenter1Diverse(state.centers, 0, tables.center1, 80);
165
- p1Sols.sort((a, b) => a.moves.length - b.moves.length);
166
- const minP1 = p1Sols[0]?.moves.length ?? 0;
167
- const candidates = [];
168
- const moveLog = new Array(25);
169
- function searchPhase2(c, r, maxl, lm, depth, bridgeState, p1Moves) {
170
- if ((0, center2_js_1.isCenter2Goal)(c, r)) {
171
- if (maxl === 0) {
172
- const moves2 = moveLog.slice(0, depth).map((m) => center2_js_1.CENTER2_MOVES[m]);
173
- const s2 = (0, moves_js_1.applyCube4Algorithm)(bridgeState, moves2);
174
- const pairing = (0, pairing_coordinate_js_1.encodePairingState)(s2.wp);
175
- if (pairing) {
176
- const eXc = (pairing.parity ^ cornerParity);
177
- const ct3 = (0, center3_js_1.encodeCenter3)(s2, eXc).coordinate;
178
- const cdist = tables.center3.pruning[ct3];
179
- if (cdist !== 0xff && cdist !== undefined) {
180
- const symIdx = (0, edge3_js_1.getMvSymFrom)(pairing.edge, pairing.edge, 20, sym.sym2Raw, sym.sym2Mask, sym.raw2Sym, sym.mvrot, sym.mvroto);
181
- const edist = (tables.edge3.distances[(symIdx >> 3) >> 1] >>
182
- (((symIdx >> 3) & 1) * 4)) &
183
- 0x0f;
184
- candidates.push({
185
- moves12: [...p1Moves, ...moves2],
186
- length12: p1Moves.length + moves2.length,
187
- ct3,
188
- cdist,
189
- edge: pairing.edge,
190
- edist,
191
- value: p1Moves.length + moves2.length + Math.max(cdist, edist),
192
- state: s2
404
+ // All mutable phase-3 state (TT, epoch-node count, call IDs and search
405
+ // stacks) belongs to this invocation. The same instance spans its
406
+ // deterministic restarts, retaining useful goal-free TT entries without
407
+ // allowing concurrent solves to contaminate one another.
408
+ const phase3Solver = new phase3_search_js_1.FastPhase3Solver();
409
+ phase3Solver.resetEpochNodes();
410
+ const collectF2Stats = typeof process !== "undefined" && process.env?.CUBE4_F2_STATS === "1";
411
+ const totalRestarts = Math.max(1, Math.floor(options?.restarts ?? 1));
412
+ // Cross-restart work accumulators (reported in nodes/timings).
413
+ let accP2 = 0;
414
+ let accF3calls = 0;
415
+ let accF1Ms = 0;
416
+ let accF2Ms = 0;
417
+ let accF3Ms = 0;
418
+ let accFinishMs = 0;
419
+ let accPolishMs = 0;
420
+ const runOnce = async (rng) => {
421
+ const tR0 = performance.now();
422
+ const p1Sols = [];
423
+ let minOverallH = 99;
424
+ const urfDistances = [];
425
+ for (const urf of [0, 1, 2]) {
426
+ const mask = (0, center1_js_1.center1Mask)(state.centers, urf);
427
+ const h = (0, center1_js_1.getCenter1Distance)(mask, tables.center1);
428
+ urfDistances.push(h);
429
+ if (h < minOverallH)
430
+ minOverallH = h;
431
+ }
432
+ const path = [];
433
+ const p1Extra = options?.phase1ExtraDepths ?? 3;
434
+ const p1Cap = options?.phase1Cap ?? 400;
435
+ // Per-restart DFS traversal order (null = identity fast path when rng is null).
436
+ const p1Order = rng === null ? null : Int32Array.from(restartOrder(center1_js_1.CENTER1_MOVES.length, rng));
437
+ for (let maxD = minOverallH; maxD <= minOverallH + p1Extra; maxD++) {
438
+ for (const urf of [0, 1, 2]) {
439
+ const minH = urfDistances[urf];
440
+ if (maxD < minH)
441
+ continue;
442
+ const mask = (0, center1_js_1.center1Mask)(state.centers, urf);
443
+ let axisCount = 0;
444
+ const dfs = (m, depth, lm) => {
445
+ if (axisCount >= p1Cap)
446
+ return true;
447
+ const h = (0, center1_js_1.getCenter1Distance)(m, tables.center1);
448
+ if (h === 0) {
449
+ let rotAlign = [];
450
+ if (m === center1_js_1.CENTER1_GOAL_MASKS.rl) {
451
+ rotAlign = [];
452
+ }
453
+ else if (m === center1_js_1.CENTER1_GOAL_MASKS.ud) {
454
+ rotAlign = ["z"];
455
+ }
456
+ else if (m === center1_js_1.CENTER1_GOAL_MASKS.fb) {
457
+ rotAlign = ["y'"];
458
+ }
459
+ else {
460
+ return false;
461
+ }
462
+ axisCount++;
463
+ p1Sols.push({
464
+ moves: path.slice(0, depth).map((mv) => center1_js_1.CENTER1_MOVES[mv]),
465
+ length: depth,
466
+ rotAlign
193
467
  });
194
- if (candidates.length >= 60)
468
+ return false;
469
+ }
470
+ if (depth + h > maxD)
471
+ return false;
472
+ for (let mi = 0; mi < center1_js_1.CENTER1_MOVES.length; mi++) {
473
+ const mv = p1Order === null ? mi : p1Order[mi];
474
+ if (lm >= 0 && Math.floor(lm / 3) === Math.floor(mv / 3))
475
+ continue;
476
+ const next = (0, center1_js_1.applyCenter1Move)(m, tables.center1.moveMaps, mv);
477
+ path[depth] = mv;
478
+ if (dfs(next, depth + 1, mv))
195
479
  return true;
196
480
  }
481
+ return false;
482
+ };
483
+ dfs(mask, 0, -1);
484
+ }
485
+ if (p1Sols.length >= p1Cap)
486
+ break;
487
+ }
488
+ p1Sols.sort((a, b) => a.moves.length - b.moves.length);
489
+ const minP1 = p1Sols[0]?.moves.length ?? 0;
490
+ const tF1End = performance.now();
491
+ const candidates = [];
492
+ const moveLog = new Int32Array(48);
493
+ // Incremental wing tracking stack for phase-2 goal transversal checks:
494
+ // avoids full 6-7 move replaying from scratch across hundreds of thousands of candidate goals.
495
+ // `wpMaskStack[d]` = pairingMaskOf(wpStack[d]) maintained in the same lazy
496
+ // fill, so the per-leaf transversal test is a single compare.
497
+ const wpStack = Array.from({ length: 32 }, () => new Uint8Array(24));
498
+ const wpMaskStack = new Int32Array(32);
499
+ let validWpDepth = 0;
500
+ const wingMaps = (0, center2_js_1.getWingMoveMaps)();
501
+ // Transversal pruning (see pair-transversal.ts): wing positions per DFS
502
+ // depth (`posStack[d][wing]`), checked against the two-pair PDB over 6
503
+ // disjoint pair-of-pairs in the last TRANSVERSAL_DEPTH levels (the PDB's
504
+ // maximum). Prunes only branches with no transversal goal, so the
505
+ // accepted candidates are unchanged.
506
+ const posStack = Array.from({ length: 33 }, () => new Uint8Array(24));
507
+ const wingPosMove = (0, pair_transversal_js_1.getWingPositionMoves)();
508
+ const TRANSVERSAL_DEPTH = 3;
509
+ let validPosDepth = 0;
510
+ /**
511
+ * Anti-hang work cap for the phase-2 search (successful attempts use
512
+ * ~0.1-0.3M nodes; this only aborts pathological explorations).
513
+ * Exceeding it degrades gracefully: the call returns false and the
514
+ * remaining (prep, length2) combinations still run.
515
+ */
516
+ const deep = options?.deep === true;
517
+ // Wide phase-2 set (28 moves) + its pruning table. Both sets are
518
+ // prefixes of CENTER2_MOVES, so search indices stay valid move-table
519
+ // indices in either mode.
520
+ const c2Wide = options?.center2Wide === true;
521
+ const C2_SEARCH = c2Wide ? center2_js_1.CENTER2_SEARCH_MOVES_WIDE : center2_js_1.CENTER2_SEARCH_MOVES;
522
+ const cancelFlat = c2Wide ? P2_CANCEL_WIDE_FLAT : P2_CANCEL_FLAT;
523
+ const c2Stride = C2_SEARCH.length;
524
+ const transversalPdb = (0, pair_transversal_js_1.getTransversalPairPdb)(C2_SEARCH.length);
525
+ const canSkipAxis = rng === null && !c2Wide;
526
+ // Per-restart child order (null = identity fast path). Entries stay
527
+ // original move-table indices, so all table lookups are unaffected.
528
+ const p2Order = rng === null ? null : Int32Array.from(restartOrder(C2_SEARCH.length, rng));
529
+ // Default set: chiral tables (goal = ct == 0 && axisDet == 1 exactly, so
530
+ // wrong-chirality goal paths are pruned instead of enumerated). The wide
531
+ // set keeps the relative tables + the axisDet filter at the goal.
532
+ const chiral = c2Wide ? null : getCenter2ChiralTables(tables.center2);
533
+ const c2ctMove = chiral ? chiral.ctMove : tables.center2.ctMove;
534
+ const c2rlMove = tables.center2.rlMove;
535
+ const c2GoalCt = chiral ? chiral.goalCt : 0;
536
+ let c2pruning = chiral ? chiral.pruning : tables.center2.pruning;
537
+ if (c2Wide) {
538
+ if (!tables.center2.pruningWide) {
539
+ throw new Error("4x4 solve: center2Wide requested but pruningWide tables are not loaded — regenerate center2 tables.");
540
+ }
541
+ c2pruning = tables.center2.pruningWide;
542
+ }
543
+ const P2_MAX_NODES = deep ? 24_000_000 : 8_000_000;
544
+ // Hoisted out of the goal path (was read per goal leaf).
545
+ const maxP1Cand = options?.p1CandCap !== undefined ? options.p1CandCap : (deep ? 24 : 16);
546
+ // Smaller pool + shorter extension (was 180/100/14/12 on the fast path):
547
+ // the deep length2 levels are where F2 tails burn. Validated on a 44-state
548
+ // corpus: -16% nodes, identical average length (one +1 in 44).
549
+ const CAND_CAP = deep ? 300 : 120;
550
+ const CAND_MIN = deep ? 180 : 35;
551
+ const P2_MAX_ABS = deep ? 18 : 14;
552
+ const P2_MAX_MIN = deep ? 16 : 10;
553
+ const L123_CAP = options?.maxReductionLen
554
+ ? Math.min(options.maxReductionLen, deep ? 48 : 45)
555
+ : (deep ? 48 : 45);
556
+ const EXT_MX = deep ? 20 : 16;
557
+ let p2nodes = 0;
558
+ let f3calls = 0;
559
+ // Bound the whole sweep, not just each individual IDA* call. A hard
560
+ // candidate pool can otherwise spend thousands of capped calls before
561
+ // reaching the deep retry (corpus-73: 114M nodes in the fast attempt).
562
+ const phase3StartNodes = phase3Solver.epochNodes;
563
+ const phase3NodeBudget = deep ? 16_000_000 : 4_000_000;
564
+ function searchPhase2(c, r, maxl, lm, depth, bridgeState, prep) {
565
+ p2nodes++;
566
+ if (p2nodes > P2_MAX_NODES)
567
+ return false;
568
+ if (c === c2GoalCt && P2_GOAL_RL[r] === 1) {
569
+ if (maxl === 0) {
570
+ while (validWpDepth < depth) {
571
+ const m = moveLog[validWpDepth];
572
+ const base = m * 24;
573
+ const src = wpStack[validWpDepth];
574
+ const dst = wpStack[validWpDepth + 1];
575
+ for (let i = 0; i < 24; i++)
576
+ dst[i] = src[wingMaps[base + i]];
577
+ wpMaskStack[validWpDepth + 1] = pairingMaskOf(dst);
578
+ validWpDepth++;
579
+ }
580
+ if (collectF2Stats)
581
+ exports.f2Stats.goals++;
582
+ // Bit-identical to `hasPairingTransversal(goalWp)`: all 12 pair bits
583
+ // distinct ⟺ the 12-bit mask is full (0xfff).
584
+ if (wpMaskStack[depth] !== 0xfff)
585
+ return false;
586
+ if (collectF2Stats)
587
+ exports.f2Stats.transversalPass++;
588
+ const moves2 = [];
589
+ for (let i = 0; i < depth; i++)
590
+ moves2.push(center2_js_1.CENTER2_MOVES[moveLog[i]]);
591
+ const s2 = (0, moves_js_1.applyCube4Algorithm)(bridgeState, moves2);
592
+ const uAxis = s2.centers[0] % 3;
593
+ const rAxis = s2.centers[4] % 3;
594
+ const fAxis = s2.centers[8] % 3;
595
+ if (collectF2Stats) {
596
+ if (depth < exports.f2Stats.minTransversalL2)
597
+ exports.f2Stats.minTransversalL2 = depth;
598
+ }
599
+ if (axisDet(uAxis, rAxis, fAxis) !== 1) {
600
+ if (collectF2Stats)
601
+ exports.f2Stats.axisRejects++;
602
+ return false;
603
+ }
604
+ if (collectF2Stats) {
605
+ if (depth < exports.f2Stats.minChiralL2)
606
+ exports.f2Stats.minChiralL2 = depth;
607
+ }
608
+ const pairing = (0, pairing_coordinate_js_1.encodePairingState)(s2.wp);
609
+ if (pairing) {
610
+ const s2CornerParity = cornerPermutationParity(s2.cp);
611
+ const eXc = (pairing.parity ^ s2CornerParity);
612
+ const ct3 = (0, center3_js_1.encodeCenter3)(s2, eXc).coordinate;
613
+ const cdist = tables.center3.pruning[ct3];
614
+ if (cdist !== 0xff && cdist !== undefined) {
615
+ const symIdx = (0, edge3_js_1.getMvSymFrom)(pairing.edge, pairing.edge, 20, sym.sym2Raw, sym.sym2Mask, sym.raw2Sym, sym.mvrot, sym.mvroto);
616
+ const edist = tables.edge3.distances.length >= 31006080
617
+ ? tables.edge3.distances[symIdx >> 3]
618
+ : (tables.edge3.distances[(symIdx >> 3) >> 1] >>
619
+ (((symIdx >> 3) & 1) * 4)) &
620
+ 0x0f;
621
+ const moves2Remapped = prep.totalRot.length === 0
622
+ ? moves2
623
+ : rotateAlgorithm(moves2, invertRotationSequence(prep.totalRot));
624
+ const hcCoord = tables.hc ? (0, hc_pdb_js_1.hcDenseIndex)(tables.hc, (0, hc_pdb_js_1.encodeHcPdb)(s2.centers)) : -1;
625
+ const hcRaw = hcCoord >= 0 && tables.hc ? tables.hc.distances[hcCoord] : 0;
626
+ const hdist = hcRaw === 0x0f ? 0 : hcRaw;
627
+ let pdist = 0;
628
+ let pdistHigh = 0;
629
+ if (tables.c3PairDist) {
630
+ const c3Base = ct3 * 144;
631
+ for (let s = 0; s < 12; s++) {
632
+ const idx = c3Base + s * 12 + pairing.edge[s];
633
+ const d = (idx & 1) === 0 ? (tables.c3PairDist[idx >> 1] & 0x0f) : (tables.c3PairDist[idx >> 1] >> 4);
634
+ if (d > pdist)
635
+ pdist = d;
636
+ if (d >= 8)
637
+ pdistHigh++;
638
+ }
639
+ }
640
+ const cand = {
641
+ moves12: [...prep.prefix, ...moves2Remapped],
642
+ length12: prep.prefix.length + moves2.length,
643
+ ct3,
644
+ cdist,
645
+ edge: pairing.edge,
646
+ edist,
647
+ hdist,
648
+ pdist,
649
+ hcCoord,
650
+ rotCost: 0,
651
+ value: prep.prefix.length + moves2.length + Math.max(cdist, edist, hdist, pdist) + (pdistHigh >= 4 ? 2 : (pdistHigh >= 2 ? 1 : 0)),
652
+ state: s2,
653
+ p1: prep.p1,
654
+ totalRot: prep.totalRot
655
+ };
656
+ if (candidates.length < CAND_CAP) {
657
+ candidates.push(cand);
658
+ if (candidates.length === CAND_CAP) {
659
+ candidates.sort((a, b) => a.value - b.value);
660
+ }
661
+ }
662
+ else if (cand.value < candidates[candidates.length - 1].value) {
663
+ candidates[candidates.length - 1] = cand;
664
+ for (let j = candidates.length - 1; j > 0 && candidates[j].value < candidates[j - 1].value; j--) {
665
+ const tmp = candidates[j];
666
+ candidates[j] = candidates[j - 1];
667
+ candidates[j - 1] = tmp;
668
+ }
669
+ }
670
+ p1CandCount++;
671
+ if (p1CandCount >= maxP1Cand)
672
+ return true;
673
+ }
674
+ }
197
675
  }
676
+ return false;
677
+ }
678
+ if (maxl === 0)
679
+ return false;
680
+ const c2RowC = c * center2_js_1.CENTER2_MOVES.length;
681
+ const c2RowR = r * center2_js_1.CENTER2_MOVES.length;
682
+ for (let mi = 0; mi < C2_SEARCH.length; mi++) {
683
+ const m = p2Order === null ? mi : p2Order[mi];
684
+ if (lm >= 0 && cancelFlat[lm * c2Stride + m]) {
685
+ continue;
686
+ }
687
+ const nc = c2ctMove[c2RowC + m];
688
+ const nr = c2rlMove[c2RowR + m];
689
+ const prun = c2pruning[nc * center2_js_1.CENTER2_RL_STATES + nr];
690
+ if (prun >= maxl) {
691
+ if (prun > maxl && canSkipAxis) {
692
+ mi = SKIP_AXIS_2[m] - 1;
693
+ }
694
+ continue;
695
+ }
696
+ let childPosValid = false;
697
+ if (maxl - 1 < TRANSVERSAL_DEPTH) {
698
+ // Lazy fill (like wpStack): replay the path down to this node.
699
+ while (validPosDepth < depth) {
700
+ const src = posStack[validPosDepth];
701
+ const dst = posStack[validPosDepth + 1];
702
+ const row = moveLog[validPosDepth] * 24;
703
+ for (let w = 0; w < 24; w++)
704
+ dst[w] = wingPosMove[row + src[w]];
705
+ validPosDepth++;
706
+ }
707
+ const parentPos = posStack[depth];
708
+ const childPos = posStack[depth + 1];
709
+ const posRow = m * 24;
710
+ const limit = maxl - 1;
711
+ let blocked = false;
712
+ // Group by group (4 wings = 2 pairs each), so a blocked child only
713
+ // pays for the groups actually checked.
714
+ for (let q = 0; q < 24; q += 4) {
715
+ const a0 = wingPosMove[posRow + parentPos[q]];
716
+ const a1 = wingPosMove[posRow + parentPos[q + 1]];
717
+ const b0 = wingPosMove[posRow + parentPos[q + 2]];
718
+ const b1 = wingPosMove[posRow + parentPos[q + 3]];
719
+ if (transversalPdb[pair_transversal_js_1.PAIR_INDEX[a0 * 24 + a1] * pair_transversal_js_1.PAIR_STATES + pair_transversal_js_1.PAIR_INDEX[b0 * 24 + b1]] > limit) {
720
+ blocked = true;
721
+ break;
722
+ }
723
+ childPos[q] = a0;
724
+ childPos[q + 1] = a1;
725
+ childPos[q + 2] = b0;
726
+ childPos[q + 3] = b1;
727
+ }
728
+ if (blocked)
729
+ continue;
730
+ childPosValid = true;
731
+ }
732
+ moveLog[depth] = m;
733
+ if (validWpDepth > depth)
734
+ validWpDepth = depth;
735
+ if (validPosDepth > depth)
736
+ validPosDepth = depth;
737
+ if (childPosValid)
738
+ validPosDepth = depth + 1;
739
+ if (searchPhase2(nc, nr, maxl - 1, m, depth + 1, bridgeState, prep))
740
+ return true;
198
741
  }
199
742
  return false;
200
743
  }
201
- if (maxl === 0)
202
- return false;
203
- for (let m = 0; m < center2_js_1.CENTER2_SEARCH_MOVES.length; m++) {
204
- if (lm >= 0 && isCancelled(center2_js_1.CENTER2_SEARCH_MOVES[lm], center2_js_1.CENTER2_SEARCH_MOVES[m])) {
744
+ let p1CandCount = 0;
745
+ let MAX_P2 = 8;
746
+ // Phase-1 application + the phase-2 bridge depend only on the phase-1
747
+ // solution, not on the phase-2 target length: compute them once instead
748
+ // of redoing the work for every length12 iteration.
749
+ // Alignment is treated purely as a coordinate frame rotation (matching
750
+ // csTimer's sym frame remapping), adding 0 physical moves to prefix.
751
+ const preps = [];
752
+ for (const p1 of p1Sols) {
753
+ const s1 = (0, moves_js_1.applyCube4Algorithm)(state, p1.moves);
754
+ const s1Aligned = p1.rotAlign.length === 0 ? s1 : applyCubeRotation(s1, p1.rotAlign);
755
+ const bridge = (0, center2_js_1.center2Bridge)(s1Aligned);
756
+ if (!bridge)
205
757
  continue;
758
+ const totalRot = [...p1.rotAlign, ...bridge.rotation];
759
+ const prefix = p1.moves;
760
+ const ct = chiral ? (0, center2_js_1.encodeCenter2ChiralCt)(bridge.state) : bridge.encoded.ct;
761
+ const prun = c2pruning[ct * center2_js_1.CENTER2_RL_STATES + bridge.encoded.rl];
762
+ // Paired wings lookahead: check how many wings are already paired in bridge.state
763
+ let pairedWings = 0;
764
+ const wp = bridge.state.wp;
765
+ for (let pair = 0; pair < 12; pair++) {
766
+ if (Math.floor(wp[pair * 2] / 2) === Math.floor(wp[pair * 2 + 1] / 2)) {
767
+ pairedWings++;
768
+ }
206
769
  }
207
- const nc = tables.center2.ctMove[c * center2_js_1.CENTER2_MOVES.length + m];
208
- const nr = tables.center2.rlMove[r * center2_js_1.CENTER2_MOVES.length + m];
209
- const prun = tables.center2.pruning[nc * center2_js_1.CENTER2_RL_STATES + nr];
210
- if (prun >= maxl)
211
- continue;
212
- moveLog[depth] = m;
213
- if (searchPhase2(nc, nr, maxl - 1, m, depth + 1, bridgeState, p1Moves))
214
- return true;
770
+ // Lookahead composite score: total estimated centers penalized by already-paired wings
771
+ const estCenters = prefix.length + prun;
772
+ const score = estCenters * 2 - Math.min(4, pairedWings);
773
+ preps.push({ p1, prefix, bridgeState: bridge.state, totalRot, ct, rl: bridge.encoded.rl, prun, score });
215
774
  }
216
- return false;
217
- }
218
- let MAX_P2 = 8;
219
- do {
220
- OUTER: for (let length12 = minP1; length12 < minP1 + 14; length12++) {
221
- for (const p1 of p1Sols) {
222
- if (p1.moves.length > length12)
223
- break;
224
- const length2 = length12 - p1.moves.length;
225
- if (length2 > MAX_P2 || length2 < 0)
226
- continue;
227
- const s1 = (0, moves_js_1.applyCube4Algorithm)(state, p1.moves);
228
- const bridge = (0, center2_js_1.center2Bridge)(s1);
229
- if (!bridge)
230
- continue;
231
- const prun = tables.center2.pruning[bridge.encoded.ct * center2_js_1.CENTER2_RL_STATES + bridge.encoded.rl];
232
- if (prun > length2)
233
- continue;
234
- const fullP1Moves = [...p1.moves, ...bridge.alignment];
235
- if (searchPhase2(bridge.encoded.ct, bridge.encoded.rl, length2, -1, 0, bridge.state, fullP1Moves)) {
236
- break OUTER;
775
+ // Ascending lookahead score (lowest total estimated center moves + wing bonus),
776
+ // tie-broken by lower phase-2 pruning bound then shorter prefix length
777
+ preps.sort((a, b) => (a.score - b.score) || (a.prun - b.prun) || (a.prefix.length - b.prefix.length));
778
+ // Filter: keep the best preps to avoid wasting phase-2 DFS work on suboptimal centers
779
+ const PREP_CAP = options?.prepCap !== undefined ? options.prepCap : (deep ? 48 : 32);
780
+ if (preps.length > PREP_CAP) {
781
+ preps.length = PREP_CAP;
782
+ }
783
+ // Per-restart tie order only (the ascending order above is preserved).
784
+ shuffleTies(preps, (p) => p.score, rng);
785
+ // Passes after the first one only extend MAX_P2 by one: every smaller
786
+ // length2 was already searched (deterministic — rerunning would push the
787
+ // same goals again as duplicates and crowd the 45 cap), so skip them.
788
+ // Tracked exactly per (prep, length2): this also fills holes left when a
789
+ // pass breaks early at the 45 cap, and is reused by the diversity
790
+ // extension below.
791
+ const searchedL2 = new Set();
792
+ let lastBestValue = 999;
793
+ let stagnantLengths = 0;
794
+ do {
795
+ OUTER: for (let length12 = minP1; length12 < minP1 + 16; length12++) {
796
+ for (let pi = 0; pi < preps.length; pi++) {
797
+ const prep = preps[pi];
798
+ if (prep.prefix.length > length12)
799
+ continue;
800
+ const length2 = length12 - prep.prefix.length;
801
+ if (length2 > MAX_P2 || length2 < 0)
802
+ continue;
803
+ if (prep.prun > length2)
804
+ continue;
805
+ const key = pi * 64 + length2;
806
+ if (searchedL2.has(key))
807
+ continue;
808
+ searchedL2.add(key);
809
+ p1CandCount = 0;
810
+ wpStack[0].set(prep.bridgeState.wp);
811
+ for (let p = 0; p < 24; p++)
812
+ posStack[0][prep.bridgeState.wp[p]] = p;
813
+ validPosDepth = 0;
814
+ wpMaskStack[0] = pairingMaskOf(prep.bridgeState.wp);
815
+ validWpDepth = 0;
816
+ searchPhase2(prep.ct, prep.rl, length2, -1, 0, prep.bridgeState, prep);
817
+ if (candidates.length >= CAND_CAP &&
818
+ candidates[0].value <= length12 + 9 &&
819
+ (candidates[0].edist <= 10 || length12 >= minP1 + 3)) {
820
+ break OUTER;
821
+ }
822
+ }
823
+ if (options?.stagnationCut !== false && candidates.length >= CAND_MIN) {
824
+ exports.stagnationStats.checks++;
825
+ // NOTE: candidates is only sorted once it reaches CAND_CAP, so
826
+ // candidates[0] is NOT the best below the cap — scan the true min.
827
+ let curBest = Infinity;
828
+ for (let ci = 0; ci < candidates.length; ci++) {
829
+ const v = candidates[ci].value;
830
+ if (v < curBest)
831
+ curBest = v;
832
+ }
833
+ if (curBest >= lastBestValue) {
834
+ stagnantLengths++;
835
+ if (stagnantLengths >= 2) {
836
+ exports.stagnationStats.cuts++;
837
+ break OUTER;
838
+ }
839
+ }
840
+ else {
841
+ lastBestValue = curBest;
842
+ stagnantLengths = 0;
843
+ }
237
844
  }
238
845
  }
846
+ MAX_P2++;
847
+ } while ((candidates.length === 0 && MAX_P2 <= P2_MAX_ABS) || (candidates.length < CAND_MIN && MAX_P2 <= P2_MAX_MIN));
848
+ if (candidates.length === 0) {
849
+ throw new Cube4Phase2CapError("4x4 solve: no valid phase 2 candidates found.");
239
850
  }
240
- MAX_P2++;
241
- } while (candidates.length === 0 && MAX_P2 <= 14);
242
- if (candidates.length === 0) {
243
- throw new Error("4x4 solve: no valid phase 2 candidates found.");
244
- }
245
- candidates.sort((a, b) => a.value - b.value);
246
- let solvedCand = null;
247
- let solvedMoves3 = null;
248
- let standardReduced = null;
249
- let rotPath = [];
250
- let rotToStandard = [];
251
- let MAX_LENGTH3 = 13;
252
- let yieldCounter = 0;
253
- do {
254
- for (let length123 = candidates[0].value; length123 < 45; length123++) {
255
- for (let i = 0; i < Math.min(candidates.length, 60); i++) {
256
- const cand = candidates[i];
257
- if (cand.value > length123)
851
+ const tF2End = performance.now();
852
+ // Cheap-fix goal filter (see Cube4SolveOptions.maxFixMoves). The deep
853
+ // fallback widens it back to 4 (legacy, always satisfiable) so a hard
854
+ // state can never fail on the filter alone.
855
+ const maxFixMoves = (options?.maxFixMoves ?? 4) + (deep ? 2 : 0);
856
+ const validReductions = [];
857
+ /**
858
+ * Per-candidate IDA* proofs shared across all sweep stages: the greatest
859
+ * phase-3 length exhaustively proved empty for that candidate. Entries
860
+ * are only written on clean proofs (no cap abort, no goal seen, no TT
861
+ * hit, complete excess info), so skipping those lengths is exact.
862
+ */
863
+ const p3Proven = new Map();
864
+ /**
865
+ * Sync downstream chain for a phase-3 goal sequence: reduction check,
866
+ * orientation remap, 3x3 conversion + phase-1 heuristic. Returns the
867
+ * ValidReduction or null. Shared by the sweep validator and the polish
868
+ * pass (which retries the winner one length shorter).
869
+ */
870
+ function buildValidReduction(cand, moves3) {
871
+ const reduced = (0, moves_js_1.applyCube4Algorithm)(cand.state, moves3);
872
+ if ((0, phase4_js_1.checkCube4Reduction)(reduced) !== null)
873
+ return null;
874
+ const totalRot = cand.totalRot;
875
+ const invTotalRot = totalRot.length === 0 ? [] : invertRotationSequence(totalRot);
876
+ const moves3Remapped = totalRot.length === 0 ? moves3 : rotateAlgorithm(moves3, invTotalRot);
877
+ const reducedOriginal = totalRot.length === 0 ? reduced : applyCubeRotation(reduced, invTotalRot);
878
+ const key = `${reducedOriginal.centers[0]},${reducedOriginal.centers[4]},${reducedOriginal.centers[8]}`;
879
+ const path = VALID_24_ORIENTATIONS.get(key);
880
+ if (!path)
881
+ return null;
882
+ const rotToStandard = invertRotationSequence(path);
883
+ const standardReduced = applyCubeRotation(reducedOriginal, rotToStandard);
884
+ const rotMoves = rotToStandard.flatMap((r) => ROT_MOVES[r]);
885
+ if (rotMoves.length > maxFixMoves)
886
+ return null;
887
+ if (typeof process !== "undefined" && process.env?.CUBE4_ROT_STATS === "1" && exports.rotStats.length < 200) {
888
+ exports.rotStats.push({
889
+ totalRot: totalRot.join(","),
890
+ rotToStandard: rotToStandard.join(","),
891
+ rotLen: rotToStandard.reduce((n, rr) => n + ROT_MOVES[rr].length, 0)
892
+ });
893
+ }
894
+ // The orientation key only covers 3 of the 24 center slots. A reduced
895
+ // arrangement outside the rotation orbit (e.g. a reflected one) can
896
+ // collide with a rotation's key, in which case rotating by `path` does
897
+ // NOT leave standard centers — the 3x3 finish would solve a different
898
+ // color scheme and the resulting solution would be garbage. Reject.
899
+ for (let i = 0; i < 24; i++) {
900
+ if (standardReduced.centers[i] !== SOLVED_CENTERS[i])
901
+ return null;
902
+ }
903
+ const cube3State = (0, phase4_js_1.toCube3State)(standardReduced);
904
+ if (!cube3State)
905
+ return null;
906
+ const h3 = tables.cube3Phase1 ? getTriAxialCube3Heuristic(standardReduced, tables.cube3Phase1) : 0;
907
+ // The reduction + rotation prefix as it will be emitted (original frame):
908
+ // apply(state, [...moves12, ...moves3Remapped ... rotMoves]) = standardReduced.
909
+ // Priced with the fix at its best position (see placeRotationFix).
910
+ const simplifiedReduction = (0, rotation_fix_js_1.placeRotationFix)([...cand.moves12, ...moves3Remapped], rotToStandard, []);
911
+ return {
912
+ cand,
913
+ moves3,
914
+ moves3Remapped,
915
+ rotPath: path,
916
+ rotToStandard,
917
+ rotMoves,
918
+ standardReduced,
919
+ h3,
920
+ totalEst: simplifiedReduction.length + h3
921
+ };
922
+ }
923
+ // Defaults 1 × 5 (was 3 × 3): with the continuous finish, extra center
924
+ // candidates stopped paying. cc:0..31: 45.19 moves @ 673 ms vs 45.16 @
925
+ // 1042 ms (2×3: 45.25 @ 799; 1×8: 45.25 @ 691).
926
+ const centersBeam = Math.max(1, options?.centersBeam ?? 1);
927
+ const edgesPerCenter = Math.max(1, options?.edgesPerCenter ?? 5);
928
+ const maxReductionsTarget = options?.finishTries !== undefined
929
+ ? Math.max(1, options.finishTries)
930
+ : (centersBeam * edgesPerCenter);
931
+ /**
932
+ * One exact-length sweep over a value-sorted pool: tries increasing
933
+ * totals, collects up to maxReductionsTarget valid reductions using
934
+ * hierarchical beam search (centersBeam distinct centers x edgesPerCenter edge pairings).
935
+ * Shared across all stages below.
936
+ *
937
+ * Bounded by `maxCalls` solveLength calls (not wall-clock): easy states
938
+ * use ~100, so adversarial pools fail fast deterministically on any
939
+ * machine instead of grinding through the whole pool.
940
+ */
941
+ function sweepPool(pool, cfg) {
942
+ const candReductions = new Map();
943
+ const distinctCenters = new Set();
944
+ let foundAtLength = null;
945
+ // The rotation fix (rotMoves) is priced INTO totalEst and varies 2-4
946
+ // moves per goal PATH (the end scheme depends on the path). A goal one
947
+ // total-step deeper with a 2-move fix beats a shallow goal with a 4-move
948
+ // fix, so keep collecting a few steps past the first find and let
949
+ // totalEst decide (was: foundAtLength + 1 with needed + 3 goals per
950
+ // call, which discarded fix-cheap alternatives).
951
+ OUTER_P3: for (let length123 = pool[0].value; length123 < L123_CAP; length123++) {
952
+ if (foundAtLength !== null && length123 > foundAtLength + 3)
258
953
  break;
259
- const length3 = length123 - cand.length12;
260
- if (length3 > MAX_LENGTH3 || length3 < 0)
261
- continue;
262
- if (cand.edist <= length3 && cand.cdist <= length3) {
263
- if (++yieldCounter % 100 === 0)
264
- await new Promise(r => setTimeout(r, 0));
265
- const moves3 = await phase3Solver.solveLength(cand.edge, cand.ct3, cand.edist, length3, tables.center3, tables.edge3.distances);
266
- if (!moves3)
954
+ // Sweep the whole (sorted) pool: see sortTop rationale.
955
+ for (let i = 0; i < pool.length; i++) {
956
+ const cand = pool[i];
957
+ if (cand.value > length123)
958
+ break;
959
+ const length3 = length123 - cand.length12;
960
+ const minL3 = Math.max(cand.edist, cand.cdist, cand.hdist, cand.pdist ?? 0);
961
+ if (length3 > cfg.maxLength3 || length3 < minL3)
267
962
  continue;
268
- const reduced = (0, moves_js_1.applyCube4Algorithm)(cand.state, moves3);
269
- const key = `${reduced.centers[0]},${reduced.centers[4]},${reduced.centers[8]}`;
270
- const path = VALID_24_ORIENTATIONS.get(key);
271
- if (!path)
963
+ // IDA* threshold propagation: a previous exact-length call may have
964
+ // exhaustively proved this candidate empty through a longer length.
965
+ if ((p3Proven.get(cand) ?? -1) >= length3)
272
966
  continue;
273
- solvedCand = cand;
274
- solvedMoves3 = moves3;
275
- rotPath = path;
276
- rotToStandard = invertRotationSequence(rotPath);
277
- standardReduced = applyCubeRotation(reduced, rotToStandard);
278
- break;
967
+ const candCount = candReductions.get(cand) ?? 0;
968
+ if (candCount >= edgesPerCenter)
969
+ continue;
970
+ if (distinctCenters.size >= centersBeam && !distinctCenters.has(cand))
971
+ continue;
972
+ if (cand.edist <= length3 && cand.cdist <= length3 && cand.hdist <= length3 && (cand.pdist ?? 0) <= length3) {
973
+ const remainingNodes = phase3NodeBudget - (phase3Solver.epochNodes - phase3StartNodes);
974
+ if (remainingNodes <= 0) {
975
+ if (validReductions.length > 0)
976
+ return;
977
+ throw new Cube4Phase3CapError("4x4 solve: phase 3 total node budget exhausted.");
978
+ }
979
+ const nodeLimit = Math.min(remainingNodes, length3 <= 12 ? 80000 : Math.max(45000, cfg.maxNodes));
980
+ f3calls++;
981
+ const needed = edgesPerCenter - candCount;
982
+ const candReductionsBuffer = [];
983
+ // Collect beyond `needed`: goal paths at the same length carry
984
+ // different rotation fixes (2 vs 4 moves), and the totalEst sort
985
+ // below needs the variety to pick a cheap-fix end.
986
+ const maxToCollect = Math.min(24, needed + 9);
987
+ const result = phase3Solver.solveLengthProven(cand.edge, cand.ct3, cand.edist, length3, tables.center3, tables.edge3.distances, nodeLimit,
988
+ // Goal budget per call: with the cheap-fix filter most goal ends
989
+ // are rejected, so the walk must be allowed to examine more of
990
+ // them before giving up on this (candidate, length3).
991
+ 48, (moves3) => {
992
+ const red = buildValidReduction(cand, moves3);
993
+ if (!red)
994
+ return false;
995
+ candReductionsBuffer.push(red);
996
+ // <=-search: goals may land shorter than the length3 bound —
997
+ // track the true total for the post-find collection window.
998
+ const total = cand.length12 + moves3.length;
999
+ if (foundAtLength === null || total < foundAtLength)
1000
+ foundAtLength = total;
1001
+ return candReductionsBuffer.length >= maxToCollect;
1002
+ },
1003
+ // hc mid-search prune disabled: on a 44-state corpus the per-node
1004
+ // hc lookups cost more than they prune, and the altered
1005
+ // capped/success pattern shifts the stop-at-3 valid selection
1006
+ // toward shorter reductions (which finish as well or better on
1007
+ // average). The exact hdist call gate above, the candidate hdist
1008
+ // ranking, and the polish-pass hc bounds below all stay.
1009
+ undefined, -1, tables.c3PairDist);
1010
+ if (candReductionsBuffer.length > 0) {
1011
+ // Sort solutions for this center candidate by totalEst (cancellation-aware length + tri-axial 3x3 heuristic)
1012
+ candReductionsBuffer.sort((a, b) => a.totalEst - b.totalEst);
1013
+ const toAdd = candReductionsBuffer.slice(0, needed);
1014
+ for (const red of toAdd) {
1015
+ validReductions.push(red);
1016
+ }
1017
+ candReductions.set(cand, candCount + toAdd.length);
1018
+ distinctCenters.add(cand);
1019
+ }
1020
+ // Predicate-scoped proof (same buildValidReduction filter for this
1021
+ // candidate across all its calls): skip every settled length.
1022
+ const proven = result.validatedThrough;
1023
+ if (result.moves === null && proven >= length3) {
1024
+ const prev = p3Proven.get(cand) ?? -1;
1025
+ if (proven > prev)
1026
+ p3Proven.set(cand, proven);
1027
+ }
1028
+ // The finish loop attempts reductions up to maxReductionsTarget
1029
+ // (with a wider collection cap so cheap-fix ends at later length
1030
+ // steps can still displace expensive-fix ones by totalEst).
1031
+ if (validReductions.length >= maxReductionsTarget + 9) {
1032
+ break OUTER_P3;
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+ }
1038
+ const sortTop = (arr) => {
1039
+ // No truncation: value misranks solvability (short bounds, hard joints),
1040
+ // and truncating demonstrably discards winners (corpus-18/49). Cost lands
1041
+ // only on hard states (early-stop rules); easy states stop early anyway.
1042
+ const sorted = [...arr].sort((a, b) => a.value - b.value);
1043
+ // Per-restart tie order only (ascending value order is preserved, so the
1044
+ // sweep's `value > length123` break stays valid).
1045
+ shuffleTies(sorted, (c) => c.value, rng);
1046
+ return sorted;
1047
+ };
1048
+ // Sweeps with growing budgets; longitudes extend dynamically (reference
1049
+ // style) while nothing is found, instead of fixed caps. Easy states only
1050
+ // pay the first sweep. The length3 caps are NOT redundant re-walks: the
1051
+ // ladder widens the pairing cap ONLY when nothing was found at all, and the
1052
+ // narrower caps keep early sweeps on short pairings (measured: a single
1053
+ // merged sweep with maxLength3 24 is ~20% SLOWER with identical output).
1054
+ // NOTE: lowering these caps was tried (20k/16k) and reverted: capped calls
1055
+ // change which valids are found (±2 moves on the corpus). Only admissible
1056
+ // heuristic strengthenings are length-safe here.
1057
+ sweepPool(sortTop(candidates), { maxNodes: 30000, maxLength3: 14 });
1058
+ if (validReductions.length === 0) {
1059
+ sweepPool(sortTop(candidates), { maxNodes: 30000, maxLength3: 16 });
1060
+ }
1061
+ if (validReductions.length === 0) {
1062
+ sweepPool(sortTop(candidates), { maxNodes: 30000, maxLength3: 18 });
1063
+ }
1064
+ if (validReductions.length === 0) {
1065
+ sweepPool(sortTop(candidates), { maxNodes: 30000, maxLength3: 20 });
1066
+ }
1067
+ if (validReductions.length === 0) {
1068
+ sweepPool(sortTop(candidates), { maxNodes: 30000, maxLength3: 24 });
1069
+ }
1070
+ if (validReductions.length === 0 && deep) {
1071
+ sweepPool(sortTop(candidates), { maxNodes: 60000, maxLength3: 27 });
1072
+ }
1073
+ // Diversity extension (only when both sweeps found nothing): the pool's
1074
+ // joints are jointly hard, so gather DEEPER phase-2 solutions (longer
1075
+ // length2 = different wings) and sweep those. Only hard states pay this.
1076
+ if (validReductions.length === 0 && !options?.maxReductionLen) {
1077
+ const poolStart = candidates.length;
1078
+ let MX = MAX_P2;
1079
+ EXTEND: while (candidates.length - poolStart < 40 && MX <= EXT_MX) {
1080
+ for (let length12 = minP1; length12 < minP1 + 18; length12++) {
1081
+ for (let pi = 0; pi < preps.length; pi++) {
1082
+ const prep = preps[pi];
1083
+ if (prep.prefix.length > length12)
1084
+ break;
1085
+ const length2 = length12 - prep.prefix.length;
1086
+ if (length2 > MX || length2 < 0)
1087
+ continue;
1088
+ if (prep.prun > length2)
1089
+ continue;
1090
+ const key = pi * 64 + length2;
1091
+ if (searchedL2.has(key))
1092
+ continue;
1093
+ searchedL2.add(key);
1094
+ p1CandCount = 0;
1095
+ wpStack[0].set(prep.bridgeState.wp);
1096
+ for (let p = 0; p < 24; p++)
1097
+ posStack[0][prep.bridgeState.wp[p]] = p;
1098
+ validPosDepth = 0;
1099
+ wpMaskStack[0] = pairingMaskOf(prep.bridgeState.wp);
1100
+ validWpDepth = 0;
1101
+ searchPhase2(prep.ct, prep.rl, length2, -1, 0, prep.bridgeState, prep);
1102
+ if (candidates.length - poolStart >= 40)
1103
+ break EXTEND;
1104
+ }
279
1105
  }
1106
+ MX++;
1107
+ }
1108
+ const fresh = sortTop(candidates.slice(poolStart));
1109
+ if (fresh.length > 0) {
1110
+ sweepPool(fresh, { maxNodes: 15000, maxLength3: 18 });
1111
+ }
1112
+ if (validReductions.length === 0 && fresh.length > 0) {
1113
+ sweepPool(fresh, { maxNodes: 30000, maxLength3: 20 });
1114
+ }
1115
+ }
1116
+ if (validReductions.length === 0) {
1117
+ throw new Cube4Phase3CapError("4x4 solve: phase 3 reduction failed across all candidates.");
1118
+ }
1119
+ // Sort reductions by Total Estimated Moves (Reduction length + 3x3 Phase 1 lower bound)
1120
+ validReductions.sort((a, b) => a.totalEst - b.totalEst);
1121
+ const tFinishStart = performance.now();
1122
+ let bestSolution = null;
1123
+ let minTotalMoves = 999;
1124
+ let bestPhases;
1125
+ let bestRed = null;
1126
+ // Best-of among the first two (as before); any further reductions are
1127
+ // only attempted when those both failed, so a flaky 3x3 finish cannot
1128
+ // sink the whole attempt while the common case costs nothing extra.
1129
+ // Best-of among the first three (phase-4 is cheap now); further
1130
+ // reductions are only attempted when those all failed.
1131
+ const finishTries = options?.finishTries ?? maxReductionsTarget;
1132
+ const collectFinishStats = typeof process !== "undefined" && process.env?.CUBE4_FINISH_STATS === "1";
1133
+ // Empirical finish lower bound (see finishStats): every observed finish
1134
+ // ran 11-16 moves ABOVE `totalEst` (continuous finish: 67 finishes over 16
1135
+ // seeds, gap = totalEst - finalLen in [-16, -11]; the legacy finish was
1136
+ // [-16, -12]) because `h3` only bounds the cube3 PHASE-1 length while the
1137
+ // finish is two-phase. A reduction whose totalEst + 11 cannot strictly
1138
+ // beat the current best therefore cannot win, and its finish is skipped
1139
+ // entirely. If a state ever lands a finish within 11 of the phase-1 bound,
1140
+ // this degrades to a slightly longer (still exact,
1141
+ // still valid) scramble for that state — never an incorrect one.
1142
+ const FINISH_LB_BIAS = 11;
1143
+ const screenTop = Math.max(0, Math.floor(options?.finishScreenTop ?? 0));
1144
+ const screened = [];
1145
+ /** Finishes one reduction; records it as the best when shorter. */
1146
+ async function finishReduction(red, effort) {
1147
+ const cube3State = (0, phase4_js_1.toCube3State)(red.standardReduced);
1148
+ if (!cube3State)
1149
+ return null;
1150
+ let cube3Solution;
1151
+ try {
1152
+ cube3Solution = await solveCube3(cube3State, effort);
1153
+ }
1154
+ catch {
1155
+ return null;
1156
+ }
1157
+ // Frame invariant (verified): apply(state, [...moves12, ...moves3Remapped])
1158
+ // is exactly `reducedOriginal`, and `reducedOriginal · rotMoves(r)`
1159
+ // (= applyCubeRotation(_, r)) is `standardReduced`, which the cube3
1160
+ // finish C solves. So the solution in the ORIGINAL frame is
1161
+ // X ++ rotMoves(r) ++ C.
1162
+ // Two previously-tried assemblies are WRONG:
1163
+ // - [rot_r(X), C] solves ρ_r(state), not state (the observed adaptive
1164
+ // whole-cube-rotation bug: apply(solved, invert(moves)) = ρ_r(T));
1165
+ // - [X, rot_{-r}(C)] lands on ρ_{-r}(solved) ≠ solved, because a
1166
+ // whole-cube rotation does NOT leave the canonical solved state
1167
+ // invariant (it permutes face colors in the absolute frame).
1168
+ // `placeRotationFix` keeps that effect but may move the rotMoves(r)
1169
+ // block anywhere in the sequence (re-mapping what follows it) where it
1170
+ // cancels against adjacent wide moves.
1171
+ const simplified = (0, rotation_fix_js_1.placeRotationFix)([...red.cand.moves12, ...red.moves3Remapped], red.rotToStandard, cube3Solution.moves);
1172
+ if (collectFinishStats) {
1173
+ exports.finishStats.finishes++;
1174
+ const gap = red.totalEst - simplified.length;
1175
+ if (gap < exports.finishStats.gapMin)
1176
+ exports.finishStats.gapMin = gap;
1177
+ if (gap > exports.finishStats.gapMax)
1178
+ exports.finishStats.gapMax = gap;
1179
+ exports.finishStats.gapSum += gap;
1180
+ exports.finishStats.gapN++;
280
1181
  }
281
- if (solvedCand)
1182
+ if (simplified.length < minTotalMoves) {
1183
+ minTotalMoves = simplified.length;
1184
+ bestSolution = simplified;
1185
+ bestPhases = {
1186
+ length12: red.cand.length12,
1187
+ length1: red.cand.p1.moves.length,
1188
+ length2: red.cand.length12 - red.cand.p1.moves.length,
1189
+ length3: red.moves3.length,
1190
+ finish: cube3Solution.moves.length
1191
+ };
1192
+ bestRed = red;
1193
+ }
1194
+ return simplified;
1195
+ }
1196
+ for (let i = 0; i < validReductions.length; i++) {
1197
+ if (i >= finishTries && bestSolution)
282
1198
  break;
1199
+ const red = validReductions[i];
1200
+ if (bestSolution !== null && red.totalEst + FINISH_LB_BIAS >= minTotalMoves) {
1201
+ exports.finishStats.skipped++;
1202
+ continue;
1203
+ }
1204
+ const done = await finishReduction(red, screenTop > 0 ? "screen" : "full");
1205
+ if (done)
1206
+ screened.push({ red, total: done.length });
1207
+ }
1208
+ // Stage 2: the best screened reductions get the full-effort finish.
1209
+ if (screenTop > 0) {
1210
+ screened.sort((a, b) => a.total - b.total);
1211
+ for (const { red } of screened.slice(0, screenTop)) {
1212
+ if (red.totalEst + FINISH_LB_BIAS >= minTotalMoves)
1213
+ continue;
1214
+ await finishReduction(red, "full");
1215
+ }
1216
+ }
1217
+ if (!bestSolution || !bestRed) {
1218
+ throw new Cube4FinishCapError("4x4 solve: reduction failed during the 3x3 finish across all valid reductions.");
1219
+ }
1220
+ // Polish pass: the sweep takes the first valid sequence per exact
1221
+ // length, which is often one above optimal. Retry the winner one length
1222
+ // shorter with a large budget (up to 2 iterations). Gated on totals
1223
+ // above target so easy states never pay it.
1224
+ const tPolishStart = performance.now();
1225
+ // Polish the winner plus the runner-up by totalEst (was: winner only).
1226
+ // Runner-up shortenings get their own finish and can only lower the min
1227
+ // over finished totals; easy states skip the inner loop via the length
1228
+ // gate below, so the extra pass costs ~nothing there.
1229
+ const redToPolish = options?.deepPolish
1230
+ ? validReductions.slice(0, 3)
1231
+ : [bestRed, ...validReductions.filter((r) => r !== bestRed)].slice(0, 2);
1232
+ for (const r of redToPolish) {
1233
+ let curRed = r;
1234
+ // The runner-up gets a cheap probe budget: its shortening is a bonus,
1235
+ // and a full 150k grind on tail states cost seconds for nothing. It is
1236
+ // also skipped entirely unless the total is high enough to matter.
1237
+ const polishBudget = r === bestRed ? 150000 : 30000;
1238
+ // NOTE: with the <=-length phase-3 search the sweep already enumerates
1239
+ // shorter goals, so the polish is a second-chance pass only (measured:
1240
+ // a wider gate/budget found nothing and cost +32% p3 nodes).
1241
+ for (let iter = 0; iter < 4 && (minTotalMoves > 43 || curRed.moves3.length > 11); iter++) {
1242
+ const cand = curRed.cand;
1243
+ const shorterLen = curRed.moves3.length - 1;
1244
+ if (shorterLen < cand.edist || shorterLen < cand.cdist || shorterLen < cand.hdist || shorterLen < (cand.pdist ?? 0))
1245
+ break;
1246
+ f3calls++;
1247
+ // Unified polish: accept any goal path with a strictly better
1248
+ // totalEst at the same or shorter length — covers both a shorter
1249
+ // phase-3 path and a cheaper rotation fix (the fix is priced into
1250
+ // totalEst and varies 2-4 moves per goal path).
1251
+ const shorter = phase3Solver.solveLengthValid(cand.edge, cand.ct3, cand.edist, curRed.moves3.length, tables.center3, tables.edge3.distances, polishBudget, 64, (moves3) => {
1252
+ const nr = buildValidReduction(cand, moves3);
1253
+ return nr !== null && nr.totalEst < curRed.totalEst;
1254
+ }, tables.hc, cand.hcCoord, tables.c3PairDist);
1255
+ if (!shorter)
1256
+ break;
1257
+ const red = buildValidReduction(cand, shorter);
1258
+ if (!red)
1259
+ break;
1260
+ const cube3State = (0, phase4_js_1.toCube3State)(red.standardReduced);
1261
+ if (!cube3State)
1262
+ break;
1263
+ let cube3Solution;
1264
+ try {
1265
+ cube3Solution = await solveCube3(cube3State);
1266
+ }
1267
+ catch {
1268
+ break;
1269
+ }
1270
+ // Same frame invariant as the finish loop: X ++ rotMoves(r) ++ C.
1271
+ const simplified = (0, rotation_fix_js_1.placeRotationFix)([...red.cand.moves12, ...red.moves3Remapped], red.rotToStandard, cube3Solution.moves);
1272
+ if (simplified.length < minTotalMoves) {
1273
+ minTotalMoves = simplified.length;
1274
+ bestSolution = simplified;
1275
+ curRed = red;
1276
+ bestPhases = {
1277
+ length12: red.cand.length12,
1278
+ length1: red.cand.p1.moves.length,
1279
+ length2: red.cand.length12 - red.cand.p1.moves.length,
1280
+ length3: red.moves3.length,
1281
+ finish: cube3Solution.moves.length
1282
+ };
1283
+ }
1284
+ else {
1285
+ break;
1286
+ }
1287
+ }
1288
+ }
1289
+ accP2 += p2nodes;
1290
+ accF3calls += f3calls;
1291
+ accF1Ms += tF1End - tR0;
1292
+ accF2Ms += tF2End - tF1End;
1293
+ accF3Ms += tFinishStart - tF2End;
1294
+ accFinishMs += tPolishStart - tFinishStart;
1295
+ accPolishMs += performance.now() - tPolishStart;
1296
+ // Final invariant: the emitted solution must solve the INPUT state
1297
+ // exactly (no rotation tolerance). Fail loudly rather than return a
1298
+ // solution that does not resolve. One replay of ~45 moves — negligible
1299
+ // next to the search cost.
1300
+ if (!(0, state_js_1.isSolvedCube4State)((0, moves_js_1.applyCube4Algorithm)(state, bestSolution))) {
1301
+ throw new Error("4x4 solve: internal verification failed — the solution does not solve the input state.");
1302
+ }
1303
+ return { moves: bestSolution, phases: bestPhases };
1304
+ };
1305
+ let bestMoves = null;
1306
+ let bestPhasesOverall;
1307
+ let lastRestartError = null;
1308
+ for (let restart = 0; restart < totalRestarts; restart++) {
1309
+ const rng = totalRestarts > 1
1310
+ ? mulberry32((hashCube4State(state) ^ Math.imul(restart + 1, 0x9e3779b9)) >>> 0)
1311
+ : null;
1312
+ try {
1313
+ const one = await runOnce(rng);
1314
+ if (!bestMoves || one.moves.length < bestMoves.length) {
1315
+ bestMoves = one.moves;
1316
+ bestPhasesOverall = one.phases;
1317
+ }
1318
+ }
1319
+ catch (error) {
1320
+ lastRestartError = error;
283
1321
  }
284
- MAX_LENGTH3++;
285
- } while (!solvedCand && MAX_LENGTH3 <= 18);
286
- if (!solvedCand || !solvedMoves3 || !standardReduced) {
287
- throw new Error("4x4 solve: reduction failed across all candidates.");
288
- }
289
- const cube3State = (0, phase4_js_1.toCube3State)(standardReduced);
290
- if (!cube3State) {
291
- throw new Error("4x4 solve: failed to convert reduced 4x4 state to 3x3 state.");
292
1322
  }
293
- let cube3Solution;
294
- try {
295
- cube3Solution = await solveCube3(cube3State);
1323
+ if (!bestMoves) {
1324
+ throw lastRestartError instanceof Error
1325
+ ? lastRestartError
1326
+ : new Error(`4x4 solve: all ${totalRestarts} restarts failed: ${String(lastRestartError)}`);
296
1327
  }
297
- catch (error) {
298
- if (error instanceof index_js_1.Cube3Phase2SearchError) {
299
- throw error;
1328
+ const scramble = (0, moves_js_1.invertCube4Algorithm)(bestMoves);
1329
+ const result = {
1330
+ moves: bestMoves,
1331
+ scramble,
1332
+ nodes: { p2: accP2, p3: phase3Solver.epochNodes, f3calls: accF3calls },
1333
+ timings: {
1334
+ reductionMs: accF1Ms + accF2Ms + accF3Ms,
1335
+ f1Ms: accF1Ms,
1336
+ f2Ms: accF2Ms,
1337
+ f3Ms: accF3Ms,
1338
+ finishMs: accFinishMs,
1339
+ polishMs: accPolishMs
300
1340
  }
301
- throw error;
302
- }
303
- const fullSolInOriginal = [
304
- ...solvedCand.moves12,
305
- ...solvedMoves3,
306
- ...rotateAlgorithm(cube3Solution.moves, rotPath)
307
- ];
308
- const fullSolToStandard = [
309
- ...fullSolInOriginal,
310
- ...rotToStandard.flatMap((r) => ROT_MOVES[r])
311
- ];
312
- const scramble = (0, moves_js_1.invertCube4Algorithm)(fullSolToStandard);
313
- return { moves: fullSolToStandard, scramble };
1341
+ };
1342
+ if (bestPhasesOverall)
1343
+ result.phases = bestPhasesOverall;
1344
+ return result;
314
1345
  }