@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
@@ -34,6 +34,17 @@ for (let i = 0; i < 20; i++) {
34
34
  }
35
35
  }
36
36
  }
37
+ /** Popcount of 12-bit masks (the inlined sym-coordinate rank). */
38
+ const POPC12 = new Uint8Array(4096);
39
+ for (let i = 1; i < 4096; i++)
40
+ POPC12[i] = POPC12[i >> 1] + (i & 1);
41
+ /** Flat row-major ckmv3 (21x20) + typed skipAxis3 for the hot recursion. */
42
+ const ckmv3Flat = new Uint8Array(21 * 20);
43
+ for (let i = 0; i < 21; i++) {
44
+ for (let j = 0; j < 20; j++)
45
+ ckmv3Flat[i * 20 + j] = ckmv3[i][j] ? 1 : 0;
46
+ }
47
+ const skipAxis3Fast = Int32Array.from(skipAxis3);
37
48
  const F_SLOT_MAP = [
38
49
  [4, 5, 2, 3, 1, 0, 6, 7, 8, 9, 10, 11],
39
50
  [1, 0, 2, 3, 5, 4, 6, 7, 8, 9, 10, 11],
@@ -76,52 +87,59 @@ export class FastPhase3Solver {
76
87
  tempep;
77
88
  moveLog;
78
89
  sym;
79
- /** Transposition Table size: 2^21 = 2,097,152 entries (24 MB in RAM).
80
- * NOTE: doubling to 4M was tried and reverted (-0.07% nodes on the
81
- * corpus — the 2M working set already captures the hit locality). */
82
- static TT_SIZE = 1 << 21;
90
+ /** Transposition Table size: 2^20 = 1,048,576 entries (12 MiB per solve).
91
+ * The previous 2M table used 24 MiB per process, plus a separate 8 MiB
92
+ * generation array. The compact 1M table leaves room for concurrent
93
+ * solvers under the 150 MiB reduction-table budget. */
94
+ static TT_SIZE = 1 << 20;
83
95
  static TT_MASK = FastPhase3Solver.TT_SIZE - 1;
84
- static tt = new Uint32Array(FastPhase3Solver.TT_SIZE * 3);
85
- static epoch = 1;
86
- /**
87
- * Per-slot exact-length-call tag (8 MB transient). TT pruning stays
88
- * epoch-scoped (unchanged traversal), but threshold proofs only trust
89
- * same-call hits: a hit from another call may have validated under a
90
- * different candidate, so it still prunes yet marks the proof dirty.
91
- */
92
- static ttGen = new Uint32Array(FastPhase3Solver.TT_SIZE);
93
- static callSeq = 1;
96
+ tt = new Uint32Array(FastPhase3Solver.TT_SIZE * 3);
97
+ // TT info packs (call id:20, previous move:5, remaining depth:6), avoiding
98
+ // a separate 8 MiB generation array while retaining exact proof tracking.
99
+ static CALL_ID_LIMIT = 1 << 20;
100
+ callSeq = 1;
94
101
  callId = 0;
95
- /** Reset the per-attempt cumulative node counter. */
102
+ contextCenter3;
103
+ contextEdge3;
104
+ /** Reset this solver's cumulative node counter (legacy method name). */
96
105
  resetEpochNodes() {
97
106
  this.epochNodes = 0;
98
107
  }
99
- static nextEpoch() {
100
- FastPhase3Solver.epoch = (FastPhase3Solver.epoch + 1) & 0xffff;
101
- if (FastPhase3Solver.epoch === 0) {
102
- FastPhase3Solver.tt.fill(0);
103
- FastPhase3Solver.ttGen.fill(0);
104
- FastPhase3Solver.callSeq = 1;
105
- FastPhase3Solver.epoch = 1;
106
- }
108
+ clearTranspositionTable() {
109
+ this.tt.fill(0);
110
+ this.callSeq = 1;
111
+ }
112
+ setTableContext(center3, edge3Distances) {
113
+ if (this.contextCenter3 === center3 && this.contextEdge3 === edge3Distances)
114
+ return;
115
+ this.clearTranspositionTable();
116
+ this.contextCenter3 = center3;
117
+ this.contextEdge3 = edge3Distances;
107
118
  }
108
- /** Fresh per-call id for TT-generation tagging (epoch wrap resets it). */
109
- static nextCallId() {
110
- const id = FastPhase3Solver.callSeq++;
111
- if (FastPhase3Solver.callSeq > 0xfffffffe) {
112
- FastPhase3Solver.ttGen.fill(0);
113
- FastPhase3Solver.callSeq = 1;
114
- return 1;
119
+ /** Fresh per-call id for TT-generation tagging. */
120
+ nextCallId() {
121
+ const id = this.callSeq++;
122
+ if (this.callSeq >= FastPhase3Solver.CALL_ID_LIMIT) {
123
+ this.tt.fill(0);
124
+ this.callSeq = 1;
115
125
  }
116
126
  return id;
117
127
  }
118
128
  constructor() {
119
- this.tempep = Array.from({ length: 30 }, () => new Uint8Array(12));
120
- this.moveLog = new Array(30);
129
+ // Preallocated to well above the deepest phase-3 sweep (maxLength3 <= 27):
130
+ // `ensureDepth` stays as a fallback for out-of-band callers.
131
+ this.tempep = Array.from({ length: 48 }, () => new Uint8Array(12));
132
+ this.moveLog = new Array(48);
121
133
  this.sym = getSymTables();
122
134
  }
135
+ ensureDepth(depth) {
136
+ while (this.tempep.length <= depth) {
137
+ this.tempep.push(new Uint8Array(12));
138
+ this.moveLog.push(0);
139
+ }
140
+ }
123
141
  nodeCount = 0;
124
- /** Cumulative nodes across calls (reset per attempt via resetEpochNodes). */
142
+ /** Cumulative nodes across calls (reset per solve generation). */
125
143
  epochNodes = 0;
126
144
  /**
127
145
  * Proof bookkeeping for IDA* threshold propagation (reset per exact-length
@@ -133,9 +151,8 @@ export class FastPhase3Solver {
133
151
  * - `sawGoal`: a goal was reached (even if the validator rejected it —
134
152
  * a nearby length may still validate, so only step L+1).
135
153
  * - `ttTouched`: a transposition-table hit from ANOTHER exact-length call
136
- * skipped a subtree (same-call hits replay this call's own exhausted
137
- * subtree and stay clean; other calls may validate under a different
138
- * candidate, so their stored proof does not transfer).
154
+ * skipped a subtree, and the hit was not certified goal-free across
155
+ * candidate validators.
139
156
  * Bound-based axis skips need no flag: the skipped siblings are
140
157
  * same-face turns, so noteExcess records their triangle-inequality bound.
141
158
  */
@@ -144,11 +161,13 @@ export class FastPhase3Solver {
144
161
  sawGoal = false;
145
162
  ttTouched = false;
146
163
  minExcess = 0;
147
- getPrun(distances, idx) {
148
- return distances.length >= 31006080
149
- ? distances[idx]
150
- : (distances[idx >> 1] >> ((idx & 1) * 4)) & 0x0f;
151
- }
164
+ /**
165
+ * Root-length coverage of the tightest TT hit this call (depth +
166
+ * storedDepth): the pruned subtree proves no goal at or below that root
167
+ * length under the call's predicate. `Number.MAX_SAFE_INTEGER` when no hit
168
+ * pruned anything. Feeds `validatedThrough`.
169
+ */
170
+ ttCoverage = Number.MAX_SAFE_INTEGER;
152
171
  /**
153
172
  * Recursive IDA* worker on table transitions with Transposition Table pruning.
154
173
  */
@@ -159,18 +178,38 @@ export class FastPhase3Solver {
159
178
  this.capped = true;
160
179
  return false;
161
180
  }
162
- if (maxl === 0) {
181
+ // Goal test at EVERY node (any length <= maxl), not only at maxl === 0
182
+ // leaves: a goal is (edge paired AND center3 at goal) — both PDB
183
+ // distances zero (the parent's gates guarantee leaves are goals, so this
184
+ // strictly generalizes the old leaf test). Exact-length-only detection
185
+ // made shorter goals on the same path invisible ("a nearby length may
186
+ // still validate"), which both blocked proofs on any raw goal and hid
187
+ // shorter solutions.
188
+ if (prun === 0 && center3Tables.pruning[ct] === 0) {
189
+ this.sawGoal = true;
163
190
  if (!collect)
164
191
  return true;
165
- this.sawGoal = true;
166
192
  collect.examined++;
167
193
  const moves = this.moveLog.slice(0, depth).map((m) => SEARCH_MOVES[m]);
168
194
  if (collect.validate(moves))
169
195
  return true;
170
- if (collect.examined >= collect.limit)
196
+ if (collect.examined >= collect.limit) {
171
197
  this.limitHit = true;
172
- return collect.examined >= collect.limit;
198
+ return true;
199
+ }
200
+ // A rejected goal is not a goal for the validator's predicate: keep
201
+ // exploring the subtree — validated goals may sit deeper on the same
202
+ // branches. At the fringe (maxl === 0) there is no subtree left, so
203
+ // record the branch's coverage claim here (nothing below `depth` on
204
+ // this branch was searched): "no validated goal at <= depth".
205
+ if (maxl === 0 && depth < this.minExcess)
206
+ this.minExcess = depth + 1;
207
+ }
208
+ else if (maxl === 0) {
209
+ return false;
173
210
  }
211
+ if (depth >= this.tempep.length)
212
+ this.ensureDepth(depth);
174
213
  const sym = this.sym;
175
214
  const symMvrot = sym.mvrot;
176
215
  const symMvroto = sym.mvroto;
@@ -184,25 +223,37 @@ export class FastPhase3Solver {
184
223
  else {
185
224
  ep.set(eplast);
186
225
  }
187
- // TT lookup (for depths >= 2 where TT hit prunes multi-move subtrees)
188
- const keyHigh = ((ct << 16) | (ep[8] | (ep[9] << 4) | (ep[10] << 8) | (ep[11] << 12))) >>> 0;
189
- const keyLow = (ep[0] | (ep[1] << 4) | (ep[2] << 8) | (ep[3] << 12) |
190
- (ep[4] << 16) | (ep[5] << 20) | (ep[6] << 24) | (ep[7] << 28)) >>> 0;
191
- const slot = (((keyLow ^ Math.imul(keyHigh, 0x9e3779b9)) & FastPhase3Solver.TT_MASK) * 3) >>> 0;
192
- if (maxl >= 2) {
193
- const storedHigh = FastPhase3Solver.tt[slot];
194
- const storedLow = FastPhase3Solver.tt[slot + 1];
195
- const storedInfo = FastPhase3Solver.tt[slot + 2];
226
+ const hasHc = hc !== undefined && hcCoord >= 0 && hc.moves.length > 0;
227
+ // HC pruning depends on a second coordinate not represented in the TT
228
+ // key. The root c3-pair check also varies only at depth zero. Do not reuse
229
+ // entries in those contexts; all other phase-3 state is key-complete.
230
+ const useTt = !hasHc && !(depth === 0 && c3PairDist !== undefined);
231
+ // TT: entries claim "no RAW goal at any length <= maxl from (ep, lm)" —
232
+ // coordinate-complete and predicate-independent (the validator's
233
+ // full-state checks are NOT coordinate-complete, so no validator-scoped
234
+ // claims may be cached). Hits are `<=`-claims and therefore
235
+ // proof-compatible for `validatedThrough`/`provenThrough` coverage.
236
+ const ttActive = useTt && maxl >= 2;
237
+ let keyHigh = 0;
238
+ let keyLow = 0;
239
+ let slot = 0;
240
+ if (ttActive) {
241
+ keyHigh = ((ct << 16) | (ep[8] | (ep[9] << 4) | (ep[10] << 8) | (ep[11] << 12))) >>> 0;
242
+ keyLow = (ep[0] | (ep[1] << 4) | (ep[2] << 8) | (ep[3] << 12) |
243
+ (ep[4] << 16) | (ep[5] << 20) | (ep[6] << 24) | (ep[7] << 28)) >>> 0;
244
+ slot = (((keyLow ^ Math.imul(keyHigh, 0x9e3779b9)) & FastPhase3Solver.TT_MASK) * 3) >>> 0;
245
+ const storedHigh = this.tt[slot];
246
+ const storedLow = this.tt[slot + 1];
247
+ const storedInfo = this.tt[slot + 2];
196
248
  if (storedHigh === keyHigh && storedLow === keyLow) {
197
- const storedEpoch = storedInfo >>> 16;
198
- const storedLm = (storedInfo >> 8) & 0xff;
199
- const storedDepth = storedInfo & 0xff;
200
- if (storedEpoch === FastPhase3Solver.epoch && storedDepth >= maxl && storedLm === lm) {
201
- // Same-call hits replay this call's own exhausted subtree (proof
202
- // stays clean); cross-call hits still prune but dirty the proof.
203
- // NOTE: ignoring cross-call entries was tried and reverted — they
204
- // carry most of the pruning power (3.82s vs 2.39s on the corpus).
205
- if (FastPhase3Solver.ttGen[(slot / 3) | 0] !== this.callId)
249
+ const storedCallId = (storedInfo >>> 11) & 0xfffff;
250
+ const storedLm = (storedInfo >>> 6) & 0x1f;
251
+ const storedDepth = storedInfo & 0x3f;
252
+ if (storedDepth >= maxl && storedLm === lm) {
253
+ const coverage = depth + storedDepth;
254
+ if (coverage < this.ttCoverage)
255
+ this.ttCoverage = coverage;
256
+ if (storedCallId !== this.callId)
206
257
  this.ttTouched = true;
207
258
  return false;
208
259
  }
@@ -214,15 +265,16 @@ export class FastPhase3Solver {
214
265
  const sym2Raw = sym.sym2Raw;
215
266
  const sym2Mask = sym.sym2Mask;
216
267
  const raw2Sym = sym.raw2Sym;
217
- const hasHc = hc !== undefined && hcCoord >= 0 && hc.moves.length > 0;
218
268
  const hcMoves = hasHc ? hc.moves : null;
219
269
  const hcDist = hasHc ? hc.distances : null;
220
270
  const hcRow = hasHc ? hcCoord * HC_PDB_MOVES.length : 0;
221
271
  // Byte-expanded (tests/generators) vs nibble-packed (loaders) distances.
222
272
  const edgeUnpacked = edge3Distances.length >= 31006080;
273
+ const examined0 = collect === undefined ? 0 : collect.examined;
274
+ const ckmvRow = lm * 20;
223
275
  for (let m = 0; m < 17; m++) {
224
- if (ckmv3[lm][m]) {
225
- m = skipAxis3[m];
276
+ if (ckmv3Flat[ckmvRow + m]) {
277
+ m = skipAxis3Fast[m];
226
278
  continue;
227
279
  }
228
280
  const nc = c3Move[c3Row + m];
@@ -230,9 +282,11 @@ export class FastPhase3Solver {
230
282
  continue;
231
283
  const prun1 = c3Prun[nc];
232
284
  if (prun1 >= maxl) {
233
- this.noteExcess(depth, prun1, prun1 > maxl && m < 14);
285
+ const ex1 = (prun1 > maxl && m < 14) ? depth + prun1 : depth + 1 + prun1;
286
+ if (ex1 < this.minExcess)
287
+ this.minExcess = ex1;
234
288
  if (prun1 > maxl && m < 14)
235
- m = skipAxis3[m];
289
+ m = skipAxis3Fast[m];
236
290
  continue;
237
291
  }
238
292
  let nhc = -1;
@@ -241,9 +295,11 @@ export class FastPhase3Solver {
241
295
  if (nhc >= 0) {
242
296
  const prunh = hcDist[nhc];
243
297
  if (prunh >= maxl) {
244
- this.noteExcess(depth, prunh, prunh > maxl && m < 14);
298
+ const exH = (prunh > maxl && m < 14) ? depth + prunh : depth + 1 + prunh;
299
+ if (exH < this.minExcess)
300
+ this.minExcess = exH;
245
301
  if (prunh > maxl && m < 14)
246
- m = skipAxis3[m];
302
+ m = skipAxis3Fast[m];
247
303
  continue;
248
304
  }
249
305
  }
@@ -264,13 +320,36 @@ export class FastPhase3Solver {
264
320
  prunPair = d;
265
321
  }
266
322
  if (prunPair >= maxl) {
267
- this.noteExcess(depth, prunPair, prunPair > maxl && m < 14);
323
+ const exP = (prunPair > maxl && m < 14) ? depth + prunPair : depth + 1 + prunPair;
324
+ if (exP < this.minExcess)
325
+ this.minExcess = exP;
268
326
  if (prunPair > maxl && m < 14)
269
- m = skipAxis3[m];
327
+ m = skipAxis3Fast[m];
270
328
  continue;
271
329
  }
272
330
  }
273
- const symIdx = getMvSymFrom(ep, ep, m, sym2Raw, sym2Mask, raw2Sym, symMvrot, symMvroto);
331
+ // Inlined getMvSymFrom(ep, ep, m, ...) (bit-identical; masks here are
332
+ // 12-bit, so one popcount lookup replaces bitCount's three).
333
+ let mrIdx = m << 3;
334
+ let movoBase = mrIdx * 12;
335
+ let raw = 0;
336
+ let bits = 0;
337
+ for (let i = 0; i < 4; i++) {
338
+ const val = symMvroto[movoBase + ep[symMvrot[movoBase + i]]];
339
+ raw = raw * (12 - i) + val - POPC12[bits & ((1 << val) - 1)];
340
+ bits |= 1 << val;
341
+ }
342
+ const symBase = raw2Sym[raw];
343
+ mrIdx |= symBase & 7;
344
+ let symOut = symBase >> 3;
345
+ bits = sym2Mask[symOut];
346
+ movoBase = mrIdx * 12;
347
+ for (let i = 4; i < 10; i++) {
348
+ const val = symMvroto[movoBase + ep[symMvrot[movoBase + i]]];
349
+ symOut = symOut * (12 - i) + val - POPC12[bits & ((1 << val) - 1)];
350
+ bits |= 1 << val;
351
+ }
352
+ const symIdx = ((symOut << 3) | (mrIdx & 7)) >>> 0;
274
353
  // Format-aware read: loaders keep the nibble-packed table (15.5MB);
275
354
  // tests and generators may pass byte-expanded buffers instead.
276
355
  const eIdx = symIdx >> 3;
@@ -278,9 +357,11 @@ export class FastPhase3Solver {
278
357
  ? edge3Distances[eIdx]
279
358
  : (edge3Distances[eIdx >> 1] >> ((eIdx & 1) * 4)) & 0x0f;
280
359
  if (prunx >= maxl) {
281
- this.noteExcess(depth, prunx, prunx > maxl && m < 14);
360
+ const exE = (prunx > maxl && m < 14) ? depth + prunx : depth + 1 + prunx;
361
+ if (exE < this.minExcess)
362
+ this.minExcess = exE;
282
363
  if (prunx > maxl && m < 14)
283
- m = skipAxis3[m];
364
+ m = skipAxis3Fast[m];
284
365
  continue;
285
366
  }
286
367
  this.moveLog[depth] = m;
@@ -288,36 +369,38 @@ export class FastPhase3Solver {
288
369
  return true;
289
370
  }
290
371
  }
291
- // Record failure in TT if node didn't abort due to node cap
292
- if (this.nodeCount <= maxNodes && maxl >= 2) {
293
- FastPhase3Solver.tt[slot] = keyHigh;
294
- FastPhase3Solver.tt[slot + 1] = keyLow;
295
- FastPhase3Solver.tt[slot + 2] = (FastPhase3Solver.epoch << 16) | (lm << 8) | maxl;
296
- FastPhase3Solver.ttGen[(slot / 3) | 0] = this.callId;
372
+ // Cache only subtrees with NO raw goal reached in them (the examined
373
+ // count is the goal counter — rejections included, since the claim is
374
+ // raw). Goal-free subtrees are safe to reuse across calls, candidates
375
+ // and predicates.
376
+ if (ttActive &&
377
+ this.nodeCount <= maxNodes &&
378
+ (collect?.examined ?? 0) === examined0) {
379
+ this.tt[slot] = keyHigh;
380
+ this.tt[slot + 1] = keyLow;
381
+ this.tt[slot + 2] = ((this.callId << 11) | (lm << 6) | maxl) >>> 0;
297
382
  }
298
383
  return false;
299
384
  }
300
385
  /**
301
- * Record an IDA* bound prune: the child at `depth + 1` needs at least
302
- * `childBound` more moves, so no solution exists through this branch
303
- * below root threshold `depth + 1 + childBound`. When a bound-based axis
304
- * skip also fires, the skipped siblings are same-face turns (verified:
305
- * every non-empty skipAxis3 run under m < 14 is a same-base triple),
306
- * hence at most 1 legal move apart — their bound is at least
386
+ * Record an IDA* bound prune (inlined at the four gate sites): the child at
387
+ * `depth + 1` needs at least `childBound` more moves, so no solution exists
388
+ * through this branch below root threshold `depth + 1 + childBound`. When a
389
+ * bound-based axis skip also fires, the skipped siblings are same-face turns
390
+ * (verified: every non-empty skipAxis3 run under m < 14 is a same-base
391
+ * triple), hence at most 1 legal move apart — their bound is at least
307
392
  * `childBound - 1` by the triangle inequality on the exact PDBs, so the
308
393
  * recorded excess `depth + childBound` stays a true lower bound.
309
394
  */
310
- noteExcess(depth, childBound, skippedSiblings) {
311
- const excess = skippedSiblings ? depth + childBound : depth + 1 + childBound;
312
- if (excess < this.minExcess)
313
- this.minExcess = excess;
314
- }
315
395
  /**
316
396
  * Search for a phase-3 solution with exact target length (first found).
317
397
  */
318
398
  solveLength(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, hc, hcCoord = -1, c3PairDist) {
399
+ this.setTableContext(center3Tables, edge3Distances);
319
400
  this.tempep[0].set(edge);
320
401
  this.nodeCount = 0;
402
+ this.callId = this.nextCallId();
403
+ this.ttCoverage = Number.MAX_SAFE_INTEGER;
321
404
  if (this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, undefined, hc, hcCoord, c3PairDist)) {
322
405
  return this.moveLog.slice(0, length3).map((m) => SEARCH_MOVES[m]);
323
406
  }
@@ -343,6 +426,7 @@ export class FastPhase3Solver {
343
426
  * it is `length3 - 1` at most (only the attempted length is settled).
344
427
  */
345
428
  solveLengthProven(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, limit = 16, validate, hc, hcCoord = -1, c3PairDist) {
429
+ this.setTableContext(center3Tables, edge3Distances);
346
430
  if (c3PairDist) {
347
431
  const c3Base = ct3 * 144;
348
432
  let maxPairDist = 0;
@@ -353,7 +437,7 @@ export class FastPhase3Solver {
353
437
  maxPairDist = d;
354
438
  }
355
439
  if (maxPairDist > length3) {
356
- return { moves: null, provenThrough: maxPairDist - 1, nodesUsed: 0 };
440
+ return { moves: null, provenThrough: maxPairDist - 1, validatedThrough: maxPairDist - 1, nodesUsed: 0 };
357
441
  }
358
442
  }
359
443
  this.tempep[0].set(edge);
@@ -363,7 +447,8 @@ export class FastPhase3Solver {
363
447
  this.sawGoal = false;
364
448
  this.ttTouched = false;
365
449
  this.minExcess = Number.MAX_SAFE_INTEGER;
366
- this.callId = FastPhase3Solver.nextCallId();
450
+ this.ttCoverage = Number.MAX_SAFE_INTEGER;
451
+ this.callId = this.nextCallId();
367
452
  let accepted = null;
368
453
  const collect = {
369
454
  limit,
@@ -373,15 +458,15 @@ export class FastPhase3Solver {
373
458
  accepted = moves;
374
459
  return true;
375
460
  }
376
- if (!accepted)
377
- accepted = moves;
461
+ // A rejected goal is not a validated solution. Enumeration callers
462
+ // may collect it themselves, but cannot use it as a search proof.
378
463
  return false;
379
464
  }
380
465
  };
381
466
  this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, collect, hc, hcCoord, c3PairDist);
382
467
  if (accepted)
383
- return { moves: accepted, provenThrough: length3, nodesUsed: this.nodeCount };
384
- if (process.env.CUBE4_PROOF_STATS === "1") {
468
+ return { moves: accepted, provenThrough: length3, validatedThrough: length3, nodesUsed: this.nodeCount };
469
+ if (typeof process !== "undefined" && process.env?.CUBE4_PROOF_STATS === "1") {
385
470
  proofStats.total++;
386
471
  if (this.capped)
387
472
  proofStats.capped++;
@@ -392,19 +477,41 @@ export class FastPhase3Solver {
392
477
  if (this.ttTouched)
393
478
  proofStats.ttTouched++;
394
479
  }
480
+ // `provenThrough` keeps its conservative raw-goal contract: it may only
481
+ // claim a depth when NO raw goal was reached at all (a rejected goal IS
482
+ // a raw goal), no TT hit intervened, and the bound prunes cover past the
483
+ // attempted length.
484
+ let provenThrough = -1;
395
485
  if (!this.capped &&
396
486
  !this.limitHit &&
397
487
  !this.sawGoal &&
398
488
  !this.ttTouched &&
399
489
  this.minExcess > length3 &&
400
490
  this.minExcess !== Number.MAX_SAFE_INTEGER) {
401
- if (process.env.CUBE4_PROOF_STATS === "1") {
491
+ if (typeof process !== "undefined" && process.env?.CUBE4_PROOF_STATS === "1") {
402
492
  proofStats.clean++;
403
493
  proofStats.jumpSum += this.minExcess - 1 - length3;
404
494
  }
405
- return { moves: null, provenThrough: this.minExcess - 1, nodesUsed: this.nodeCount };
495
+ provenThrough = this.minExcess - 1;
496
+ }
497
+ // `validatedThrough` is scoped to THIS call's validator predicate (all
498
+ // calls sharing one solver instance use one predicate): rejected raw
499
+ // goals are not goals for it, and TT hits are proof-compatible (entries
500
+ // mean "no goal under the call's predicate at any length <= stored").
501
+ // Every walked branch leaves a claim: bound prunes record their
502
+ // threshold (noteExcess), fringe rejections record `length3 + 1` below,
503
+ // and TT-hit subtrees record `ttCoverage`.
504
+ let validatedThrough = -1;
505
+ if (!this.capped && !this.limitHit) {
506
+ let bound = length3;
507
+ if (this.minExcess !== Number.MAX_SAFE_INTEGER && this.minExcess - 1 > bound) {
508
+ bound = this.minExcess - 1;
509
+ }
510
+ if (this.ttCoverage < bound)
511
+ bound = this.ttCoverage;
512
+ validatedThrough = bound;
406
513
  }
407
- return { moves: null, provenThrough: length3, nodesUsed: this.nodeCount };
514
+ return { moves: null, provenThrough, validatedThrough, nodesUsed: this.nodeCount };
408
515
  }
409
516
  }
410
517
  /** Temporary instrumentation for proof-statistics (CUBE4_PROOF_STATS=1). */
@@ -426,11 +533,11 @@ export function resetProofStats() {
426
533
  proofStats.clean = 0;
427
534
  proofStats.jumpSum = 0;
428
535
  }
429
- const defaultSolver = new FastPhase3Solver();
430
536
  /**
431
537
  * Solve Phase 3 with iterative deepening on a single Phase3Start.
432
538
  */
433
- export async function solvePhase3(start, center3Tables, edge3Tables, maxLength = 20) {
539
+ export async function solvePhase3(start, center3Tables, edge3Tables, maxLength = 20, maxNodes = 80_000) {
540
+ const solver = new FastPhase3Solver();
434
541
  const sym = getSymTables();
435
542
  const symIdx = getMvSymFrom(start.edge, start.edge, 20, sym.sym2Raw, sym.sym2Mask, sym.raw2Sym, sym.mvrot, sym.mvroto);
436
543
  const edist = (edge3Tables.distances[(symIdx >> 3) >> 1] >> (((symIdx >> 3) & 1) * 4)) & 0x0f;
@@ -439,7 +546,7 @@ export async function solvePhase3(start, center3Tables, edge3Tables, maxLength =
439
546
  return null;
440
547
  const minLength = Math.max(cdist, edist);
441
548
  for (let l = minLength; l <= maxLength; l++) {
442
- const moves = await defaultSolver.solveLength(start.edge, start.ct3, edist, l, center3Tables, edge3Tables.distances);
549
+ const moves = solver.solveLength(start.edge, start.ct3, edist, l, center3Tables, edge3Tables.distances, maxNodes);
443
550
  if (moves) {
444
551
  return {
445
552
  moves,
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Placement of the whole-cube rotation fix inside a 4x4 solution.
3
+ *
4
+ * The reduction ends with the cube solved in some orientation, so the solve
5
+ * is `P ++ W(R) ++ Q` where `W(R)` are the 2-4 wide moves of the rotation R
6
+ * (`x` = `Rw Lw'`, ...). A rotation commutes with any move sequence once the
7
+ * sequence's faces are re-mapped:
8
+ *
9
+ * X ++ W(R) ≡ W(R) ++ rotateAlgorithm(X, R)
10
+ *
11
+ * so the fix can sit at ANY point of the solution (with the moves after it
12
+ * re-mapped), and a two-rotation fix can be split across two points. The
13
+ * total is unchanged except at the insertion junctions, where the fix can
14
+ * cancel against same-axis wide moves (`Uw2 · y2` = `Uw2 · Uw2 Dw2` = `Dw2`).
15
+ * `placeRotationFix` picks the placement with the shortest simplified total.
16
+ */
17
+ import type { Cube4Move } from "../types.js";
18
+ /** Wide-move words of the 9 single rotations. */
19
+ export declare const ROTATION_WORDS: Readonly<Record<string, readonly Cube4Move[]>>;
20
+ /**
21
+ * Shortest simplified solution equivalent to `prefix ++ W(rotation) ++
22
+ * suffix` (same effect on every state), over every run-boundary insertion
23
+ * point of the fix and every minimal factorization of the rotation. Only
24
+ * run boundaries matter: the fix can only merge with runs on its own axis,
25
+ * and re-mapping preserves the run structure of what follows it.
26
+ */
27
+ export declare function placeRotationFix(prefix: readonly Cube4Move[], rotation: readonly string[], suffix: readonly Cube4Move[]): Cube4Move[];
28
+ //# sourceMappingURL=rotation-fix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rotation-fix.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube4/reduction/rotation-fix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,iDAAiD;AACjD,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,CAUzE,CAAC;AA0FF;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,SAAS,EAAE,CA0Fb"}