@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,10 +6,12 @@
6
6
  * and all 12 wing pairs paired (sym-index == 0), using the 17-move exploration set.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.FastPhase3Solver = exports.skipAxis3 = exports.ckmv3 = exports.SEARCH_MOVES = void 0;
9
+ exports.proofStats = exports.FastPhase3Solver = exports.skipAxis3 = exports.ckmv3 = exports.SEARCH_MOVES = void 0;
10
+ exports.resetProofStats = resetProofStats;
10
11
  exports.solvePhase3 = solvePhase3;
11
12
  const center3_js_1 = require("./center3.js");
12
13
  const edge3_js_1 = require("./edge3.js");
14
+ const hc_pdb_js_1 = require("./hc-pdb.js");
13
15
  /** The 17 exploration moves (the reference's m < 17 bound). */
14
16
  exports.SEARCH_MOVES = center3_js_1.CENTER3_MOVES.slice(0, 17);
15
17
  // Commutation / move-cancellation pruning tables
@@ -37,81 +39,399 @@ for (let i = 0; i < 20; i++) {
37
39
  }
38
40
  }
39
41
  }
40
- /**
41
- * Fast table-driven Phase 3 IDA* solver class.
42
- */
42
+ const F_SLOT_MAP = [
43
+ [4, 5, 2, 3, 1, 0, 6, 7, 8, 9, 10, 11],
44
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 9, 10, 11],
45
+ [5, 4, 2, 3, 0, 1, 6, 7, 8, 9, 10, 11],
46
+ [0, 1, 2, 3, 4, 6, 5, 7, 8, 9, 11, 10],
47
+ [11, 1, 2, 8, 4, 5, 6, 7, 0, 9, 10, 3],
48
+ [3, 1, 2, 0, 4, 5, 6, 7, 11, 9, 10, 8],
49
+ [8, 1, 2, 11, 4, 5, 6, 7, 3, 9, 10, 0],
50
+ [0, 1, 7, 6, 4, 5, 2, 3, 8, 9, 10, 11],
51
+ [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 10, 11],
52
+ [0, 1, 6, 7, 4, 5, 3, 2, 8, 9, 10, 11],
53
+ [0, 1, 2, 3, 7, 5, 6, 4, 9, 8, 10, 11],
54
+ [0, 9, 10, 3, 4, 5, 6, 7, 8, 2, 1, 11],
55
+ [0, 2, 1, 3, 4, 5, 6, 7, 8, 10, 9, 11],
56
+ [0, 10, 9, 3, 4, 5, 6, 7, 8, 1, 2, 11],
57
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 11, 10, 9],
58
+ [0, 3, 2, 1, 4, 6, 5, 7, 8, 9, 11, 10],
59
+ [3, 1, 2, 0, 4, 7, 6, 5, 11, 9, 10, 8]
60
+ ];
61
+ const H_SLOT_MAP = [
62
+ [4, 5, 2, 3, 1, 0, 6, 7, 8, 9, 10, 11],
63
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 9, 10, 11],
64
+ [5, 4, 2, 3, 0, 1, 6, 7, 8, 9, 10, 11],
65
+ [0, 1, 2, 3, 4, 6, 5, 7, 8, 9, 11, 10],
66
+ [11, 1, 2, 8, 4, 5, 6, 7, 0, 9, 10, 3],
67
+ [3, 1, 2, 0, 4, 5, 6, 7, 11, 9, 10, 8],
68
+ [8, 1, 2, 11, 4, 5, 6, 7, 3, 9, 10, 0],
69
+ [0, 1, 7, 6, 4, 5, 2, 3, 8, 9, 10, 11],
70
+ [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 10, 11],
71
+ [0, 1, 6, 7, 4, 5, 3, 2, 8, 9, 10, 11],
72
+ [0, 1, 2, 3, 7, 5, 6, 4, 9, 8, 10, 11],
73
+ [0, 9, 10, 3, 4, 5, 6, 7, 8, 2, 1, 11],
74
+ [0, 2, 1, 3, 4, 5, 6, 7, 8, 10, 9, 11],
75
+ [0, 10, 9, 3, 4, 5, 6, 7, 8, 1, 2, 11],
76
+ [1, 0, 2, 3, 5, 4, 6, 7, 10, 9, 8, 11],
77
+ [2, 1, 0, 3, 4, 6, 5, 7, 8, 9, 11, 10],
78
+ [3, 1, 2, 0, 6, 5, 4, 7, 11, 9, 10, 8]
79
+ ];
43
80
  class FastPhase3Solver {
44
81
  tempep;
45
82
  moveLog;
46
83
  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;
88
+ 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;
99
+ callId = 0;
100
+ /** Reset the per-attempt cumulative node counter. */
101
+ resetEpochNodes() {
102
+ this.epochNodes = 0;
103
+ }
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
+ }
112
+ }
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;
120
+ }
121
+ return id;
122
+ }
47
123
  constructor() {
48
124
  this.tempep = Array.from({ length: 30 }, () => new Uint8Array(12));
49
125
  this.moveLog = new Array(30);
50
126
  this.sym = (0, edge3_js_1.getSymTables)();
51
127
  }
128
+ nodeCount = 0;
129
+ /** Cumulative nodes across calls (reset per attempt via resetEpochNodes). */
130
+ epochNodes = 0;
131
+ /**
132
+ * Proof bookkeeping for IDA* threshold propagation (reset per exact-length
133
+ * call). When a length-L search exhausts its tree WITHOUT hitting any of
134
+ * these flags, no (center, edge) solution exists at any length below
135
+ * `minExcess`, so the caller may skip straight to it:
136
+ * - `capped`: the node budget aborted the search (bounds unknown).
137
+ * - `limitHit`: goal enumeration hit its cap (tree not exhausted).
138
+ * - `sawGoal`: a goal was reached (even if the validator rejected it —
139
+ * a nearby length may still validate, so only step L+1).
140
+ * - `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).
144
+ * Bound-based axis skips need no flag: the skipped siblings are
145
+ * same-face turns, so noteExcess records their triangle-inequality bound.
146
+ */
147
+ capped = false;
148
+ limitHit = false;
149
+ sawGoal = false;
150
+ ttTouched = false;
151
+ minExcess = 0;
52
152
  getPrun(distances, idx) {
53
- const byte = distances[idx >> 1];
54
- return (idx & 1) === 0 ? byte & 0x0f : byte >> 4;
153
+ return distances.length >= 31006080
154
+ ? distances[idx]
155
+ : (distances[idx >> 1] >> ((idx & 1) * 4)) & 0x0f;
55
156
  }
56
157
  /**
57
- * Recursive IDA* worker on table transitions.
158
+ * Recursive IDA* worker on table transitions with Transposition Table pruning.
58
159
  */
59
- async search(eplast, ct, prun, maxl, lm, depth, center3Tables, edge3Distances) {
60
- if (maxl === 0)
61
- return true;
62
- if (depth % 500 === 0)
63
- await new Promise(r => setTimeout(r, 0));
160
+ search(eplast, ct, prun, maxl, lm, depth, center3Tables, edge3Distances, maxNodes = 15000, collect, hc, hcCoord = -1, c3PairDist) {
161
+ this.nodeCount++;
162
+ this.epochNodes++;
163
+ if (this.nodeCount > maxNodes) {
164
+ this.capped = true;
165
+ return false;
166
+ }
167
+ if (maxl === 0) {
168
+ if (!collect)
169
+ return true;
170
+ this.sawGoal = true;
171
+ collect.examined++;
172
+ const moves = this.moveLog.slice(0, depth).map((m) => exports.SEARCH_MOVES[m]);
173
+ if (collect.validate(moves))
174
+ return true;
175
+ if (collect.examined >= collect.limit)
176
+ this.limitHit = true;
177
+ return collect.examined >= collect.limit;
178
+ }
179
+ const sym = this.sym;
180
+ const symMvrot = sym.mvrot;
181
+ const symMvroto = sym.mvroto;
64
182
  const ep = this.tempep[depth];
65
183
  if (lm !== 20) {
66
184
  const movoBase = (lm << 3) * 12;
67
- const movBase = (lm << 3) * 12;
68
185
  for (let i = 0; i < 12; i++) {
69
- ep[i] = this.sym.mvroto[movoBase + eplast[this.sym.mvrot[movBase + i]]];
186
+ ep[i] = symMvroto[movoBase + eplast[symMvrot[movoBase + i]]];
70
187
  }
71
188
  }
72
189
  else {
73
190
  ep.set(eplast);
74
191
  }
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];
201
+ 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)
211
+ this.ttTouched = true;
212
+ return false;
213
+ }
214
+ }
215
+ }
216
+ const c3Move = center3Tables.moveTable;
217
+ const c3Prun = center3Tables.pruning;
218
+ const c3Row = ct * center3_js_1.CENTER3_MOVES.length;
219
+ const sym2Raw = sym.sym2Raw;
220
+ const sym2Mask = sym.sym2Mask;
221
+ const raw2Sym = sym.raw2Sym;
222
+ const hasHc = hc !== undefined && hcCoord >= 0 && hc.moves.length > 0;
223
+ const hcMoves = hasHc ? hc.moves : null;
224
+ const hcDist = hasHc ? hc.distances : null;
225
+ const hcRow = hasHc ? hcCoord * hc_pdb_js_1.HC_PDB_MOVES.length : 0;
226
+ // Byte-expanded (tests/generators) vs nibble-packed (loaders) distances.
227
+ const edgeUnpacked = edge3Distances.length >= 31006080;
75
228
  for (let m = 0; m < 17; m++) {
76
229
  if (exports.ckmv3[lm][m]) {
77
230
  m = exports.skipAxis3[m];
78
231
  continue;
79
232
  }
80
- const nc = center3Tables.moveTable[ct * center3_js_1.CENTER3_MOVES.length + m];
233
+ const nc = c3Move[c3Row + m];
81
234
  if (nc < 0)
82
235
  continue;
83
- const prun1 = center3Tables.pruning[nc];
236
+ const prun1 = c3Prun[nc];
84
237
  if (prun1 >= maxl) {
238
+ this.noteExcess(depth, prun1, prun1 > maxl && m < 14);
85
239
  if (prun1 > maxl && m < 14)
86
240
  m = exports.skipAxis3[m];
87
241
  continue;
88
242
  }
89
- const symIdx = (0, edge3_js_1.getMvSymFrom)(ep, ep, m, this.sym.sym2Raw, this.sym.sym2Mask, this.sym.raw2Sym, this.sym.mvrot, this.sym.mvroto);
90
- const prunx = this.getPrun(edge3Distances, symIdx >> 3);
243
+ let nhc = -1;
244
+ if (hasHc) {
245
+ nhc = hcMoves[hcRow + m];
246
+ if (nhc >= 0) {
247
+ const prunh = hcDist[nhc];
248
+ if (prunh >= maxl) {
249
+ this.noteExcess(depth, prunh, prunh > maxl && m < 14);
250
+ if (prunh > maxl && m < 14)
251
+ m = exports.skipAxis3[m];
252
+ continue;
253
+ }
254
+ }
255
+ }
256
+ if (c3PairDist && depth === 0) {
257
+ const c3Base = nc * 144;
258
+ const fSlot = F_SLOT_MAP[m];
259
+ const hSlot = H_SLOT_MAP[m];
260
+ let prunPair = 0;
261
+ for (let s = 0; s < 12; s++) {
262
+ const idx = c3Base + fSlot[s] * 12 + hSlot[ep[s]];
263
+ const d = (idx & 1) === 0 ? (c3PairDist[idx >> 1] & 0x0f) : (c3PairDist[idx >> 1] >> 4);
264
+ if (d >= maxl) {
265
+ prunPair = d;
266
+ break;
267
+ }
268
+ if (d > prunPair)
269
+ prunPair = d;
270
+ }
271
+ if (prunPair >= maxl) {
272
+ this.noteExcess(depth, prunPair, prunPair > maxl && m < 14);
273
+ if (prunPair > maxl && m < 14)
274
+ m = exports.skipAxis3[m];
275
+ continue;
276
+ }
277
+ }
278
+ const symIdx = (0, edge3_js_1.getMvSymFrom)(ep, ep, m, sym2Raw, sym2Mask, raw2Sym, symMvrot, symMvroto);
279
+ // Format-aware read: loaders keep the nibble-packed table (15.5MB);
280
+ // tests and generators may pass byte-expanded buffers instead.
281
+ const eIdx = symIdx >> 3;
282
+ const prunx = edgeUnpacked
283
+ ? edge3Distances[eIdx]
284
+ : (edge3Distances[eIdx >> 1] >> ((eIdx & 1) * 4)) & 0x0f;
91
285
  if (prunx >= maxl) {
286
+ this.noteExcess(depth, prunx, prunx > maxl && m < 14);
92
287
  if (prunx > maxl && m < 14)
93
288
  m = exports.skipAxis3[m];
94
289
  continue;
95
290
  }
96
- if (await this.search(ep, nc, prunx, maxl - 1, m, depth + 1, center3Tables, edge3Distances)) {
97
- this.moveLog[depth] = m;
291
+ this.moveLog[depth] = m;
292
+ if (this.search(ep, nc, prunx, maxl - 1, m, depth + 1, center3Tables, edge3Distances, maxNodes, collect, hc, nhc, c3PairDist)) {
98
293
  return true;
99
294
  }
100
295
  }
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;
302
+ }
101
303
  return false;
102
304
  }
103
305
  /**
104
- * Search for a phase-3 solution with exact target length.
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
312
+ * `childBound - 1` by the triangle inequality on the exact PDBs, so the
313
+ * recorded excess `depth + childBound` stays a true lower bound.
105
314
  */
106
- async solveLength(edge, ct3, edist, length3, center3Tables, edge3Distances) {
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
+ /**
321
+ * Search for a phase-3 solution with exact target length (first found).
322
+ */
323
+ solveLength(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, hc, hcCoord = -1, c3PairDist) {
107
324
  this.tempep[0].set(edge);
108
- if (await this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances)) {
325
+ this.nodeCount = 0;
326
+ if (this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, undefined, hc, hcCoord, c3PairDist)) {
109
327
  return this.moveLog.slice(0, length3).map((m) => exports.SEARCH_MOVES[m]);
110
328
  }
111
329
  return null;
112
330
  }
331
+ /**
332
+ * Search for a phase-3 solution with exact target length, accepting the
333
+ * first goal sequence that passes `validate` (up to `limit` examined,
334
+ * sharing `maxNodes`). With a validator that enforces the downstream
335
+ * requirements (reduction + standard orientation + 3x3 conversion), hard
336
+ * states whose first-found solutions are unusable get more draws from
337
+ * the same budget instead of an immediate miss.
338
+ */
339
+ solveLengthValid(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, limit = 16, validate, hc, hcCoord = -1, c3PairDist) {
340
+ return this.solveLengthProven(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes, limit, validate, hc, hcCoord, c3PairDist).moves;
341
+ }
342
+ /**
343
+ * Exact-length validated search that also reports what the call proved.
344
+ * `provenThrough` is the greatest length with provably no (center, edge)
345
+ * solution: set only when the tree was exhaustively explored with no goal
346
+ * reached, no node-cap abort, no TT hit, and complete excess info — the
347
+ * caller may then skip every shorter length for this candidate. Otherwise
348
+ * it is `length3 - 1` at most (only the attempted length is settled).
349
+ */
350
+ solveLengthProven(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, limit = 16, validate, hc, hcCoord = -1, c3PairDist) {
351
+ if (c3PairDist) {
352
+ const c3Base = ct3 * 144;
353
+ let maxPairDist = 0;
354
+ for (let s = 0; s < 12; s++) {
355
+ const idx = c3Base + s * 12 + edge[s];
356
+ const d = (idx & 1) === 0 ? (c3PairDist[idx >> 1] & 0x0f) : (c3PairDist[idx >> 1] >> 4);
357
+ if (d > maxPairDist)
358
+ maxPairDist = d;
359
+ }
360
+ if (maxPairDist > length3) {
361
+ return { moves: null, provenThrough: maxPairDist - 1, nodesUsed: 0 };
362
+ }
363
+ }
364
+ this.tempep[0].set(edge);
365
+ this.nodeCount = 0;
366
+ this.capped = false;
367
+ this.limitHit = false;
368
+ this.sawGoal = false;
369
+ this.ttTouched = false;
370
+ this.minExcess = Number.MAX_SAFE_INTEGER;
371
+ this.callId = FastPhase3Solver.nextCallId();
372
+ let accepted = null;
373
+ const collect = {
374
+ limit,
375
+ examined: 0,
376
+ validate: (moves) => {
377
+ if (validate(moves)) {
378
+ accepted = moves;
379
+ return true;
380
+ }
381
+ if (!accepted)
382
+ accepted = moves;
383
+ return false;
384
+ }
385
+ };
386
+ this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, collect, hc, hcCoord, c3PairDist);
387
+ if (accepted)
388
+ return { moves: accepted, provenThrough: length3, nodesUsed: this.nodeCount };
389
+ if (process.env.CUBE4_PROOF_STATS === "1") {
390
+ exports.proofStats.total++;
391
+ if (this.capped)
392
+ exports.proofStats.capped++;
393
+ if (this.limitHit)
394
+ exports.proofStats.limitHit++;
395
+ if (this.sawGoal)
396
+ exports.proofStats.sawGoal++;
397
+ if (this.ttTouched)
398
+ exports.proofStats.ttTouched++;
399
+ }
400
+ if (!this.capped &&
401
+ !this.limitHit &&
402
+ !this.sawGoal &&
403
+ !this.ttTouched &&
404
+ this.minExcess > length3 &&
405
+ this.minExcess !== Number.MAX_SAFE_INTEGER) {
406
+ if (process.env.CUBE4_PROOF_STATS === "1") {
407
+ exports.proofStats.clean++;
408
+ exports.proofStats.jumpSum += this.minExcess - 1 - length3;
409
+ }
410
+ return { moves: null, provenThrough: this.minExcess - 1, nodesUsed: this.nodeCount };
411
+ }
412
+ return { moves: null, provenThrough: length3, nodesUsed: this.nodeCount };
413
+ }
113
414
  }
114
415
  exports.FastPhase3Solver = FastPhase3Solver;
416
+ /** Temporary instrumentation for proof-statistics (CUBE4_PROOF_STATS=1). */
417
+ exports.proofStats = {
418
+ total: 0,
419
+ capped: 0,
420
+ limitHit: 0,
421
+ sawGoal: 0,
422
+ ttTouched: 0,
423
+ clean: 0,
424
+ jumpSum: 0
425
+ };
426
+ function resetProofStats() {
427
+ exports.proofStats.total = 0;
428
+ exports.proofStats.capped = 0;
429
+ exports.proofStats.limitHit = 0;
430
+ exports.proofStats.sawGoal = 0;
431
+ exports.proofStats.ttTouched = 0;
432
+ exports.proofStats.clean = 0;
433
+ exports.proofStats.jumpSum = 0;
434
+ }
115
435
  const defaultSolver = new FastPhase3Solver();
116
436
  /**
117
437
  * Solve Phase 3 with iterative deepening on a single Phase3Start.