@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.
- package/README.md +15 -4
- package/dist/core/api.d.ts.map +1 -1
- package/dist/core/api.js +10 -2
- package/dist/puzzles/cube2/solver/browser-loader.d.ts +4 -7
- package/dist/puzzles/cube2/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/browser-loader.js +42 -14
- package/dist/puzzles/cube2/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/generated-assets.js +8 -3
- package/dist/puzzles/cube3/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/browser-loader.js +42 -20
- package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
- package/dist/puzzles/cube3/solver/phase1.d.ts +19 -0
- package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase1.js +29 -6
- package/dist/puzzles/cube3/solver/phase2.d.ts +28 -0
- package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase2.js +56 -15
- package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/tables.js +14 -12
- package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
- package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
- package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
- package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
- package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
- package/dist/puzzles/cube4/browser-worker.js +31 -0
- package/dist/puzzles/cube4/index.d.ts +7 -0
- package/dist/puzzles/cube4/index.d.ts.map +1 -1
- package/dist/puzzles/cube4/index.js +4 -0
- package/dist/puzzles/cube4/moves.d.ts.map +1 -1
- package/dist/puzzles/cube4/moves.js +122 -30
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts +26 -1
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/browser-loader.js +212 -48
- package/dist/puzzles/cube4/reduction/center2.d.ts +24 -0
- package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/center2.js +99 -1
- package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
- package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/finish3.js +393 -0
- package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +5 -3
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/hc-pdb.js +30 -12
- package/dist/puzzles/cube4/reduction/node-loader.d.ts +10 -1
- package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/node-loader.js +170 -36
- package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
- package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts +33 -30
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/phase3-search.js +211 -104
- package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
- package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
- package/dist/puzzles/cube4/reduction/solve.d.ts +72 -13
- package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/solve.js +374 -92
- package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
- package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
- package/dist/puzzles/cube4/scramble-cache.js +150 -0
- package/dist/puzzles/cube4/scrambler.d.ts +112 -18
- package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
- package/dist/puzzles/cube4/scrambler.js +271 -83
- package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
- package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
- package/dist/puzzles/cube4/worker-pool.js +405 -0
- package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
- package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
- package/dist/puzzles/cube4/worker-protocol.js +58 -0
- package/dist/puzzles/cube4/worker.d.ts +26 -0
- package/dist/puzzles/cube4/worker.d.ts.map +1 -0
- package/dist/puzzles/cube4/worker.js +49 -0
- package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
- package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
- package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
- package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
- package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
- package/dist/puzzles/shared/compressed-fetch.js +226 -31
- package/dist/puzzles/shared/load-once.d.ts +16 -0
- package/dist/puzzles/shared/load-once.d.ts.map +1 -0
- package/dist/puzzles/shared/load-once.js +11 -0
- package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
- package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
- package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
- package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
- package/dist-cjs/core/api.js +10 -2
- package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
- package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
- package/dist-cjs/puzzles/cube3/solver/browser-loader.js +42 -20
- package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
- package/dist-cjs/puzzles/cube3/solver/phase1.js +29 -6
- package/dist-cjs/puzzles/cube3/solver/phase2.js +56 -15
- package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
- package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
- package/dist-cjs/puzzles/cube4/index.js +21 -1
- package/dist-cjs/puzzles/cube4/moves.js +122 -30
- package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +212 -48
- package/dist-cjs/puzzles/cube4/reduction/center2.js +102 -2
- package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
- package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +31 -12
- package/dist-cjs/puzzles/cube4/reduction/node-loader.js +171 -36
- package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
- package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +211 -104
- package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
- package/dist-cjs/puzzles/cube4/reduction/solve.js +374 -88
- package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
- package/dist-cjs/puzzles/cube4/scrambler.js +271 -80
- package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
- package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
- package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
- package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
- package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
- package/dist-cjs/puzzles/shared/load-once.js +14 -0
- package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
- package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
- package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
- package/docs/development.md +19 -7
- 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^
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
const id =
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|
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
|
|
137
|
-
*
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
198
|
-
const storedLm = (storedInfo
|
|
199
|
-
const storedDepth = storedInfo &
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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 (
|
|
225
|
-
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
327
|
+
m = skipAxis3Fast[m];
|
|
270
328
|
continue;
|
|
271
329
|
}
|
|
272
330
|
}
|
|
273
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
302
|
-
* `childBound` more moves, so no solution exists
|
|
303
|
-
* below root threshold `depth + 1 + childBound`. When a
|
|
304
|
-
* skip also fires, the skipped siblings are same-face turns
|
|
305
|
-
* every non-empty skipAxis3 run under m < 14 is a same-base
|
|
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.
|
|
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
|
-
|
|
377
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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"}
|