@cubingcenter/scrambler 0.7.0 → 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 (126) hide show
  1. package/README.md +15 -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/cube3/solver/browser-loader.d.ts.map +1 -1
  10. package/dist/puzzles/cube3/solver/browser-loader.js +42 -20
  11. package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
  12. package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
  13. package/dist/puzzles/cube3/solver/phase1.d.ts +19 -0
  14. package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
  15. package/dist/puzzles/cube3/solver/phase1.js +29 -6
  16. package/dist/puzzles/cube3/solver/phase2.d.ts +28 -0
  17. package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
  18. package/dist/puzzles/cube3/solver/phase2.js +56 -15
  19. package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
  20. package/dist/puzzles/cube3/solver/tables.js +14 -12
  21. package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
  22. package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
  23. package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
  24. package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
  25. package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
  26. package/dist/puzzles/cube4/browser-worker.js +31 -0
  27. package/dist/puzzles/cube4/index.d.ts +7 -0
  28. package/dist/puzzles/cube4/index.d.ts.map +1 -1
  29. package/dist/puzzles/cube4/index.js +4 -0
  30. package/dist/puzzles/cube4/moves.d.ts.map +1 -1
  31. package/dist/puzzles/cube4/moves.js +122 -30
  32. package/dist/puzzles/cube4/reduction/browser-loader.d.ts +26 -1
  33. package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
  34. package/dist/puzzles/cube4/reduction/browser-loader.js +212 -48
  35. package/dist/puzzles/cube4/reduction/center2.d.ts +24 -0
  36. package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
  37. package/dist/puzzles/cube4/reduction/center2.js +99 -1
  38. package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
  39. package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
  40. package/dist/puzzles/cube4/reduction/finish3.js +393 -0
  41. package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
  42. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +5 -3
  43. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -1
  44. package/dist/puzzles/cube4/reduction/hc-pdb.js +30 -12
  45. package/dist/puzzles/cube4/reduction/node-loader.d.ts +10 -1
  46. package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
  47. package/dist/puzzles/cube4/reduction/node-loader.js +170 -36
  48. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
  49. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
  50. package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
  51. package/dist/puzzles/cube4/reduction/phase3-search.d.ts +33 -30
  52. package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
  53. package/dist/puzzles/cube4/reduction/phase3-search.js +211 -104
  54. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
  55. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
  56. package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
  57. package/dist/puzzles/cube4/reduction/solve.d.ts +72 -13
  58. package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
  59. package/dist/puzzles/cube4/reduction/solve.js +374 -92
  60. package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
  61. package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
  62. package/dist/puzzles/cube4/scramble-cache.js +150 -0
  63. package/dist/puzzles/cube4/scrambler.d.ts +112 -18
  64. package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
  65. package/dist/puzzles/cube4/scrambler.js +271 -83
  66. package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
  67. package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
  68. package/dist/puzzles/cube4/worker-pool.js +405 -0
  69. package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
  70. package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
  71. package/dist/puzzles/cube4/worker-protocol.js +58 -0
  72. package/dist/puzzles/cube4/worker.d.ts +26 -0
  73. package/dist/puzzles/cube4/worker.d.ts.map +1 -0
  74. package/dist/puzzles/cube4/worker.js +49 -0
  75. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
  76. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
  77. package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
  78. package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
  79. package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
  80. package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
  81. package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
  82. package/dist/puzzles/shared/compressed-fetch.js +226 -31
  83. package/dist/puzzles/shared/load-once.d.ts +16 -0
  84. package/dist/puzzles/shared/load-once.d.ts.map +1 -0
  85. package/dist/puzzles/shared/load-once.js +11 -0
  86. package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
  87. package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
  88. package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
  89. package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
  90. package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
  91. package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
  92. package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
  93. package/dist-cjs/core/api.js +10 -2
  94. package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
  95. package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
  96. package/dist-cjs/puzzles/cube3/solver/browser-loader.js +42 -20
  97. package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
  98. package/dist-cjs/puzzles/cube3/solver/phase1.js +29 -6
  99. package/dist-cjs/puzzles/cube3/solver/phase2.js +56 -15
  100. package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
  101. package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
  102. package/dist-cjs/puzzles/cube4/index.js +21 -1
  103. package/dist-cjs/puzzles/cube4/moves.js +122 -30
  104. package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +212 -48
  105. package/dist-cjs/puzzles/cube4/reduction/center2.js +102 -2
  106. package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
  107. package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
  108. package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +31 -12
  109. package/dist-cjs/puzzles/cube4/reduction/node-loader.js +171 -36
  110. package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
  111. package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +211 -104
  112. package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
  113. package/dist-cjs/puzzles/cube4/reduction/solve.js +374 -88
  114. package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
  115. package/dist-cjs/puzzles/cube4/scrambler.js +271 -80
  116. package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
  117. package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
  118. package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
  119. package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
  120. package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
  121. package/dist-cjs/puzzles/shared/load-once.js +14 -0
  122. package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
  123. package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
  124. package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
  125. package/docs/development.md +19 -7
  126. package/package.json +4 -3
@@ -39,6 +39,17 @@ for (let i = 0; i < 20; i++) {
39
39
  }
40
40
  }
41
41
  }
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);
42
53
  const F_SLOT_MAP = [
43
54
  [4, 5, 2, 3, 1, 0, 6, 7, 8, 9, 10, 11],
44
55
  [1, 0, 2, 3, 5, 4, 6, 7, 8, 9, 10, 11],
@@ -81,52 +92,59 @@ class FastPhase3Solver {
81
92
  tempep;
82
93
  moveLog;
83
94
  sym;
84
- /** Transposition Table size: 2^21 = 2,097,152 entries (24 MB in RAM).
85
- * NOTE: doubling to 4M was tried and reverted (-0.07% nodes on the
86
- * corpus — the 2M working set already captures the hit locality). */
87
- static TT_SIZE = 1 << 21;
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;
88
100
  static TT_MASK = FastPhase3Solver.TT_SIZE - 1;
89
- static tt = new Uint32Array(FastPhase3Solver.TT_SIZE * 3);
90
- static epoch = 1;
91
- /**
92
- * Per-slot exact-length-call tag (8 MB transient). TT pruning stays
93
- * epoch-scoped (unchanged traversal), but threshold proofs only trust
94
- * same-call hits: a hit from another call may have validated under a
95
- * different candidate, so it still prunes yet marks the proof dirty.
96
- */
97
- static ttGen = new Uint32Array(FastPhase3Solver.TT_SIZE);
98
- static callSeq = 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;
99
106
  callId = 0;
100
- /** Reset the per-attempt cumulative node counter. */
107
+ contextCenter3;
108
+ contextEdge3;
109
+ /** Reset this solver's cumulative node counter (legacy method name). */
101
110
  resetEpochNodes() {
102
111
  this.epochNodes = 0;
103
112
  }
104
- static nextEpoch() {
105
- FastPhase3Solver.epoch = (FastPhase3Solver.epoch + 1) & 0xffff;
106
- if (FastPhase3Solver.epoch === 0) {
107
- FastPhase3Solver.tt.fill(0);
108
- FastPhase3Solver.ttGen.fill(0);
109
- FastPhase3Solver.callSeq = 1;
110
- FastPhase3Solver.epoch = 1;
111
- }
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;
112
123
  }
113
- /** Fresh per-call id for TT-generation tagging (epoch wrap resets it). */
114
- static nextCallId() {
115
- const id = FastPhase3Solver.callSeq++;
116
- if (FastPhase3Solver.callSeq > 0xfffffffe) {
117
- FastPhase3Solver.ttGen.fill(0);
118
- FastPhase3Solver.callSeq = 1;
119
- return 1;
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;
120
130
  }
121
131
  return id;
122
132
  }
123
133
  constructor() {
124
- this.tempep = Array.from({ length: 30 }, () => new Uint8Array(12));
125
- 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);
126
138
  this.sym = (0, edge3_js_1.getSymTables)();
127
139
  }
140
+ ensureDepth(depth) {
141
+ while (this.tempep.length <= depth) {
142
+ this.tempep.push(new Uint8Array(12));
143
+ this.moveLog.push(0);
144
+ }
145
+ }
128
146
  nodeCount = 0;
129
- /** Cumulative nodes across calls (reset per attempt via resetEpochNodes). */
147
+ /** Cumulative nodes across calls (reset per solve generation). */
130
148
  epochNodes = 0;
131
149
  /**
132
150
  * Proof bookkeeping for IDA* threshold propagation (reset per exact-length
@@ -138,9 +156,8 @@ class FastPhase3Solver {
138
156
  * - `sawGoal`: a goal was reached (even if the validator rejected it —
139
157
  * a nearby length may still validate, so only step L+1).
140
158
  * - `ttTouched`: a transposition-table hit from ANOTHER exact-length call
141
- * skipped a subtree (same-call hits replay this call's own exhausted
142
- * subtree and stay clean; other calls may validate under a different
143
- * candidate, so their stored proof does not transfer).
159
+ * skipped a subtree, and the hit was not certified goal-free across
160
+ * candidate validators.
144
161
  * Bound-based axis skips need no flag: the skipped siblings are
145
162
  * same-face turns, so noteExcess records their triangle-inequality bound.
146
163
  */
@@ -149,11 +166,13 @@ class FastPhase3Solver {
149
166
  sawGoal = false;
150
167
  ttTouched = false;
151
168
  minExcess = 0;
152
- getPrun(distances, idx) {
153
- return distances.length >= 31006080
154
- ? distances[idx]
155
- : (distances[idx >> 1] >> ((idx & 1) * 4)) & 0x0f;
156
- }
169
+ /**
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`.
174
+ */
175
+ ttCoverage = Number.MAX_SAFE_INTEGER;
157
176
  /**
158
177
  * Recursive IDA* worker on table transitions with Transposition Table pruning.
159
178
  */
@@ -164,18 +183,38 @@ class FastPhase3Solver {
164
183
  this.capped = true;
165
184
  return false;
166
185
  }
167
- if (maxl === 0) {
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;
168
195
  if (!collect)
169
196
  return true;
170
- this.sawGoal = true;
171
197
  collect.examined++;
172
198
  const moves = this.moveLog.slice(0, depth).map((m) => exports.SEARCH_MOVES[m]);
173
199
  if (collect.validate(moves))
174
200
  return true;
175
- if (collect.examined >= collect.limit)
201
+ if (collect.examined >= collect.limit) {
176
202
  this.limitHit = true;
177
- return collect.examined >= collect.limit;
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;
178
215
  }
216
+ if (depth >= this.tempep.length)
217
+ this.ensureDepth(depth);
179
218
  const sym = this.sym;
180
219
  const symMvrot = sym.mvrot;
181
220
  const symMvroto = sym.mvroto;
@@ -189,25 +228,37 @@ class FastPhase3Solver {
189
228
  else {
190
229
  ep.set(eplast);
191
230
  }
192
- // TT lookup (for depths >= 2 where TT hit prunes multi-move subtrees)
193
- const keyHigh = ((ct << 16) | (ep[8] | (ep[9] << 4) | (ep[10] << 8) | (ep[11] << 12))) >>> 0;
194
- const keyLow = (ep[0] | (ep[1] << 4) | (ep[2] << 8) | (ep[3] << 12) |
195
- (ep[4] << 16) | (ep[5] << 20) | (ep[6] << 24) | (ep[7] << 28)) >>> 0;
196
- const slot = (((keyLow ^ Math.imul(keyHigh, 0x9e3779b9)) & FastPhase3Solver.TT_MASK) * 3) >>> 0;
197
- if (maxl >= 2) {
198
- const storedHigh = FastPhase3Solver.tt[slot];
199
- const storedLow = FastPhase3Solver.tt[slot + 1];
200
- const storedInfo = FastPhase3Solver.tt[slot + 2];
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];
201
253
  if (storedHigh === keyHigh && storedLow === keyLow) {
202
- const storedEpoch = storedInfo >>> 16;
203
- const storedLm = (storedInfo >> 8) & 0xff;
204
- const storedDepth = storedInfo & 0xff;
205
- if (storedEpoch === FastPhase3Solver.epoch && storedDepth >= maxl && storedLm === lm) {
206
- // Same-call hits replay this call's own exhausted subtree (proof
207
- // stays clean); cross-call hits still prune but dirty the proof.
208
- // NOTE: ignoring cross-call entries was tried and reverted — they
209
- // carry most of the pruning power (3.82s vs 2.39s on the corpus).
210
- if (FastPhase3Solver.ttGen[(slot / 3) | 0] !== this.callId)
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)
211
262
  this.ttTouched = true;
212
263
  return false;
213
264
  }
@@ -219,15 +270,16 @@ class FastPhase3Solver {
219
270
  const sym2Raw = sym.sym2Raw;
220
271
  const sym2Mask = sym.sym2Mask;
221
272
  const raw2Sym = sym.raw2Sym;
222
- const hasHc = hc !== undefined && hcCoord >= 0 && hc.moves.length > 0;
223
273
  const hcMoves = hasHc ? hc.moves : null;
224
274
  const hcDist = hasHc ? hc.distances : null;
225
275
  const hcRow = hasHc ? hcCoord * hc_pdb_js_1.HC_PDB_MOVES.length : 0;
226
276
  // Byte-expanded (tests/generators) vs nibble-packed (loaders) distances.
227
277
  const edgeUnpacked = edge3Distances.length >= 31006080;
278
+ const examined0 = collect === undefined ? 0 : collect.examined;
279
+ const ckmvRow = lm * 20;
228
280
  for (let m = 0; m < 17; m++) {
229
- if (exports.ckmv3[lm][m]) {
230
- m = exports.skipAxis3[m];
281
+ if (ckmv3Flat[ckmvRow + m]) {
282
+ m = skipAxis3Fast[m];
231
283
  continue;
232
284
  }
233
285
  const nc = c3Move[c3Row + m];
@@ -235,9 +287,11 @@ class FastPhase3Solver {
235
287
  continue;
236
288
  const prun1 = c3Prun[nc];
237
289
  if (prun1 >= maxl) {
238
- this.noteExcess(depth, prun1, prun1 > maxl && m < 14);
290
+ const ex1 = (prun1 > maxl && m < 14) ? depth + prun1 : depth + 1 + prun1;
291
+ if (ex1 < this.minExcess)
292
+ this.minExcess = ex1;
239
293
  if (prun1 > maxl && m < 14)
240
- m = exports.skipAxis3[m];
294
+ m = skipAxis3Fast[m];
241
295
  continue;
242
296
  }
243
297
  let nhc = -1;
@@ -246,9 +300,11 @@ class FastPhase3Solver {
246
300
  if (nhc >= 0) {
247
301
  const prunh = hcDist[nhc];
248
302
  if (prunh >= maxl) {
249
- this.noteExcess(depth, prunh, prunh > maxl && m < 14);
303
+ const exH = (prunh > maxl && m < 14) ? depth + prunh : depth + 1 + prunh;
304
+ if (exH < this.minExcess)
305
+ this.minExcess = exH;
250
306
  if (prunh > maxl && m < 14)
251
- m = exports.skipAxis3[m];
307
+ m = skipAxis3Fast[m];
252
308
  continue;
253
309
  }
254
310
  }
@@ -269,13 +325,36 @@ class FastPhase3Solver {
269
325
  prunPair = d;
270
326
  }
271
327
  if (prunPair >= maxl) {
272
- this.noteExcess(depth, prunPair, prunPair > maxl && m < 14);
328
+ const exP = (prunPair > maxl && m < 14) ? depth + prunPair : depth + 1 + prunPair;
329
+ if (exP < this.minExcess)
330
+ this.minExcess = exP;
273
331
  if (prunPair > maxl && m < 14)
274
- m = exports.skipAxis3[m];
332
+ m = skipAxis3Fast[m];
275
333
  continue;
276
334
  }
277
335
  }
278
- const symIdx = (0, edge3_js_1.getMvSymFrom)(ep, ep, m, sym2Raw, sym2Mask, raw2Sym, symMvrot, symMvroto);
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;
279
358
  // Format-aware read: loaders keep the nibble-packed table (15.5MB);
280
359
  // tests and generators may pass byte-expanded buffers instead.
281
360
  const eIdx = symIdx >> 3;
@@ -283,9 +362,11 @@ class FastPhase3Solver {
283
362
  ? edge3Distances[eIdx]
284
363
  : (edge3Distances[eIdx >> 1] >> ((eIdx & 1) * 4)) & 0x0f;
285
364
  if (prunx >= maxl) {
286
- this.noteExcess(depth, prunx, prunx > maxl && m < 14);
365
+ const exE = (prunx > maxl && m < 14) ? depth + prunx : depth + 1 + prunx;
366
+ if (exE < this.minExcess)
367
+ this.minExcess = exE;
287
368
  if (prunx > maxl && m < 14)
288
- m = exports.skipAxis3[m];
369
+ m = skipAxis3Fast[m];
289
370
  continue;
290
371
  }
291
372
  this.moveLog[depth] = m;
@@ -293,36 +374,38 @@ class FastPhase3Solver {
293
374
  return true;
294
375
  }
295
376
  }
296
- // Record failure in TT if node didn't abort due to node cap
297
- if (this.nodeCount <= maxNodes && maxl >= 2) {
298
- FastPhase3Solver.tt[slot] = keyHigh;
299
- FastPhase3Solver.tt[slot + 1] = keyLow;
300
- FastPhase3Solver.tt[slot + 2] = (FastPhase3Solver.epoch << 16) | (lm << 8) | maxl;
301
- FastPhase3Solver.ttGen[(slot / 3) | 0] = this.callId;
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;
302
387
  }
303
388
  return false;
304
389
  }
305
390
  /**
306
- * Record an IDA* bound prune: the child at `depth + 1` needs at least
307
- * `childBound` more moves, so no solution exists through this branch
308
- * below root threshold `depth + 1 + childBound`. When a bound-based axis
309
- * skip also fires, the skipped siblings are same-face turns (verified:
310
- * every non-empty skipAxis3 run under m < 14 is a same-base triple),
311
- * hence at most 1 legal move apart — their bound is at least
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
312
397
  * `childBound - 1` by the triangle inequality on the exact PDBs, so the
313
398
  * recorded excess `depth + childBound` stays a true lower bound.
314
399
  */
315
- noteExcess(depth, childBound, skippedSiblings) {
316
- const excess = skippedSiblings ? depth + childBound : depth + 1 + childBound;
317
- if (excess < this.minExcess)
318
- this.minExcess = excess;
319
- }
320
400
  /**
321
401
  * Search for a phase-3 solution with exact target length (first found).
322
402
  */
323
403
  solveLength(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, hc, hcCoord = -1, c3PairDist) {
404
+ this.setTableContext(center3Tables, edge3Distances);
324
405
  this.tempep[0].set(edge);
325
406
  this.nodeCount = 0;
407
+ this.callId = this.nextCallId();
408
+ this.ttCoverage = Number.MAX_SAFE_INTEGER;
326
409
  if (this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, undefined, hc, hcCoord, c3PairDist)) {
327
410
  return this.moveLog.slice(0, length3).map((m) => exports.SEARCH_MOVES[m]);
328
411
  }
@@ -348,6 +431,7 @@ class FastPhase3Solver {
348
431
  * it is `length3 - 1` at most (only the attempted length is settled).
349
432
  */
350
433
  solveLengthProven(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, limit = 16, validate, hc, hcCoord = -1, c3PairDist) {
434
+ this.setTableContext(center3Tables, edge3Distances);
351
435
  if (c3PairDist) {
352
436
  const c3Base = ct3 * 144;
353
437
  let maxPairDist = 0;
@@ -358,7 +442,7 @@ class FastPhase3Solver {
358
442
  maxPairDist = d;
359
443
  }
360
444
  if (maxPairDist > length3) {
361
- return { moves: null, provenThrough: maxPairDist - 1, nodesUsed: 0 };
445
+ return { moves: null, provenThrough: maxPairDist - 1, validatedThrough: maxPairDist - 1, nodesUsed: 0 };
362
446
  }
363
447
  }
364
448
  this.tempep[0].set(edge);
@@ -368,7 +452,8 @@ class FastPhase3Solver {
368
452
  this.sawGoal = false;
369
453
  this.ttTouched = false;
370
454
  this.minExcess = Number.MAX_SAFE_INTEGER;
371
- this.callId = FastPhase3Solver.nextCallId();
455
+ this.ttCoverage = Number.MAX_SAFE_INTEGER;
456
+ this.callId = this.nextCallId();
372
457
  let accepted = null;
373
458
  const collect = {
374
459
  limit,
@@ -378,15 +463,15 @@ class FastPhase3Solver {
378
463
  accepted = moves;
379
464
  return true;
380
465
  }
381
- if (!accepted)
382
- accepted = moves;
466
+ // A rejected goal is not a validated solution. Enumeration callers
467
+ // may collect it themselves, but cannot use it as a search proof.
383
468
  return false;
384
469
  }
385
470
  };
386
471
  this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, collect, hc, hcCoord, c3PairDist);
387
472
  if (accepted)
388
- return { moves: accepted, provenThrough: length3, nodesUsed: this.nodeCount };
389
- if (process.env.CUBE4_PROOF_STATS === "1") {
473
+ return { moves: accepted, provenThrough: length3, validatedThrough: length3, nodesUsed: this.nodeCount };
474
+ if (typeof process !== "undefined" && process.env?.CUBE4_PROOF_STATS === "1") {
390
475
  exports.proofStats.total++;
391
476
  if (this.capped)
392
477
  exports.proofStats.capped++;
@@ -397,19 +482,41 @@ class FastPhase3Solver {
397
482
  if (this.ttTouched)
398
483
  exports.proofStats.ttTouched++;
399
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;
400
490
  if (!this.capped &&
401
491
  !this.limitHit &&
402
492
  !this.sawGoal &&
403
493
  !this.ttTouched &&
404
494
  this.minExcess > length3 &&
405
495
  this.minExcess !== Number.MAX_SAFE_INTEGER) {
406
- if (process.env.CUBE4_PROOF_STATS === "1") {
496
+ if (typeof process !== "undefined" && process.env?.CUBE4_PROOF_STATS === "1") {
407
497
  exports.proofStats.clean++;
408
498
  exports.proofStats.jumpSum += this.minExcess - 1 - length3;
409
499
  }
410
- return { moves: null, provenThrough: this.minExcess - 1, nodesUsed: this.nodeCount };
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;
411
518
  }
412
- return { moves: null, provenThrough: length3, nodesUsed: this.nodeCount };
519
+ return { moves: null, provenThrough, validatedThrough, nodesUsed: this.nodeCount };
413
520
  }
414
521
  }
415
522
  exports.FastPhase3Solver = FastPhase3Solver;
@@ -432,11 +539,11 @@ function resetProofStats() {
432
539
  exports.proofStats.clean = 0;
433
540
  exports.proofStats.jumpSum = 0;
434
541
  }
435
- const defaultSolver = new FastPhase3Solver();
436
542
  /**
437
543
  * Solve Phase 3 with iterative deepening on a single Phase3Start.
438
544
  */
439
- 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();
440
547
  const sym = (0, edge3_js_1.getSymTables)();
441
548
  const symIdx = (0, edge3_js_1.getMvSymFrom)(start.edge, start.edge, 20, sym.sym2Raw, sym.sym2Mask, sym.raw2Sym, sym.mvrot, sym.mvroto);
442
549
  const edist = (edge3Tables.distances[(symIdx >> 3) >> 1] >> (((symIdx >> 3) & 1) * 4)) & 0x0f;
@@ -445,7 +552,7 @@ async function solvePhase3(start, center3Tables, edge3Tables, maxLength = 20) {
445
552
  return null;
446
553
  const minLength = Math.max(cdist, edist);
447
554
  for (let l = minLength; l <= maxLength; l++) {
448
- 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);
449
556
  if (moves) {
450
557
  return {
451
558
  moves,