@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
@@ -116,12 +116,12 @@ function applyPairingMoveToArray(arr24, move) {
116
116
  * and preserved by the phase-3 move set (verified).
117
117
  */
118
118
  function hasPairingTransversal(wp) {
119
- const seen = new Uint8Array(12);
120
- for (const s of exports.PAIRING_SECOND_HALVES) {
121
- const pair = wp[s] >> 1;
122
- if (seen[pair] !== 0)
119
+ let mask = 0;
120
+ for (let i = 0; i < 12; i++) {
121
+ const bit = 1 << (wp[exports.PAIRING_SECOND_HALVES[i]] >> 1);
122
+ if ((mask & bit) !== 0)
123
123
  return false;
124
- seen[pair] = 1;
124
+ mask |= bit;
125
125
  }
126
126
  return true;
127
127
  }
@@ -6,10 +6,12 @@
6
6
  * and all 12 wing pairs paired (sym-index == 0), using the 17-move exploration set.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.FastPhase3Solver = exports.skipAxis3 = exports.ckmv3 = exports.SEARCH_MOVES = void 0;
9
+ exports.proofStats = exports.FastPhase3Solver = exports.skipAxis3 = exports.ckmv3 = exports.SEARCH_MOVES = void 0;
10
+ exports.resetProofStats = resetProofStats;
10
11
  exports.solvePhase3 = solvePhase3;
11
12
  const center3_js_1 = require("./center3.js");
12
13
  const edge3_js_1 = require("./edge3.js");
14
+ const hc_pdb_js_1 = require("./hc-pdb.js");
13
15
  /** The 17 exploration moves (the reference's m < 17 bound). */
14
16
  exports.SEARCH_MOVES = center3_js_1.CENTER3_MOVES.slice(0, 17);
15
17
  // Commutation / move-cancellation pruning tables
@@ -37,86 +39,511 @@ for (let i = 0; i < 20; i++) {
37
39
  }
38
40
  }
39
41
  }
40
- /**
41
- * Fast table-driven Phase 3 IDA* solver class.
42
- */
42
+ /** Popcount of 12-bit masks (the inlined sym-coordinate rank). */
43
+ const POPC12 = new Uint8Array(4096);
44
+ for (let i = 1; i < 4096; i++)
45
+ POPC12[i] = POPC12[i >> 1] + (i & 1);
46
+ /** Flat row-major ckmv3 (21x20) + typed skipAxis3 for the hot recursion. */
47
+ const ckmv3Flat = new Uint8Array(21 * 20);
48
+ for (let i = 0; i < 21; i++) {
49
+ for (let j = 0; j < 20; j++)
50
+ ckmv3Flat[i * 20 + j] = exports.ckmv3[i][j] ? 1 : 0;
51
+ }
52
+ const skipAxis3Fast = Int32Array.from(exports.skipAxis3);
53
+ const F_SLOT_MAP = [
54
+ [4, 5, 2, 3, 1, 0, 6, 7, 8, 9, 10, 11],
55
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 9, 10, 11],
56
+ [5, 4, 2, 3, 0, 1, 6, 7, 8, 9, 10, 11],
57
+ [0, 1, 2, 3, 4, 6, 5, 7, 8, 9, 11, 10],
58
+ [11, 1, 2, 8, 4, 5, 6, 7, 0, 9, 10, 3],
59
+ [3, 1, 2, 0, 4, 5, 6, 7, 11, 9, 10, 8],
60
+ [8, 1, 2, 11, 4, 5, 6, 7, 3, 9, 10, 0],
61
+ [0, 1, 7, 6, 4, 5, 2, 3, 8, 9, 10, 11],
62
+ [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 10, 11],
63
+ [0, 1, 6, 7, 4, 5, 3, 2, 8, 9, 10, 11],
64
+ [0, 1, 2, 3, 7, 5, 6, 4, 9, 8, 10, 11],
65
+ [0, 9, 10, 3, 4, 5, 6, 7, 8, 2, 1, 11],
66
+ [0, 2, 1, 3, 4, 5, 6, 7, 8, 10, 9, 11],
67
+ [0, 10, 9, 3, 4, 5, 6, 7, 8, 1, 2, 11],
68
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 11, 10, 9],
69
+ [0, 3, 2, 1, 4, 6, 5, 7, 8, 9, 11, 10],
70
+ [3, 1, 2, 0, 4, 7, 6, 5, 11, 9, 10, 8]
71
+ ];
72
+ const H_SLOT_MAP = [
73
+ [4, 5, 2, 3, 1, 0, 6, 7, 8, 9, 10, 11],
74
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 9, 10, 11],
75
+ [5, 4, 2, 3, 0, 1, 6, 7, 8, 9, 10, 11],
76
+ [0, 1, 2, 3, 4, 6, 5, 7, 8, 9, 11, 10],
77
+ [11, 1, 2, 8, 4, 5, 6, 7, 0, 9, 10, 3],
78
+ [3, 1, 2, 0, 4, 5, 6, 7, 11, 9, 10, 8],
79
+ [8, 1, 2, 11, 4, 5, 6, 7, 3, 9, 10, 0],
80
+ [0, 1, 7, 6, 4, 5, 2, 3, 8, 9, 10, 11],
81
+ [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 10, 11],
82
+ [0, 1, 6, 7, 4, 5, 3, 2, 8, 9, 10, 11],
83
+ [0, 1, 2, 3, 7, 5, 6, 4, 9, 8, 10, 11],
84
+ [0, 9, 10, 3, 4, 5, 6, 7, 8, 2, 1, 11],
85
+ [0, 2, 1, 3, 4, 5, 6, 7, 8, 10, 9, 11],
86
+ [0, 10, 9, 3, 4, 5, 6, 7, 8, 1, 2, 11],
87
+ [1, 0, 2, 3, 5, 4, 6, 7, 10, 9, 8, 11],
88
+ [2, 1, 0, 3, 4, 6, 5, 7, 8, 9, 11, 10],
89
+ [3, 1, 2, 0, 6, 5, 4, 7, 11, 9, 10, 8]
90
+ ];
43
91
  class FastPhase3Solver {
44
92
  tempep;
45
93
  moveLog;
46
94
  sym;
95
+ /** Transposition Table size: 2^20 = 1,048,576 entries (12 MiB per solve).
96
+ * The previous 2M table used 24 MiB per process, plus a separate 8 MiB
97
+ * generation array. The compact 1M table leaves room for concurrent
98
+ * solvers under the 150 MiB reduction-table budget. */
99
+ static TT_SIZE = 1 << 20;
100
+ static TT_MASK = FastPhase3Solver.TT_SIZE - 1;
101
+ tt = new Uint32Array(FastPhase3Solver.TT_SIZE * 3);
102
+ // TT info packs (call id:20, previous move:5, remaining depth:6), avoiding
103
+ // a separate 8 MiB generation array while retaining exact proof tracking.
104
+ static CALL_ID_LIMIT = 1 << 20;
105
+ callSeq = 1;
106
+ callId = 0;
107
+ contextCenter3;
108
+ contextEdge3;
109
+ /** Reset this solver's cumulative node counter (legacy method name). */
110
+ resetEpochNodes() {
111
+ this.epochNodes = 0;
112
+ }
113
+ clearTranspositionTable() {
114
+ this.tt.fill(0);
115
+ this.callSeq = 1;
116
+ }
117
+ setTableContext(center3, edge3Distances) {
118
+ if (this.contextCenter3 === center3 && this.contextEdge3 === edge3Distances)
119
+ return;
120
+ this.clearTranspositionTable();
121
+ this.contextCenter3 = center3;
122
+ this.contextEdge3 = edge3Distances;
123
+ }
124
+ /** Fresh per-call id for TT-generation tagging. */
125
+ nextCallId() {
126
+ const id = this.callSeq++;
127
+ if (this.callSeq >= FastPhase3Solver.CALL_ID_LIMIT) {
128
+ this.tt.fill(0);
129
+ this.callSeq = 1;
130
+ }
131
+ return id;
132
+ }
47
133
  constructor() {
48
- this.tempep = Array.from({ length: 30 }, () => new Uint8Array(12));
49
- this.moveLog = new Array(30);
134
+ // Preallocated to well above the deepest phase-3 sweep (maxLength3 <= 27):
135
+ // `ensureDepth` stays as a fallback for out-of-band callers.
136
+ this.tempep = Array.from({ length: 48 }, () => new Uint8Array(12));
137
+ this.moveLog = new Array(48);
50
138
  this.sym = (0, edge3_js_1.getSymTables)();
51
139
  }
52
- getPrun(distances, idx) {
53
- const byte = distances[idx >> 1];
54
- return (idx & 1) === 0 ? byte & 0x0f : byte >> 4;
140
+ ensureDepth(depth) {
141
+ while (this.tempep.length <= depth) {
142
+ this.tempep.push(new Uint8Array(12));
143
+ this.moveLog.push(0);
144
+ }
55
145
  }
146
+ nodeCount = 0;
147
+ /** Cumulative nodes across calls (reset per solve generation). */
148
+ epochNodes = 0;
149
+ /**
150
+ * Proof bookkeeping for IDA* threshold propagation (reset per exact-length
151
+ * call). When a length-L search exhausts its tree WITHOUT hitting any of
152
+ * these flags, no (center, edge) solution exists at any length below
153
+ * `minExcess`, so the caller may skip straight to it:
154
+ * - `capped`: the node budget aborted the search (bounds unknown).
155
+ * - `limitHit`: goal enumeration hit its cap (tree not exhausted).
156
+ * - `sawGoal`: a goal was reached (even if the validator rejected it —
157
+ * a nearby length may still validate, so only step L+1).
158
+ * - `ttTouched`: a transposition-table hit from ANOTHER exact-length call
159
+ * skipped a subtree, and the hit was not certified goal-free across
160
+ * candidate validators.
161
+ * Bound-based axis skips need no flag: the skipped siblings are
162
+ * same-face turns, so noteExcess records their triangle-inequality bound.
163
+ */
164
+ capped = false;
165
+ limitHit = false;
166
+ sawGoal = false;
167
+ ttTouched = false;
168
+ minExcess = 0;
56
169
  /**
57
- * Recursive IDA* worker on table transitions.
170
+ * Root-length coverage of the tightest TT hit this call (depth +
171
+ * storedDepth): the pruned subtree proves no goal at or below that root
172
+ * length under the call's predicate. `Number.MAX_SAFE_INTEGER` when no hit
173
+ * pruned anything. Feeds `validatedThrough`.
58
174
  */
59
- async search(eplast, ct, prun, maxl, lm, depth, center3Tables, edge3Distances) {
60
- if (maxl === 0)
61
- return true;
62
- if (depth % 500 === 0)
63
- await new Promise(r => setTimeout(r, 0));
175
+ ttCoverage = Number.MAX_SAFE_INTEGER;
176
+ /**
177
+ * Recursive IDA* worker on table transitions with Transposition Table pruning.
178
+ */
179
+ search(eplast, ct, prun, maxl, lm, depth, center3Tables, edge3Distances, maxNodes = 15000, collect, hc, hcCoord = -1, c3PairDist) {
180
+ this.nodeCount++;
181
+ this.epochNodes++;
182
+ if (this.nodeCount > maxNodes) {
183
+ this.capped = true;
184
+ return false;
185
+ }
186
+ // Goal test at EVERY node (any length <= maxl), not only at maxl === 0
187
+ // leaves: a goal is (edge paired AND center3 at goal) — both PDB
188
+ // distances zero (the parent's gates guarantee leaves are goals, so this
189
+ // strictly generalizes the old leaf test). Exact-length-only detection
190
+ // made shorter goals on the same path invisible ("a nearby length may
191
+ // still validate"), which both blocked proofs on any raw goal and hid
192
+ // shorter solutions.
193
+ if (prun === 0 && center3Tables.pruning[ct] === 0) {
194
+ this.sawGoal = true;
195
+ if (!collect)
196
+ return true;
197
+ collect.examined++;
198
+ const moves = this.moveLog.slice(0, depth).map((m) => exports.SEARCH_MOVES[m]);
199
+ if (collect.validate(moves))
200
+ return true;
201
+ if (collect.examined >= collect.limit) {
202
+ this.limitHit = true;
203
+ return true;
204
+ }
205
+ // A rejected goal is not a goal for the validator's predicate: keep
206
+ // exploring the subtree — validated goals may sit deeper on the same
207
+ // branches. At the fringe (maxl === 0) there is no subtree left, so
208
+ // record the branch's coverage claim here (nothing below `depth` on
209
+ // this branch was searched): "no validated goal at <= depth".
210
+ if (maxl === 0 && depth < this.minExcess)
211
+ this.minExcess = depth + 1;
212
+ }
213
+ else if (maxl === 0) {
214
+ return false;
215
+ }
216
+ if (depth >= this.tempep.length)
217
+ this.ensureDepth(depth);
218
+ const sym = this.sym;
219
+ const symMvrot = sym.mvrot;
220
+ const symMvroto = sym.mvroto;
64
221
  const ep = this.tempep[depth];
65
222
  if (lm !== 20) {
66
223
  const movoBase = (lm << 3) * 12;
67
- const movBase = (lm << 3) * 12;
68
224
  for (let i = 0; i < 12; i++) {
69
- ep[i] = this.sym.mvroto[movoBase + eplast[this.sym.mvrot[movBase + i]]];
225
+ ep[i] = symMvroto[movoBase + eplast[symMvrot[movoBase + i]]];
70
226
  }
71
227
  }
72
228
  else {
73
229
  ep.set(eplast);
74
230
  }
231
+ const hasHc = hc !== undefined && hcCoord >= 0 && hc.moves.length > 0;
232
+ // HC pruning depends on a second coordinate not represented in the TT
233
+ // key. The root c3-pair check also varies only at depth zero. Do not reuse
234
+ // entries in those contexts; all other phase-3 state is key-complete.
235
+ const useTt = !hasHc && !(depth === 0 && c3PairDist !== undefined);
236
+ // TT: entries claim "no RAW goal at any length <= maxl from (ep, lm)" —
237
+ // coordinate-complete and predicate-independent (the validator's
238
+ // full-state checks are NOT coordinate-complete, so no validator-scoped
239
+ // claims may be cached). Hits are `<=`-claims and therefore
240
+ // proof-compatible for `validatedThrough`/`provenThrough` coverage.
241
+ const ttActive = useTt && maxl >= 2;
242
+ let keyHigh = 0;
243
+ let keyLow = 0;
244
+ let slot = 0;
245
+ if (ttActive) {
246
+ keyHigh = ((ct << 16) | (ep[8] | (ep[9] << 4) | (ep[10] << 8) | (ep[11] << 12))) >>> 0;
247
+ keyLow = (ep[0] | (ep[1] << 4) | (ep[2] << 8) | (ep[3] << 12) |
248
+ (ep[4] << 16) | (ep[5] << 20) | (ep[6] << 24) | (ep[7] << 28)) >>> 0;
249
+ slot = (((keyLow ^ Math.imul(keyHigh, 0x9e3779b9)) & FastPhase3Solver.TT_MASK) * 3) >>> 0;
250
+ const storedHigh = this.tt[slot];
251
+ const storedLow = this.tt[slot + 1];
252
+ const storedInfo = this.tt[slot + 2];
253
+ if (storedHigh === keyHigh && storedLow === keyLow) {
254
+ const storedCallId = (storedInfo >>> 11) & 0xfffff;
255
+ const storedLm = (storedInfo >>> 6) & 0x1f;
256
+ const storedDepth = storedInfo & 0x3f;
257
+ if (storedDepth >= maxl && storedLm === lm) {
258
+ const coverage = depth + storedDepth;
259
+ if (coverage < this.ttCoverage)
260
+ this.ttCoverage = coverage;
261
+ if (storedCallId !== this.callId)
262
+ this.ttTouched = true;
263
+ return false;
264
+ }
265
+ }
266
+ }
267
+ const c3Move = center3Tables.moveTable;
268
+ const c3Prun = center3Tables.pruning;
269
+ const c3Row = ct * center3_js_1.CENTER3_MOVES.length;
270
+ const sym2Raw = sym.sym2Raw;
271
+ const sym2Mask = sym.sym2Mask;
272
+ const raw2Sym = sym.raw2Sym;
273
+ const hcMoves = hasHc ? hc.moves : null;
274
+ const hcDist = hasHc ? hc.distances : null;
275
+ const hcRow = hasHc ? hcCoord * hc_pdb_js_1.HC_PDB_MOVES.length : 0;
276
+ // Byte-expanded (tests/generators) vs nibble-packed (loaders) distances.
277
+ const edgeUnpacked = edge3Distances.length >= 31006080;
278
+ const examined0 = collect === undefined ? 0 : collect.examined;
279
+ const ckmvRow = lm * 20;
75
280
  for (let m = 0; m < 17; m++) {
76
- if (exports.ckmv3[lm][m]) {
77
- m = exports.skipAxis3[m];
281
+ if (ckmv3Flat[ckmvRow + m]) {
282
+ m = skipAxis3Fast[m];
78
283
  continue;
79
284
  }
80
- const nc = center3Tables.moveTable[ct * center3_js_1.CENTER3_MOVES.length + m];
285
+ const nc = c3Move[c3Row + m];
81
286
  if (nc < 0)
82
287
  continue;
83
- const prun1 = center3Tables.pruning[nc];
288
+ const prun1 = c3Prun[nc];
84
289
  if (prun1 >= maxl) {
290
+ const ex1 = (prun1 > maxl && m < 14) ? depth + prun1 : depth + 1 + prun1;
291
+ if (ex1 < this.minExcess)
292
+ this.minExcess = ex1;
85
293
  if (prun1 > maxl && m < 14)
86
- m = exports.skipAxis3[m];
294
+ m = skipAxis3Fast[m];
87
295
  continue;
88
296
  }
89
- const symIdx = (0, edge3_js_1.getMvSymFrom)(ep, ep, m, this.sym.sym2Raw, this.sym.sym2Mask, this.sym.raw2Sym, this.sym.mvrot, this.sym.mvroto);
90
- const prunx = this.getPrun(edge3Distances, symIdx >> 3);
297
+ let nhc = -1;
298
+ if (hasHc) {
299
+ nhc = hcMoves[hcRow + m];
300
+ if (nhc >= 0) {
301
+ const prunh = hcDist[nhc];
302
+ if (prunh >= maxl) {
303
+ const exH = (prunh > maxl && m < 14) ? depth + prunh : depth + 1 + prunh;
304
+ if (exH < this.minExcess)
305
+ this.minExcess = exH;
306
+ if (prunh > maxl && m < 14)
307
+ m = skipAxis3Fast[m];
308
+ continue;
309
+ }
310
+ }
311
+ }
312
+ if (c3PairDist && depth === 0) {
313
+ const c3Base = nc * 144;
314
+ const fSlot = F_SLOT_MAP[m];
315
+ const hSlot = H_SLOT_MAP[m];
316
+ let prunPair = 0;
317
+ for (let s = 0; s < 12; s++) {
318
+ const idx = c3Base + fSlot[s] * 12 + hSlot[ep[s]];
319
+ const d = (idx & 1) === 0 ? (c3PairDist[idx >> 1] & 0x0f) : (c3PairDist[idx >> 1] >> 4);
320
+ if (d >= maxl) {
321
+ prunPair = d;
322
+ break;
323
+ }
324
+ if (d > prunPair)
325
+ prunPair = d;
326
+ }
327
+ if (prunPair >= maxl) {
328
+ const exP = (prunPair > maxl && m < 14) ? depth + prunPair : depth + 1 + prunPair;
329
+ if (exP < this.minExcess)
330
+ this.minExcess = exP;
331
+ if (prunPair > maxl && m < 14)
332
+ m = skipAxis3Fast[m];
333
+ continue;
334
+ }
335
+ }
336
+ // Inlined getMvSymFrom(ep, ep, m, ...) (bit-identical; masks here are
337
+ // 12-bit, so one popcount lookup replaces bitCount's three).
338
+ let mrIdx = m << 3;
339
+ let movoBase = mrIdx * 12;
340
+ let raw = 0;
341
+ let bits = 0;
342
+ for (let i = 0; i < 4; i++) {
343
+ const val = symMvroto[movoBase + ep[symMvrot[movoBase + i]]];
344
+ raw = raw * (12 - i) + val - POPC12[bits & ((1 << val) - 1)];
345
+ bits |= 1 << val;
346
+ }
347
+ const symBase = raw2Sym[raw];
348
+ mrIdx |= symBase & 7;
349
+ let symOut = symBase >> 3;
350
+ bits = sym2Mask[symOut];
351
+ movoBase = mrIdx * 12;
352
+ for (let i = 4; i < 10; i++) {
353
+ const val = symMvroto[movoBase + ep[symMvrot[movoBase + i]]];
354
+ symOut = symOut * (12 - i) + val - POPC12[bits & ((1 << val) - 1)];
355
+ bits |= 1 << val;
356
+ }
357
+ const symIdx = ((symOut << 3) | (mrIdx & 7)) >>> 0;
358
+ // Format-aware read: loaders keep the nibble-packed table (15.5MB);
359
+ // tests and generators may pass byte-expanded buffers instead.
360
+ const eIdx = symIdx >> 3;
361
+ const prunx = edgeUnpacked
362
+ ? edge3Distances[eIdx]
363
+ : (edge3Distances[eIdx >> 1] >> ((eIdx & 1) * 4)) & 0x0f;
91
364
  if (prunx >= maxl) {
365
+ const exE = (prunx > maxl && m < 14) ? depth + prunx : depth + 1 + prunx;
366
+ if (exE < this.minExcess)
367
+ this.minExcess = exE;
92
368
  if (prunx > maxl && m < 14)
93
- m = exports.skipAxis3[m];
369
+ m = skipAxis3Fast[m];
94
370
  continue;
95
371
  }
96
- if (await this.search(ep, nc, prunx, maxl - 1, m, depth + 1, center3Tables, edge3Distances)) {
97
- this.moveLog[depth] = m;
372
+ this.moveLog[depth] = m;
373
+ if (this.search(ep, nc, prunx, maxl - 1, m, depth + 1, center3Tables, edge3Distances, maxNodes, collect, hc, nhc, c3PairDist)) {
98
374
  return true;
99
375
  }
100
376
  }
377
+ // Cache only subtrees with NO raw goal reached in them (the examined
378
+ // count is the goal counter — rejections included, since the claim is
379
+ // raw). Goal-free subtrees are safe to reuse across calls, candidates
380
+ // and predicates.
381
+ if (ttActive &&
382
+ this.nodeCount <= maxNodes &&
383
+ (collect?.examined ?? 0) === examined0) {
384
+ this.tt[slot] = keyHigh;
385
+ this.tt[slot + 1] = keyLow;
386
+ this.tt[slot + 2] = ((this.callId << 11) | (lm << 6) | maxl) >>> 0;
387
+ }
101
388
  return false;
102
389
  }
103
390
  /**
104
- * Search for a phase-3 solution with exact target length.
391
+ * Record an IDA* bound prune (inlined at the four gate sites): the child at
392
+ * `depth + 1` needs at least `childBound` more moves, so no solution exists
393
+ * through this branch below root threshold `depth + 1 + childBound`. When a
394
+ * bound-based axis skip also fires, the skipped siblings are same-face turns
395
+ * (verified: every non-empty skipAxis3 run under m < 14 is a same-base
396
+ * triple), hence at most 1 legal move apart — their bound is at least
397
+ * `childBound - 1` by the triangle inequality on the exact PDBs, so the
398
+ * recorded excess `depth + childBound` stays a true lower bound.
399
+ */
400
+ /**
401
+ * Search for a phase-3 solution with exact target length (first found).
105
402
  */
106
- async solveLength(edge, ct3, edist, length3, center3Tables, edge3Distances) {
403
+ solveLength(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, hc, hcCoord = -1, c3PairDist) {
404
+ this.setTableContext(center3Tables, edge3Distances);
107
405
  this.tempep[0].set(edge);
108
- if (await this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances)) {
406
+ this.nodeCount = 0;
407
+ this.callId = this.nextCallId();
408
+ this.ttCoverage = Number.MAX_SAFE_INTEGER;
409
+ if (this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, undefined, hc, hcCoord, c3PairDist)) {
109
410
  return this.moveLog.slice(0, length3).map((m) => exports.SEARCH_MOVES[m]);
110
411
  }
111
412
  return null;
112
413
  }
414
+ /**
415
+ * Search for a phase-3 solution with exact target length, accepting the
416
+ * first goal sequence that passes `validate` (up to `limit` examined,
417
+ * sharing `maxNodes`). With a validator that enforces the downstream
418
+ * requirements (reduction + standard orientation + 3x3 conversion), hard
419
+ * states whose first-found solutions are unusable get more draws from
420
+ * the same budget instead of an immediate miss.
421
+ */
422
+ solveLengthValid(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, limit = 16, validate, hc, hcCoord = -1, c3PairDist) {
423
+ return this.solveLengthProven(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes, limit, validate, hc, hcCoord, c3PairDist).moves;
424
+ }
425
+ /**
426
+ * Exact-length validated search that also reports what the call proved.
427
+ * `provenThrough` is the greatest length with provably no (center, edge)
428
+ * solution: set only when the tree was exhaustively explored with no goal
429
+ * reached, no node-cap abort, no TT hit, and complete excess info — the
430
+ * caller may then skip every shorter length for this candidate. Otherwise
431
+ * it is `length3 - 1` at most (only the attempted length is settled).
432
+ */
433
+ solveLengthProven(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, limit = 16, validate, hc, hcCoord = -1, c3PairDist) {
434
+ this.setTableContext(center3Tables, edge3Distances);
435
+ if (c3PairDist) {
436
+ const c3Base = ct3 * 144;
437
+ let maxPairDist = 0;
438
+ for (let s = 0; s < 12; s++) {
439
+ const idx = c3Base + s * 12 + edge[s];
440
+ const d = (idx & 1) === 0 ? (c3PairDist[idx >> 1] & 0x0f) : (c3PairDist[idx >> 1] >> 4);
441
+ if (d > maxPairDist)
442
+ maxPairDist = d;
443
+ }
444
+ if (maxPairDist > length3) {
445
+ return { moves: null, provenThrough: maxPairDist - 1, validatedThrough: maxPairDist - 1, nodesUsed: 0 };
446
+ }
447
+ }
448
+ this.tempep[0].set(edge);
449
+ this.nodeCount = 0;
450
+ this.capped = false;
451
+ this.limitHit = false;
452
+ this.sawGoal = false;
453
+ this.ttTouched = false;
454
+ this.minExcess = Number.MAX_SAFE_INTEGER;
455
+ this.ttCoverage = Number.MAX_SAFE_INTEGER;
456
+ this.callId = this.nextCallId();
457
+ let accepted = null;
458
+ const collect = {
459
+ limit,
460
+ examined: 0,
461
+ validate: (moves) => {
462
+ if (validate(moves)) {
463
+ accepted = moves;
464
+ return true;
465
+ }
466
+ // A rejected goal is not a validated solution. Enumeration callers
467
+ // may collect it themselves, but cannot use it as a search proof.
468
+ return false;
469
+ }
470
+ };
471
+ this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, collect, hc, hcCoord, c3PairDist);
472
+ if (accepted)
473
+ return { moves: accepted, provenThrough: length3, validatedThrough: length3, nodesUsed: this.nodeCount };
474
+ if (typeof process !== "undefined" && process.env?.CUBE4_PROOF_STATS === "1") {
475
+ exports.proofStats.total++;
476
+ if (this.capped)
477
+ exports.proofStats.capped++;
478
+ if (this.limitHit)
479
+ exports.proofStats.limitHit++;
480
+ if (this.sawGoal)
481
+ exports.proofStats.sawGoal++;
482
+ if (this.ttTouched)
483
+ exports.proofStats.ttTouched++;
484
+ }
485
+ // `provenThrough` keeps its conservative raw-goal contract: it may only
486
+ // claim a depth when NO raw goal was reached at all (a rejected goal IS
487
+ // a raw goal), no TT hit intervened, and the bound prunes cover past the
488
+ // attempted length.
489
+ let provenThrough = -1;
490
+ if (!this.capped &&
491
+ !this.limitHit &&
492
+ !this.sawGoal &&
493
+ !this.ttTouched &&
494
+ this.minExcess > length3 &&
495
+ this.minExcess !== Number.MAX_SAFE_INTEGER) {
496
+ if (typeof process !== "undefined" && process.env?.CUBE4_PROOF_STATS === "1") {
497
+ exports.proofStats.clean++;
498
+ exports.proofStats.jumpSum += this.minExcess - 1 - length3;
499
+ }
500
+ provenThrough = this.minExcess - 1;
501
+ }
502
+ // `validatedThrough` is scoped to THIS call's validator predicate (all
503
+ // calls sharing one solver instance use one predicate): rejected raw
504
+ // goals are not goals for it, and TT hits are proof-compatible (entries
505
+ // mean "no goal under the call's predicate at any length <= stored").
506
+ // Every walked branch leaves a claim: bound prunes record their
507
+ // threshold (noteExcess), fringe rejections record `length3 + 1` below,
508
+ // and TT-hit subtrees record `ttCoverage`.
509
+ let validatedThrough = -1;
510
+ if (!this.capped && !this.limitHit) {
511
+ let bound = length3;
512
+ if (this.minExcess !== Number.MAX_SAFE_INTEGER && this.minExcess - 1 > bound) {
513
+ bound = this.minExcess - 1;
514
+ }
515
+ if (this.ttCoverage < bound)
516
+ bound = this.ttCoverage;
517
+ validatedThrough = bound;
518
+ }
519
+ return { moves: null, provenThrough, validatedThrough, nodesUsed: this.nodeCount };
520
+ }
113
521
  }
114
522
  exports.FastPhase3Solver = FastPhase3Solver;
115
- const defaultSolver = new FastPhase3Solver();
523
+ /** Temporary instrumentation for proof-statistics (CUBE4_PROOF_STATS=1). */
524
+ exports.proofStats = {
525
+ total: 0,
526
+ capped: 0,
527
+ limitHit: 0,
528
+ sawGoal: 0,
529
+ ttTouched: 0,
530
+ clean: 0,
531
+ jumpSum: 0
532
+ };
533
+ function resetProofStats() {
534
+ exports.proofStats.total = 0;
535
+ exports.proofStats.capped = 0;
536
+ exports.proofStats.limitHit = 0;
537
+ exports.proofStats.sawGoal = 0;
538
+ exports.proofStats.ttTouched = 0;
539
+ exports.proofStats.clean = 0;
540
+ exports.proofStats.jumpSum = 0;
541
+ }
116
542
  /**
117
543
  * Solve Phase 3 with iterative deepening on a single Phase3Start.
118
544
  */
119
- async function solvePhase3(start, center3Tables, edge3Tables, maxLength = 20) {
545
+ async function solvePhase3(start, center3Tables, edge3Tables, maxLength = 20, maxNodes = 80_000) {
546
+ const solver = new FastPhase3Solver();
120
547
  const sym = (0, edge3_js_1.getSymTables)();
121
548
  const symIdx = (0, edge3_js_1.getMvSymFrom)(start.edge, start.edge, 20, sym.sym2Raw, sym.sym2Mask, sym.raw2Sym, sym.mvrot, sym.mvroto);
122
549
  const edist = (edge3Tables.distances[(symIdx >> 3) >> 1] >> (((symIdx >> 3) & 1) * 4)) & 0x0f;
@@ -125,7 +552,7 @@ async function solvePhase3(start, center3Tables, edge3Tables, maxLength = 20) {
125
552
  return null;
126
553
  const minLength = Math.max(cdist, edist);
127
554
  for (let l = minLength; l <= maxLength; l++) {
128
- const moves = await defaultSolver.solveLength(start.edge, start.ct3, edist, l, center3Tables, edge3Tables.distances);
555
+ const moves = solver.solveLength(start.edge, start.ct3, edist, l, center3Tables, edge3Tables.distances, maxNodes);
129
556
  if (moves) {
130
557
  return {
131
558
  moves,