@cubingcenter/scrambler 0.6.3 → 0.7.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 (115) hide show
  1. package/README.md +2 -0
  2. package/dist/puzzles/cube2/solver/search.d.ts.map +1 -1
  3. package/dist/puzzles/cube2/solver/search.js +3 -3
  4. package/dist/puzzles/cube2/solver/tables.d.ts +6 -0
  5. package/dist/puzzles/cube2/solver/tables.d.ts.map +1 -1
  6. package/dist/puzzles/cube2/solver/tables.js +16 -2
  7. package/dist/puzzles/cube3/index.d.ts +1 -0
  8. package/dist/puzzles/cube3/index.d.ts.map +1 -1
  9. package/dist/puzzles/cube3/moves.d.ts.map +1 -1
  10. package/dist/puzzles/cube3/moves.js +104 -18
  11. package/dist/puzzles/cube3/scrambler.d.ts +16 -1
  12. package/dist/puzzles/cube3/scrambler.d.ts.map +1 -1
  13. package/dist/puzzles/cube3/scrambler.js +1 -1
  14. package/dist/puzzles/cube3/solver/browser-loader.js +6 -2
  15. package/dist/puzzles/cube3/solver/index.d.ts +5 -3
  16. package/dist/puzzles/cube3/solver/index.d.ts.map +1 -1
  17. package/dist/puzzles/cube3/solver/index.js +2 -1
  18. package/dist/puzzles/cube3/solver/node-loader.js +9 -2
  19. package/dist/puzzles/cube3/solver/phase1.d.ts +18 -0
  20. package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
  21. package/dist/puzzles/cube3/solver/phase1.js +132 -14
  22. package/dist/puzzles/cube3/solver/phase2.d.ts +47 -0
  23. package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
  24. package/dist/puzzles/cube3/solver/phase2.js +236 -66
  25. package/dist/puzzles/cube3/solver/search-utils.d.ts +1 -1
  26. package/dist/puzzles/cube3/solver/search-utils.d.ts.map +1 -1
  27. package/dist/puzzles/cube3/solver/symmetry.d.ts +24 -0
  28. package/dist/puzzles/cube3/solver/symmetry.d.ts.map +1 -0
  29. package/dist/puzzles/cube3/solver/symmetry.js +74 -0
  30. package/dist/puzzles/cube3/solver/tables.d.ts +6 -1
  31. package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
  32. package/dist/puzzles/cube3/validation.d.ts.map +1 -1
  33. package/dist/puzzles/cube3/validation.js +15 -7
  34. package/dist/puzzles/cube4/index.d.ts +3 -2
  35. package/dist/puzzles/cube4/index.d.ts.map +1 -1
  36. package/dist/puzzles/cube4/index.js +2 -2
  37. package/dist/puzzles/cube4/moves.d.ts +5 -0
  38. package/dist/puzzles/cube4/moves.d.ts.map +1 -1
  39. package/dist/puzzles/cube4/moves.js +120 -11
  40. package/dist/puzzles/cube4/reduction/browser-loader.d.ts +3 -0
  41. package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
  42. package/dist/puzzles/cube4/reduction/browser-loader.js +34 -3
  43. package/dist/puzzles/cube4/reduction/center2.d.ts +20 -3
  44. package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
  45. package/dist/puzzles/cube4/reduction/center2.js +53 -9
  46. package/dist/puzzles/cube4/reduction/edge3.d.ts +4 -0
  47. package/dist/puzzles/cube4/reduction/edge3.d.ts.map +1 -1
  48. package/dist/puzzles/cube4/reduction/edge3.js +23 -3
  49. package/dist/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
  50. package/dist/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
  51. package/dist/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
  52. package/dist/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
  53. package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +38 -0
  54. package/dist/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
  55. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +50 -0
  56. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -0
  57. package/dist/puzzles/cube4/reduction/hc-pdb.js +202 -0
  58. package/dist/puzzles/cube4/reduction/node-loader.d.ts +5 -0
  59. package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
  60. package/dist/puzzles/cube4/reduction/node-loader.js +48 -8
  61. package/dist/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
  62. package/dist/puzzles/cube4/reduction/phase3-search.d.ts +111 -5
  63. package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
  64. package/dist/puzzles/cube4/reduction/phase3-search.js +341 -22
  65. package/dist/puzzles/cube4/reduction/solve.d.ts +112 -1
  66. package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
  67. package/dist/puzzles/cube4/reduction/solve.js +920 -181
  68. package/dist/puzzles/cube4/scrambler.d.ts +105 -4
  69. package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
  70. package/dist/puzzles/cube4/scrambler.js +216 -166
  71. package/dist/puzzles/cube4/state.d.ts.map +1 -1
  72. package/dist/puzzles/cube4/state.js +4 -1
  73. package/dist/puzzles/pyraminx/solver/search.d.ts.map +1 -1
  74. package/dist/puzzles/pyraminx/solver/search.js +3 -6
  75. package/dist/puzzles/pyraminx/solver/tables.d.ts +6 -0
  76. package/dist/puzzles/pyraminx/solver/tables.d.ts.map +1 -1
  77. package/dist/puzzles/pyraminx/solver/tables.js +17 -0
  78. package/dist/puzzles/skewb/solver/search.js +3 -3
  79. package/dist/puzzles/skewb/solver/tables.d.ts +6 -0
  80. package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -1
  81. package/dist/puzzles/skewb/solver/tables.js +17 -0
  82. package/dist-cjs/puzzles/cube2/solver/search.js +2 -2
  83. package/dist-cjs/puzzles/cube2/solver/tables.js +17 -3
  84. package/dist-cjs/puzzles/cube3/moves.js +104 -18
  85. package/dist-cjs/puzzles/cube3/scrambler.js +1 -1
  86. package/dist-cjs/puzzles/cube3/solver/browser-loader.js +6 -2
  87. package/dist-cjs/puzzles/cube3/solver/index.js +8 -1
  88. package/dist-cjs/puzzles/cube3/solver/node-loader.js +9 -2
  89. package/dist-cjs/puzzles/cube3/solver/phase1.js +133 -14
  90. package/dist-cjs/puzzles/cube3/solver/phase2.js +233 -63
  91. package/dist-cjs/puzzles/cube3/solver/symmetry.js +80 -0
  92. package/dist-cjs/puzzles/cube3/validation.js +15 -7
  93. package/dist-cjs/puzzles/cube4/index.js +3 -1
  94. package/dist-cjs/puzzles/cube4/moves.js +121 -11
  95. package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +34 -3
  96. package/dist-cjs/puzzles/cube4/reduction/center2.js +54 -9
  97. package/dist-cjs/puzzles/cube4/reduction/edge3.js +24 -3
  98. package/dist-cjs/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
  99. package/dist-cjs/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
  100. package/dist-cjs/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
  101. package/dist-cjs/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
  102. package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +38 -0
  103. package/dist-cjs/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
  104. package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +213 -0
  105. package/dist-cjs/puzzles/cube4/reduction/node-loader.js +48 -8
  106. package/dist-cjs/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
  107. package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +343 -23
  108. package/dist-cjs/puzzles/cube4/reduction/solve.js +918 -173
  109. package/dist-cjs/puzzles/cube4/scrambler.js +214 -163
  110. package/dist-cjs/puzzles/cube4/state.js +4 -1
  111. package/dist-cjs/puzzles/pyraminx/solver/search.js +2 -5
  112. package/dist-cjs/puzzles/pyraminx/solver/tables.js +18 -1
  113. package/dist-cjs/puzzles/skewb/solver/search.js +2 -2
  114. package/dist-cjs/puzzles/skewb/solver/tables.js +18 -1
  115. package/package.json +14 -5
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { CENTER3_MOVES } from "./center3.js";
8
8
  import { getSymTables, getMvSymFrom, MOVE3_STD } from "./edge3.js";
9
+ import { HC_PDB_MOVES } from "./hc-pdb.js";
9
10
  /** The 17 exploration moves (the reference's m < 17 bound). */
10
11
  export const SEARCH_MOVES = CENTER3_MOVES.slice(0, 17);
11
12
  // Commutation / move-cancellation pruning tables
@@ -33,79 +34,397 @@ for (let i = 0; i < 20; i++) {
33
34
  }
34
35
  }
35
36
  }
36
- /**
37
- * Fast table-driven Phase 3 IDA* solver class.
38
- */
37
+ const F_SLOT_MAP = [
38
+ [4, 5, 2, 3, 1, 0, 6, 7, 8, 9, 10, 11],
39
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 9, 10, 11],
40
+ [5, 4, 2, 3, 0, 1, 6, 7, 8, 9, 10, 11],
41
+ [0, 1, 2, 3, 4, 6, 5, 7, 8, 9, 11, 10],
42
+ [11, 1, 2, 8, 4, 5, 6, 7, 0, 9, 10, 3],
43
+ [3, 1, 2, 0, 4, 5, 6, 7, 11, 9, 10, 8],
44
+ [8, 1, 2, 11, 4, 5, 6, 7, 3, 9, 10, 0],
45
+ [0, 1, 7, 6, 4, 5, 2, 3, 8, 9, 10, 11],
46
+ [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 10, 11],
47
+ [0, 1, 6, 7, 4, 5, 3, 2, 8, 9, 10, 11],
48
+ [0, 1, 2, 3, 7, 5, 6, 4, 9, 8, 10, 11],
49
+ [0, 9, 10, 3, 4, 5, 6, 7, 8, 2, 1, 11],
50
+ [0, 2, 1, 3, 4, 5, 6, 7, 8, 10, 9, 11],
51
+ [0, 10, 9, 3, 4, 5, 6, 7, 8, 1, 2, 11],
52
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 11, 10, 9],
53
+ [0, 3, 2, 1, 4, 6, 5, 7, 8, 9, 11, 10],
54
+ [3, 1, 2, 0, 4, 7, 6, 5, 11, 9, 10, 8]
55
+ ];
56
+ const H_SLOT_MAP = [
57
+ [4, 5, 2, 3, 1, 0, 6, 7, 8, 9, 10, 11],
58
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 9, 10, 11],
59
+ [5, 4, 2, 3, 0, 1, 6, 7, 8, 9, 10, 11],
60
+ [0, 1, 2, 3, 4, 6, 5, 7, 8, 9, 11, 10],
61
+ [11, 1, 2, 8, 4, 5, 6, 7, 0, 9, 10, 3],
62
+ [3, 1, 2, 0, 4, 5, 6, 7, 11, 9, 10, 8],
63
+ [8, 1, 2, 11, 4, 5, 6, 7, 3, 9, 10, 0],
64
+ [0, 1, 7, 6, 4, 5, 2, 3, 8, 9, 10, 11],
65
+ [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 10, 11],
66
+ [0, 1, 6, 7, 4, 5, 3, 2, 8, 9, 10, 11],
67
+ [0, 1, 2, 3, 7, 5, 6, 4, 9, 8, 10, 11],
68
+ [0, 9, 10, 3, 4, 5, 6, 7, 8, 2, 1, 11],
69
+ [0, 2, 1, 3, 4, 5, 6, 7, 8, 10, 9, 11],
70
+ [0, 10, 9, 3, 4, 5, 6, 7, 8, 1, 2, 11],
71
+ [1, 0, 2, 3, 5, 4, 6, 7, 10, 9, 8, 11],
72
+ [2, 1, 0, 3, 4, 6, 5, 7, 8, 9, 11, 10],
73
+ [3, 1, 2, 0, 6, 5, 4, 7, 11, 9, 10, 8]
74
+ ];
39
75
  export class FastPhase3Solver {
40
76
  tempep;
41
77
  moveLog;
42
78
  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;
83
+ 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;
94
+ callId = 0;
95
+ /** Reset the per-attempt cumulative node counter. */
96
+ resetEpochNodes() {
97
+ this.epochNodes = 0;
98
+ }
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
+ }
107
+ }
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;
115
+ }
116
+ return id;
117
+ }
43
118
  constructor() {
44
119
  this.tempep = Array.from({ length: 30 }, () => new Uint8Array(12));
45
120
  this.moveLog = new Array(30);
46
121
  this.sym = getSymTables();
47
122
  }
123
+ nodeCount = 0;
124
+ /** Cumulative nodes across calls (reset per attempt via resetEpochNodes). */
125
+ epochNodes = 0;
126
+ /**
127
+ * Proof bookkeeping for IDA* threshold propagation (reset per exact-length
128
+ * call). When a length-L search exhausts its tree WITHOUT hitting any of
129
+ * these flags, no (center, edge) solution exists at any length below
130
+ * `minExcess`, so the caller may skip straight to it:
131
+ * - `capped`: the node budget aborted the search (bounds unknown).
132
+ * - `limitHit`: goal enumeration hit its cap (tree not exhausted).
133
+ * - `sawGoal`: a goal was reached (even if the validator rejected it —
134
+ * a nearby length may still validate, so only step L+1).
135
+ * - `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).
139
+ * Bound-based axis skips need no flag: the skipped siblings are
140
+ * same-face turns, so noteExcess records their triangle-inequality bound.
141
+ */
142
+ capped = false;
143
+ limitHit = false;
144
+ sawGoal = false;
145
+ ttTouched = false;
146
+ minExcess = 0;
48
147
  getPrun(distances, idx) {
49
- const byte = distances[idx >> 1];
50
- return (idx & 1) === 0 ? byte & 0x0f : byte >> 4;
148
+ return distances.length >= 31006080
149
+ ? distances[idx]
150
+ : (distances[idx >> 1] >> ((idx & 1) * 4)) & 0x0f;
51
151
  }
52
152
  /**
53
- * Recursive IDA* worker on table transitions.
153
+ * Recursive IDA* worker on table transitions with Transposition Table pruning.
54
154
  */
55
- async search(eplast, ct, prun, maxl, lm, depth, center3Tables, edge3Distances) {
56
- if (maxl === 0)
57
- return true;
58
- if (depth % 500 === 0)
59
- await new Promise(r => setTimeout(r, 0));
155
+ search(eplast, ct, prun, maxl, lm, depth, center3Tables, edge3Distances, maxNodes = 15000, collect, hc, hcCoord = -1, c3PairDist) {
156
+ this.nodeCount++;
157
+ this.epochNodes++;
158
+ if (this.nodeCount > maxNodes) {
159
+ this.capped = true;
160
+ return false;
161
+ }
162
+ if (maxl === 0) {
163
+ if (!collect)
164
+ return true;
165
+ this.sawGoal = true;
166
+ collect.examined++;
167
+ const moves = this.moveLog.slice(0, depth).map((m) => SEARCH_MOVES[m]);
168
+ if (collect.validate(moves))
169
+ return true;
170
+ if (collect.examined >= collect.limit)
171
+ this.limitHit = true;
172
+ return collect.examined >= collect.limit;
173
+ }
174
+ const sym = this.sym;
175
+ const symMvrot = sym.mvrot;
176
+ const symMvroto = sym.mvroto;
60
177
  const ep = this.tempep[depth];
61
178
  if (lm !== 20) {
62
179
  const movoBase = (lm << 3) * 12;
63
- const movBase = (lm << 3) * 12;
64
180
  for (let i = 0; i < 12; i++) {
65
- ep[i] = this.sym.mvroto[movoBase + eplast[this.sym.mvrot[movBase + i]]];
181
+ ep[i] = symMvroto[movoBase + eplast[symMvrot[movoBase + i]]];
66
182
  }
67
183
  }
68
184
  else {
69
185
  ep.set(eplast);
70
186
  }
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];
196
+ 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)
206
+ this.ttTouched = true;
207
+ return false;
208
+ }
209
+ }
210
+ }
211
+ const c3Move = center3Tables.moveTable;
212
+ const c3Prun = center3Tables.pruning;
213
+ const c3Row = ct * CENTER3_MOVES.length;
214
+ const sym2Raw = sym.sym2Raw;
215
+ const sym2Mask = sym.sym2Mask;
216
+ const raw2Sym = sym.raw2Sym;
217
+ const hasHc = hc !== undefined && hcCoord >= 0 && hc.moves.length > 0;
218
+ const hcMoves = hasHc ? hc.moves : null;
219
+ const hcDist = hasHc ? hc.distances : null;
220
+ const hcRow = hasHc ? hcCoord * HC_PDB_MOVES.length : 0;
221
+ // Byte-expanded (tests/generators) vs nibble-packed (loaders) distances.
222
+ const edgeUnpacked = edge3Distances.length >= 31006080;
71
223
  for (let m = 0; m < 17; m++) {
72
224
  if (ckmv3[lm][m]) {
73
225
  m = skipAxis3[m];
74
226
  continue;
75
227
  }
76
- const nc = center3Tables.moveTable[ct * CENTER3_MOVES.length + m];
228
+ const nc = c3Move[c3Row + m];
77
229
  if (nc < 0)
78
230
  continue;
79
- const prun1 = center3Tables.pruning[nc];
231
+ const prun1 = c3Prun[nc];
80
232
  if (prun1 >= maxl) {
233
+ this.noteExcess(depth, prun1, prun1 > maxl && m < 14);
81
234
  if (prun1 > maxl && m < 14)
82
235
  m = skipAxis3[m];
83
236
  continue;
84
237
  }
85
- const symIdx = getMvSymFrom(ep, ep, m, this.sym.sym2Raw, this.sym.sym2Mask, this.sym.raw2Sym, this.sym.mvrot, this.sym.mvroto);
86
- const prunx = this.getPrun(edge3Distances, symIdx >> 3);
238
+ let nhc = -1;
239
+ if (hasHc) {
240
+ nhc = hcMoves[hcRow + m];
241
+ if (nhc >= 0) {
242
+ const prunh = hcDist[nhc];
243
+ if (prunh >= maxl) {
244
+ this.noteExcess(depth, prunh, prunh > maxl && m < 14);
245
+ if (prunh > maxl && m < 14)
246
+ m = skipAxis3[m];
247
+ continue;
248
+ }
249
+ }
250
+ }
251
+ if (c3PairDist && depth === 0) {
252
+ const c3Base = nc * 144;
253
+ const fSlot = F_SLOT_MAP[m];
254
+ const hSlot = H_SLOT_MAP[m];
255
+ let prunPair = 0;
256
+ for (let s = 0; s < 12; s++) {
257
+ const idx = c3Base + fSlot[s] * 12 + hSlot[ep[s]];
258
+ const d = (idx & 1) === 0 ? (c3PairDist[idx >> 1] & 0x0f) : (c3PairDist[idx >> 1] >> 4);
259
+ if (d >= maxl) {
260
+ prunPair = d;
261
+ break;
262
+ }
263
+ if (d > prunPair)
264
+ prunPair = d;
265
+ }
266
+ if (prunPair >= maxl) {
267
+ this.noteExcess(depth, prunPair, prunPair > maxl && m < 14);
268
+ if (prunPair > maxl && m < 14)
269
+ m = skipAxis3[m];
270
+ continue;
271
+ }
272
+ }
273
+ const symIdx = getMvSymFrom(ep, ep, m, sym2Raw, sym2Mask, raw2Sym, symMvrot, symMvroto);
274
+ // Format-aware read: loaders keep the nibble-packed table (15.5MB);
275
+ // tests and generators may pass byte-expanded buffers instead.
276
+ const eIdx = symIdx >> 3;
277
+ const prunx = edgeUnpacked
278
+ ? edge3Distances[eIdx]
279
+ : (edge3Distances[eIdx >> 1] >> ((eIdx & 1) * 4)) & 0x0f;
87
280
  if (prunx >= maxl) {
281
+ this.noteExcess(depth, prunx, prunx > maxl && m < 14);
88
282
  if (prunx > maxl && m < 14)
89
283
  m = skipAxis3[m];
90
284
  continue;
91
285
  }
92
- if (await this.search(ep, nc, prunx, maxl - 1, m, depth + 1, center3Tables, edge3Distances)) {
93
- this.moveLog[depth] = m;
286
+ this.moveLog[depth] = m;
287
+ if (this.search(ep, nc, prunx, maxl - 1, m, depth + 1, center3Tables, edge3Distances, maxNodes, collect, hc, nhc, c3PairDist)) {
94
288
  return true;
95
289
  }
96
290
  }
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;
297
+ }
97
298
  return false;
98
299
  }
99
300
  /**
100
- * Search for a phase-3 solution with exact target length.
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
307
+ * `childBound - 1` by the triangle inequality on the exact PDBs, so the
308
+ * recorded excess `depth + childBound` stays a true lower bound.
101
309
  */
102
- async solveLength(edge, ct3, edist, length3, center3Tables, edge3Distances) {
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
+ /**
316
+ * Search for a phase-3 solution with exact target length (first found).
317
+ */
318
+ solveLength(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, hc, hcCoord = -1, c3PairDist) {
103
319
  this.tempep[0].set(edge);
104
- if (await this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances)) {
320
+ this.nodeCount = 0;
321
+ if (this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, undefined, hc, hcCoord, c3PairDist)) {
105
322
  return this.moveLog.slice(0, length3).map((m) => SEARCH_MOVES[m]);
106
323
  }
107
324
  return null;
108
325
  }
326
+ /**
327
+ * Search for a phase-3 solution with exact target length, accepting the
328
+ * first goal sequence that passes `validate` (up to `limit` examined,
329
+ * sharing `maxNodes`). With a validator that enforces the downstream
330
+ * requirements (reduction + standard orientation + 3x3 conversion), hard
331
+ * states whose first-found solutions are unusable get more draws from
332
+ * the same budget instead of an immediate miss.
333
+ */
334
+ solveLengthValid(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, limit = 16, validate, hc, hcCoord = -1, c3PairDist) {
335
+ return this.solveLengthProven(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes, limit, validate, hc, hcCoord, c3PairDist).moves;
336
+ }
337
+ /**
338
+ * Exact-length validated search that also reports what the call proved.
339
+ * `provenThrough` is the greatest length with provably no (center, edge)
340
+ * solution: set only when the tree was exhaustively explored with no goal
341
+ * reached, no node-cap abort, no TT hit, and complete excess info — the
342
+ * caller may then skip every shorter length for this candidate. Otherwise
343
+ * it is `length3 - 1` at most (only the attempted length is settled).
344
+ */
345
+ solveLengthProven(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, limit = 16, validate, hc, hcCoord = -1, c3PairDist) {
346
+ if (c3PairDist) {
347
+ const c3Base = ct3 * 144;
348
+ let maxPairDist = 0;
349
+ for (let s = 0; s < 12; s++) {
350
+ const idx = c3Base + s * 12 + edge[s];
351
+ const d = (idx & 1) === 0 ? (c3PairDist[idx >> 1] & 0x0f) : (c3PairDist[idx >> 1] >> 4);
352
+ if (d > maxPairDist)
353
+ maxPairDist = d;
354
+ }
355
+ if (maxPairDist > length3) {
356
+ return { moves: null, provenThrough: maxPairDist - 1, nodesUsed: 0 };
357
+ }
358
+ }
359
+ this.tempep[0].set(edge);
360
+ this.nodeCount = 0;
361
+ this.capped = false;
362
+ this.limitHit = false;
363
+ this.sawGoal = false;
364
+ this.ttTouched = false;
365
+ this.minExcess = Number.MAX_SAFE_INTEGER;
366
+ this.callId = FastPhase3Solver.nextCallId();
367
+ let accepted = null;
368
+ const collect = {
369
+ limit,
370
+ examined: 0,
371
+ validate: (moves) => {
372
+ if (validate(moves)) {
373
+ accepted = moves;
374
+ return true;
375
+ }
376
+ if (!accepted)
377
+ accepted = moves;
378
+ return false;
379
+ }
380
+ };
381
+ this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, collect, hc, hcCoord, c3PairDist);
382
+ if (accepted)
383
+ return { moves: accepted, provenThrough: length3, nodesUsed: this.nodeCount };
384
+ if (process.env.CUBE4_PROOF_STATS === "1") {
385
+ proofStats.total++;
386
+ if (this.capped)
387
+ proofStats.capped++;
388
+ if (this.limitHit)
389
+ proofStats.limitHit++;
390
+ if (this.sawGoal)
391
+ proofStats.sawGoal++;
392
+ if (this.ttTouched)
393
+ proofStats.ttTouched++;
394
+ }
395
+ if (!this.capped &&
396
+ !this.limitHit &&
397
+ !this.sawGoal &&
398
+ !this.ttTouched &&
399
+ this.minExcess > length3 &&
400
+ this.minExcess !== Number.MAX_SAFE_INTEGER) {
401
+ if (process.env.CUBE4_PROOF_STATS === "1") {
402
+ proofStats.clean++;
403
+ proofStats.jumpSum += this.minExcess - 1 - length3;
404
+ }
405
+ return { moves: null, provenThrough: this.minExcess - 1, nodesUsed: this.nodeCount };
406
+ }
407
+ return { moves: null, provenThrough: length3, nodesUsed: this.nodeCount };
408
+ }
409
+ }
410
+ /** Temporary instrumentation for proof-statistics (CUBE4_PROOF_STATS=1). */
411
+ export const proofStats = {
412
+ total: 0,
413
+ capped: 0,
414
+ limitHit: 0,
415
+ sawGoal: 0,
416
+ ttTouched: 0,
417
+ clean: 0,
418
+ jumpSum: 0
419
+ };
420
+ export function resetProofStats() {
421
+ proofStats.total = 0;
422
+ proofStats.capped = 0;
423
+ proofStats.limitHit = 0;
424
+ proofStats.sawGoal = 0;
425
+ proofStats.ttTouched = 0;
426
+ proofStats.clean = 0;
427
+ proofStats.jumpSum = 0;
109
428
  }
110
429
  const defaultSolver = new FastPhase3Solver();
111
430
  /**
@@ -7,25 +7,136 @@ import type { Cube4State, Cube4Move } from "../types.js";
7
7
  import { type Center1Tables } from "./center1.js";
8
8
  import { type Center2Tables } from "./center2.js";
9
9
  import { type Center3Tables } from "./center3.js";
10
+ import { type HcDenseTables } from "./hc-pdb.js";
10
11
  import { type Edge3Tables } from "./edge3.js";
11
12
  import type { Cube3State, Cube3Move } from "../../cube3/types.js";
13
+ import type { Cube3Phase1Tables } from "../../cube3/solver/tables.js";
12
14
  export interface Cube4SolveTables {
13
15
  center1: Center1Tables;
14
16
  center2: Center2Tables;
15
17
  center3: Center3Tables;
16
18
  edge3: Edge3Tables;
19
+ cube3Phase1?: Cube3Phase1Tables;
20
+ /** Half-class exact PDB (optional; tightens the phase-3 root bound and node-level pruning). */
21
+ hc?: HcDenseTables;
22
+ /** Joint Center3 x Single Pair PDB distances (optional). */
23
+ c3PairDist?: Uint8Array;
17
24
  }
18
25
  export interface Cube4SolveResult {
19
26
  /** The complete solve sequence (solves the input state). */
20
27
  moves: Cube4Move[];
21
28
  /** The scramble (the inverted solution). */
22
29
  scramble: Cube4Move[];
30
+ /** Winning reduction breakdown (unsimplified lengths). */
31
+ phases?: {
32
+ length12: number;
33
+ length1: number;
34
+ length2: number;
35
+ length3: number;
36
+ finish: number;
37
+ };
38
+ /** Search work: phase-2 DFS nodes + phase-3 IDA* nodes this attempt. */
39
+ nodes?: {
40
+ p2: number;
41
+ p3: number;
42
+ f3calls: number;
43
+ };
44
+ /** Wall-clock split of this solve (reduction search / finish loop / polish). */
45
+ timings?: {
46
+ reductionMs: number;
47
+ f1Ms: number;
48
+ f2Ms: number;
49
+ f3Ms: number;
50
+ finishMs: number;
51
+ polishMs: number;
52
+ };
23
53
  }
54
+ export declare function rotateAlgorithm(alg: readonly Cube4Move[], rotSeq: string[]): Cube4Move[];
55
+ export declare function invertRotationSequence(rotSeq: string[]): string[];
56
+ export declare function applyCubeRotation(state: Cube4State, rotSeq: string[]): Cube4State;
57
+ export declare const P2_CANCEL: readonly (readonly boolean[])[];
58
+ /** Temporary instrumentation for F2 goal statistics (CUBE4_F2_STATS=1). */
59
+ export declare const f2Stats: {
60
+ goals: number;
61
+ transversalPass: number;
62
+ axisRejects: number;
63
+ minTransversalL2: number;
64
+ minChiralL2: number;
65
+ };
66
+ export declare function resetF2Stats(): void;
67
+ /** Trigger rate for the stagnationCut early-stop (checks vs actual cuts). */
68
+ export declare const stagnationStats: {
69
+ checks: number;
70
+ cuts: number;
71
+ };
72
+ export declare function resetStagnationStats(): void;
24
73
  /**
25
74
  * Solve a 4x4 state with the reduction solver + the cube3 two-phase finish.
26
75
  * `solveCube3` is injected (the cube3 solver loads its tables lazily).
76
+ *
77
+ * The search is bounded by deterministic work caps (candidate counts, node
78
+ * budgets, length caps), so `timeoutMs` is only a wall-clock backstop for
79
+ * pathological cases — not a quality filter. Attempt failures mean the
80
+ * state is outside the solver's horizon, never "ran out of time".
27
81
  */
82
+ export interface Cube4SolveOptions {
83
+ /**
84
+ * Accepted for API compatibility but NOT enforced inside the search
85
+ * (deliberately: every loop is work-capped so the same state always
86
+ * takes the same path). Callers enforce their own wall-clock backstop
87
+ * around the call.
88
+ */
89
+ timeoutMs?: number;
90
+ /**
91
+ * Last-resort effort: widens every work cap (phase-2 depth/nodes,
92
+ * phase-3 budgets/lengths, pool caps). Only the fast path failing
93
+ * should trigger this — easy states never pay it.
94
+ */
95
+ deep?: boolean;
96
+ /** Phase-4 attempts among valid reductions (default 3, or centersBeam * edgesPerCenter). */
97
+ finishTries?: number;
98
+ /** Number of distinct center candidates from phase 2 to explore in phase 3 (default 3). */
99
+ centersBeam?: number | undefined;
100
+ /** Maximum number of distinct edge pairings to collect per center candidate in phase 3 (default 3). */
101
+ edgesPerCenter?: number | undefined;
102
+ /** Deep multi-candidate polish pass in Phase 3. */
103
+ deepPolish?: boolean | undefined;
104
+ /** Prune searches whose reduction exceeds this bound (branch-and-bound). */
105
+ maxReductionLen?: number | undefined;
106
+ /**
107
+ * Phase-2 wide move set: search all 28 center2 moves (adds
108
+ * Dw2/Lw/Lw2/Lw'/Bw2) with the `pruningWide` table instead of the
109
+ * default 23. Requires regenerated center2 tables; default false.
110
+ * (Measured: ~-0.2 moves for ~+15% time; keep opt-in.)
111
+ */
112
+ center2Wide?: boolean | undefined;
113
+ /**
114
+ * F1 enumeration depth beyond the per-axis optimum (default 3):
115
+ * collects up to `phase1Cap` F1 solutions of length <= minH + this,
116
+ * deliberately supra-optimal, to feed diverse F2/F3 candidates.
117
+ * (Measured: restricting to 0 shows no consistent benefit and
118
+ * interacts badly with center2Wide; keep the default.)
119
+ */
120
+ phase1ExtraDepths?: number | undefined;
121
+ /** F1 solution collection cap (default 60). */
122
+ phase1Cap?: number | undefined;
123
+ /** Phase-2 prep collection cap (default 100, or 160 in deep mode). */
124
+ prepCap?: number | undefined;
125
+ /** Max candidates to collect per (prep, length) DFS call (default 64, or 96 in deep mode). */
126
+ p1CandCap?: number | undefined;
127
+ /** Early stop when candidate pool has >= CAND_MIN and best value stagnates across 2 lengths. */
128
+ stagnationCut?: boolean | undefined;
129
+ /**
130
+ * Full-pipeline restarts over the SAME state (default 1): each restart
131
+ * re-runs phases 1-4 with a reshuffled search order (seeded by the state
132
+ * hash ^ restart index, so results stay deterministic) and the shortest
133
+ * simplified total wins. Same-state diversity preserves the sampled
134
+ * state's distribution — unlike bestOf, which samples new states.
135
+ * `1` keeps the legacy single deterministic pass bit-identical.
136
+ */
137
+ restarts?: number | undefined;
138
+ }
28
139
  export declare function solveCube4State(state: Cube4State, tables: Cube4SolveTables, solveCube3: (state: Cube3State) => Promise<{
29
140
  moves: Cube3Move[];
30
- }>): Promise<Cube4SolveResult>;
141
+ }>, options?: Cube4SolveOptions): Promise<Cube4SolveResult>;
31
142
  //# sourceMappingURL=solve.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"solve.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube4/reduction/solve.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEzD,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAKjE,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAIlE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,4CAA4C;IAC5C,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB;AAuKD;;;GAGG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,gBAAgB,EACxB,UAAU,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC;IAAE,KAAK,EAAE,SAAS,EAAE,CAAA;CAAE,CAAC,GACjE,OAAO,CAAC,gBAAgB,CAAC,CAqL3B"}
1
+ {"version":3,"file":"solve.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube4/reduction/solve.ts"],"names":[],"mappings":"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"}