@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
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* The scramble for a sampled state is the inverted solution.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.stagnationStats = exports.f2Stats = exports.P2_CANCEL = void 0;
|
|
8
|
+
exports.rotStats = exports.finishStats = exports.stagnationStats = exports.f2Stats = exports.P2_CANCEL = exports.Cube4FinishCapError = exports.Cube4Phase3CapError = exports.Cube4Phase2CapError = void 0;
|
|
9
9
|
exports.rotateAlgorithm = rotateAlgorithm;
|
|
10
|
+
exports.cubeRotationMoves = cubeRotationMoves;
|
|
10
11
|
exports.invertRotationSequence = invertRotationSequence;
|
|
11
12
|
exports.applyCubeRotation = applyCubeRotation;
|
|
12
13
|
exports.resetF2Stats = resetF2Stats;
|
|
@@ -20,6 +21,8 @@ const pairing_coordinate_js_1 = require("./pairing-coordinate.js");
|
|
|
20
21
|
const hc_pdb_js_1 = require("./hc-pdb.js");
|
|
21
22
|
const edge3_js_1 = require("./edge3.js");
|
|
22
23
|
const phase3_search_js_1 = require("./phase3-search.js");
|
|
24
|
+
const rotation_fix_js_1 = require("./rotation-fix.js");
|
|
25
|
+
const pair_transversal_js_1 = require("./pair-transversal.js");
|
|
23
26
|
const phase4_js_1 = require("./phase4.js");
|
|
24
27
|
const coordinates_js_1 = require("../../cube3/solver/coordinates.js");
|
|
25
28
|
const state_js_1 = require("../state.js");
|
|
@@ -113,6 +116,32 @@ function getTriAxialCube3Heuristic(standardReduced, tables) {
|
|
|
113
116
|
}
|
|
114
117
|
return minH;
|
|
115
118
|
}
|
|
119
|
+
class Cube4SolveError extends Error {
|
|
120
|
+
}
|
|
121
|
+
class Cube4Phase2CapError extends Cube4SolveError {
|
|
122
|
+
code = "cube4-phase2-cap";
|
|
123
|
+
constructor(message) {
|
|
124
|
+
super(message);
|
|
125
|
+
this.name = "Cube4Phase2CapError";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
exports.Cube4Phase2CapError = Cube4Phase2CapError;
|
|
129
|
+
class Cube4Phase3CapError extends Cube4SolveError {
|
|
130
|
+
code = "cube4-phase3-cap";
|
|
131
|
+
constructor(message) {
|
|
132
|
+
super(message);
|
|
133
|
+
this.name = "Cube4Phase3CapError";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.Cube4Phase3CapError = Cube4Phase3CapError;
|
|
137
|
+
class Cube4FinishCapError extends Cube4SolveError {
|
|
138
|
+
code = "cube4-finish-cap";
|
|
139
|
+
constructor(message) {
|
|
140
|
+
super(message);
|
|
141
|
+
this.name = "Cube4FinishCapError";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.Cube4FinishCapError = Cube4FinishCapError;
|
|
116
145
|
function cornerPermutationParity(cp) {
|
|
117
146
|
let parity = 0;
|
|
118
147
|
const seen = new Array(8).fill(false);
|
|
@@ -192,6 +221,8 @@ const VALID_24_ORIENTATIONS = (() => {
|
|
|
192
221
|
}
|
|
193
222
|
return map;
|
|
194
223
|
})();
|
|
224
|
+
/** Centers of the solved state — used to verify a full (not just partial) standard orientation. */
|
|
225
|
+
const SOLVED_CENTERS = (0, state_js_1.createSolvedCube4State)().centers;
|
|
195
226
|
const ROT_MAP = {
|
|
196
227
|
"x": { "U": "B", "R": "R", "F": "U", "D": "F", "L": "L", "B": "D" },
|
|
197
228
|
"x'": { "U": "F", "R": "R", "F": "D", "D": "B", "L": "L", "B": "U" },
|
|
@@ -227,6 +258,10 @@ function rotateAlgorithm(alg, rotSeq) {
|
|
|
227
258
|
return current;
|
|
228
259
|
});
|
|
229
260
|
}
|
|
261
|
+
/** Physical wide-move sequence implementing a whole-cube rotation. */
|
|
262
|
+
function cubeRotationMoves(rotSeq) {
|
|
263
|
+
return rotSeq.flatMap((rotation) => ROT_MOVES[rotation]);
|
|
264
|
+
}
|
|
230
265
|
function invertRotationSequence(rotSeq) {
|
|
231
266
|
return [...rotSeq].reverse().map((r) => INVERT_ROT[r]);
|
|
232
267
|
}
|
|
@@ -267,6 +302,49 @@ function isCancelled(previous, current) {
|
|
|
267
302
|
exports.P2_CANCEL = center2_js_1.CENTER2_SEARCH_MOVES.map((previous) => center2_js_1.CENTER2_SEARCH_MOVES.map((current) => isCancelled(previous, current)));
|
|
268
303
|
// Same over the wide 28-move set (used when `center2Wide` is set).
|
|
269
304
|
const P2_CANCEL_WIDE = center2_js_1.CENTER2_SEARCH_MOVES_WIDE.map((previous) => center2_js_1.CENTER2_SEARCH_MOVES_WIDE.map((current) => isCancelled(previous, current)));
|
|
305
|
+
/** Flat (row-major) cancel tables for the hot DFS loop; same semantics. */
|
|
306
|
+
function flattenCancel(table) {
|
|
307
|
+
const n = table.length;
|
|
308
|
+
const flat = new Uint8Array(n * n);
|
|
309
|
+
for (let i = 0; i < n; i++) {
|
|
310
|
+
const row = table[i];
|
|
311
|
+
for (let j = 0; j < n; j++)
|
|
312
|
+
flat[i * n + j] = row[j] ? 1 : 0;
|
|
313
|
+
}
|
|
314
|
+
return flat;
|
|
315
|
+
}
|
|
316
|
+
const P2_CANCEL_FLAT = flattenCancel(exports.P2_CANCEL);
|
|
317
|
+
const P2_CANCEL_WIDE_FLAT = flattenCancel(P2_CANCEL_WIDE);
|
|
318
|
+
/**
|
|
319
|
+
* Wing transversal mask: bit k set iff pair k occupies a second-half slot.
|
|
320
|
+
* `hasPairingTransversal(wp)` is equivalent to `pairingMaskOf(wp) === 0xfff`
|
|
321
|
+
* (12 slots, 12 pair ids in 0..11: all distinct ⟺ all 12 bits set). The mask
|
|
322
|
+
* form is O(1) at the millions of phase-2 goal leaves.
|
|
323
|
+
*/
|
|
324
|
+
function pairingMaskOf(wp) {
|
|
325
|
+
let mask = 0;
|
|
326
|
+
for (let i = 0; i < 12; i++) {
|
|
327
|
+
mask |= 1 << (wp[pairing_coordinate_js_1.PAIRING_SECOND_HALVES[i]] >> 1);
|
|
328
|
+
}
|
|
329
|
+
return mask;
|
|
330
|
+
}
|
|
331
|
+
/** rl side of the phase-2 goal test (same flags as `isCenter2Goal`). */
|
|
332
|
+
const P2_GOAL_RL = (() => {
|
|
333
|
+
const flags = new Uint8Array(center2_js_1.CENTER2_RL_STATES);
|
|
334
|
+
for (const rl of center2_js_1.CENTER2_GOAL_RL)
|
|
335
|
+
flags[rl] = 1;
|
|
336
|
+
return flags;
|
|
337
|
+
})();
|
|
338
|
+
/** Chiral phase-2 tables, generated once per loaded center2 table set. */
|
|
339
|
+
const chiralTablesCache = new WeakMap();
|
|
340
|
+
function getCenter2ChiralTables(center2) {
|
|
341
|
+
let chiral = chiralTablesCache.get(center2);
|
|
342
|
+
if (!chiral) {
|
|
343
|
+
chiral = (0, center2_js_1.generateCenter2ChiralTables)(center2.rlMove);
|
|
344
|
+
chiralTablesCache.set(center2, chiral);
|
|
345
|
+
}
|
|
346
|
+
return chiral;
|
|
347
|
+
}
|
|
270
348
|
const SKIP_AXIS_2 = new Int32Array(center2_js_1.CENTER2_SEARCH_MOVES.length);
|
|
271
349
|
for (let i = 0; i < center2_js_1.CENTER2_SEARCH_MOVES.length; i++) {
|
|
272
350
|
const m1 = center2_js_1.CENTER2_SEARCH_MOVES[i];
|
|
@@ -283,7 +361,6 @@ for (let i = 0; i < center2_js_1.CENTER2_SEARCH_MOVES.length; i++) {
|
|
|
283
361
|
}
|
|
284
362
|
}
|
|
285
363
|
}
|
|
286
|
-
const phase3Solver = new phase3_search_js_1.FastPhase3Solver();
|
|
287
364
|
/** Temporary instrumentation for F2 goal statistics (CUBE4_F2_STATS=1). */
|
|
288
365
|
exports.f2Stats = { goals: 0, transversalPass: 0, axisRejects: 0, minTransversalL2: 99, minChiralL2: 99 };
|
|
289
366
|
function resetF2Stats() {
|
|
@@ -299,13 +376,38 @@ function resetStagnationStats() {
|
|
|
299
376
|
exports.stagnationStats.checks = 0;
|
|
300
377
|
exports.stagnationStats.cuts = 0;
|
|
301
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Temporary instrumentation for the finish-skip bound (CUBE4_FINISH_STATS=1):
|
|
381
|
+
* `gap = totalEst - finalLen` per finished reduction. A skip
|
|
382
|
+
* `totalEst - SLACK >= minTotalMoves` is only sound while SLACK covers the
|
|
383
|
+
* observed gap maximum (junction cancellation + finish-vs-h3 slack).
|
|
384
|
+
*/
|
|
385
|
+
exports.finishStats = {
|
|
386
|
+
gapMin: 99,
|
|
387
|
+
gapMax: -99,
|
|
388
|
+
gapSum: 0,
|
|
389
|
+
gapN: 0,
|
|
390
|
+
finishes: 0,
|
|
391
|
+
skipped: 0
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Temporary instrumentation for the rotMoves cost (CUBE4_ROT_STATS=1):
|
|
395
|
+
* per built reduction records the prep frame rotation, the derived
|
|
396
|
+
* normalization rotation and the emitted fix length.
|
|
397
|
+
*/
|
|
398
|
+
exports.rotStats = [];
|
|
302
399
|
async function solveCube4State(state, tables, solveCube3, options) {
|
|
303
400
|
// NOTE: no wall-clock deadline inside the search — all loops are bounded
|
|
304
|
-
// by deterministic work caps (node counts, candidate counts, length
|
|
305
|
-
//
|
|
306
|
-
// its own backstop (`timeoutMs` handled at the call site).
|
|
401
|
+
// by deterministic work caps (node counts, candidate counts, length caps),
|
|
402
|
+
// so the same state always takes the same path.
|
|
307
403
|
const sym = (0, edge3_js_1.getSymTables)();
|
|
404
|
+
// All mutable phase-3 state (TT, epoch-node count, call IDs and search
|
|
405
|
+
// stacks) belongs to this invocation. The same instance spans its
|
|
406
|
+
// deterministic restarts, retaining useful goal-free TT entries without
|
|
407
|
+
// allowing concurrent solves to contaminate one another.
|
|
408
|
+
const phase3Solver = new phase3_search_js_1.FastPhase3Solver();
|
|
308
409
|
phase3Solver.resetEpochNodes();
|
|
410
|
+
const collectF2Stats = typeof process !== "undefined" && process.env?.CUBE4_F2_STATS === "1";
|
|
309
411
|
const totalRestarts = Math.max(1, Math.floor(options?.restarts ?? 1));
|
|
310
412
|
// Cross-restart work accumulators (reported in nodes/timings).
|
|
311
413
|
let accP2 = 0;
|
|
@@ -330,8 +432,8 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
330
432
|
const path = [];
|
|
331
433
|
const p1Extra = options?.phase1ExtraDepths ?? 3;
|
|
332
434
|
const p1Cap = options?.phase1Cap ?? 400;
|
|
333
|
-
// Per-restart DFS traversal order (identity when rng is null).
|
|
334
|
-
const p1Order = restartOrder(center1_js_1.CENTER1_MOVES.length, rng);
|
|
435
|
+
// Per-restart DFS traversal order (null = identity fast path when rng is null).
|
|
436
|
+
const p1Order = rng === null ? null : Int32Array.from(restartOrder(center1_js_1.CENTER1_MOVES.length, rng));
|
|
335
437
|
for (let maxD = minOverallH; maxD <= minOverallH + p1Extra; maxD++) {
|
|
336
438
|
for (const urf of [0, 1, 2]) {
|
|
337
439
|
const minH = urfDistances[urf];
|
|
@@ -368,7 +470,7 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
368
470
|
if (depth + h > maxD)
|
|
369
471
|
return false;
|
|
370
472
|
for (let mi = 0; mi < center1_js_1.CENTER1_MOVES.length; mi++) {
|
|
371
|
-
const mv = p1Order[mi];
|
|
473
|
+
const mv = p1Order === null ? mi : p1Order[mi];
|
|
372
474
|
if (lm >= 0 && Math.floor(lm / 3) === Math.floor(mv / 3))
|
|
373
475
|
continue;
|
|
374
476
|
const next = (0, center1_js_1.applyCenter1Move)(m, tables.center1.moveMaps, mv);
|
|
@@ -387,12 +489,24 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
387
489
|
const minP1 = p1Sols[0]?.moves.length ?? 0;
|
|
388
490
|
const tF1End = performance.now();
|
|
389
491
|
const candidates = [];
|
|
390
|
-
const moveLog = new
|
|
492
|
+
const moveLog = new Int32Array(48);
|
|
391
493
|
// Incremental wing tracking stack for phase-2 goal transversal checks:
|
|
392
494
|
// avoids full 6-7 move replaying from scratch across hundreds of thousands of candidate goals.
|
|
393
|
-
|
|
495
|
+
// `wpMaskStack[d]` = pairingMaskOf(wpStack[d]) maintained in the same lazy
|
|
496
|
+
// fill, so the per-leaf transversal test is a single compare.
|
|
497
|
+
const wpStack = Array.from({ length: 32 }, () => new Uint8Array(24));
|
|
498
|
+
const wpMaskStack = new Int32Array(32);
|
|
394
499
|
let validWpDepth = 0;
|
|
395
500
|
const wingMaps = (0, center2_js_1.getWingMoveMaps)();
|
|
501
|
+
// Transversal pruning (see pair-transversal.ts): wing positions per DFS
|
|
502
|
+
// depth (`posStack[d][wing]`), checked against the two-pair PDB over 6
|
|
503
|
+
// disjoint pair-of-pairs in the last TRANSVERSAL_DEPTH levels (the PDB's
|
|
504
|
+
// maximum). Prunes only branches with no transversal goal, so the
|
|
505
|
+
// accepted candidates are unchanged.
|
|
506
|
+
const posStack = Array.from({ length: 33 }, () => new Uint8Array(24));
|
|
507
|
+
const wingPosMove = (0, pair_transversal_js_1.getWingPositionMoves)();
|
|
508
|
+
const TRANSVERSAL_DEPTH = 3;
|
|
509
|
+
let validPosDepth = 0;
|
|
396
510
|
/**
|
|
397
511
|
* Anti-hang work cap for the phase-2 search (successful attempts use
|
|
398
512
|
* ~0.1-0.3M nodes; this only aborts pathological explorations).
|
|
@@ -405,11 +519,21 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
405
519
|
// indices in either mode.
|
|
406
520
|
const c2Wide = options?.center2Wide === true;
|
|
407
521
|
const C2_SEARCH = c2Wide ? center2_js_1.CENTER2_SEARCH_MOVES_WIDE : center2_js_1.CENTER2_SEARCH_MOVES;
|
|
408
|
-
const
|
|
409
|
-
|
|
522
|
+
const cancelFlat = c2Wide ? P2_CANCEL_WIDE_FLAT : P2_CANCEL_FLAT;
|
|
523
|
+
const c2Stride = C2_SEARCH.length;
|
|
524
|
+
const transversalPdb = (0, pair_transversal_js_1.getTransversalPairPdb)(C2_SEARCH.length);
|
|
525
|
+
const canSkipAxis = rng === null && !c2Wide;
|
|
526
|
+
// Per-restart child order (null = identity fast path). Entries stay
|
|
410
527
|
// original move-table indices, so all table lookups are unaffected.
|
|
411
|
-
const p2Order = restartOrder(C2_SEARCH.length, rng);
|
|
412
|
-
|
|
528
|
+
const p2Order = rng === null ? null : Int32Array.from(restartOrder(C2_SEARCH.length, rng));
|
|
529
|
+
// Default set: chiral tables (goal = ct == 0 && axisDet == 1 exactly, so
|
|
530
|
+
// wrong-chirality goal paths are pruned instead of enumerated). The wide
|
|
531
|
+
// set keeps the relative tables + the axisDet filter at the goal.
|
|
532
|
+
const chiral = c2Wide ? null : getCenter2ChiralTables(tables.center2);
|
|
533
|
+
const c2ctMove = chiral ? chiral.ctMove : tables.center2.ctMove;
|
|
534
|
+
const c2rlMove = tables.center2.rlMove;
|
|
535
|
+
const c2GoalCt = chiral ? chiral.goalCt : 0;
|
|
536
|
+
let c2pruning = chiral ? chiral.pruning : tables.center2.pruning;
|
|
413
537
|
if (c2Wide) {
|
|
414
538
|
if (!tables.center2.pruningWide) {
|
|
415
539
|
throw new Error("4x4 solve: center2Wide requested but pruningWide tables are not loaded — regenerate center2 tables.");
|
|
@@ -417,6 +541,8 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
417
541
|
c2pruning = tables.center2.pruningWide;
|
|
418
542
|
}
|
|
419
543
|
const P2_MAX_NODES = deep ? 24_000_000 : 8_000_000;
|
|
544
|
+
// Hoisted out of the goal path (was read per goal leaf).
|
|
545
|
+
const maxP1Cand = options?.p1CandCap !== undefined ? options.p1CandCap : (deep ? 24 : 16);
|
|
420
546
|
// Smaller pool + shorter extension (was 180/100/14/12 on the fast path):
|
|
421
547
|
// the deep length2 levels are where F2 tails burn. Validated on a 44-state
|
|
422
548
|
// corpus: -16% nodes, identical average length (one +1 in 44).
|
|
@@ -430,11 +556,16 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
430
556
|
const EXT_MX = deep ? 20 : 16;
|
|
431
557
|
let p2nodes = 0;
|
|
432
558
|
let f3calls = 0;
|
|
559
|
+
// Bound the whole sweep, not just each individual IDA* call. A hard
|
|
560
|
+
// candidate pool can otherwise spend thousands of capped calls before
|
|
561
|
+
// reaching the deep retry (corpus-73: 114M nodes in the fast attempt).
|
|
562
|
+
const phase3StartNodes = phase3Solver.epochNodes;
|
|
563
|
+
const phase3NodeBudget = deep ? 16_000_000 : 4_000_000;
|
|
433
564
|
function searchPhase2(c, r, maxl, lm, depth, bridgeState, prep) {
|
|
434
565
|
p2nodes++;
|
|
435
566
|
if (p2nodes > P2_MAX_NODES)
|
|
436
567
|
return false;
|
|
437
|
-
if (
|
|
568
|
+
if (c === c2GoalCt && P2_GOAL_RL[r] === 1) {
|
|
438
569
|
if (maxl === 0) {
|
|
439
570
|
while (validWpDepth < depth) {
|
|
440
571
|
const m = moveLog[validWpDepth];
|
|
@@ -443,30 +574,34 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
443
574
|
const dst = wpStack[validWpDepth + 1];
|
|
444
575
|
for (let i = 0; i < 24; i++)
|
|
445
576
|
dst[i] = src[wingMaps[base + i]];
|
|
577
|
+
wpMaskStack[validWpDepth + 1] = pairingMaskOf(dst);
|
|
446
578
|
validWpDepth++;
|
|
447
579
|
}
|
|
448
|
-
|
|
449
|
-
if (process.env.CUBE4_F2_STATS === "1")
|
|
580
|
+
if (collectF2Stats)
|
|
450
581
|
exports.f2Stats.goals++;
|
|
451
|
-
|
|
582
|
+
// Bit-identical to `hasPairingTransversal(goalWp)`: all 12 pair bits
|
|
583
|
+
// distinct ⟺ the 12-bit mask is full (0xfff).
|
|
584
|
+
if (wpMaskStack[depth] !== 0xfff)
|
|
452
585
|
return false;
|
|
453
|
-
if (
|
|
586
|
+
if (collectF2Stats)
|
|
454
587
|
exports.f2Stats.transversalPass++;
|
|
455
|
-
const moves2 =
|
|
588
|
+
const moves2 = [];
|
|
589
|
+
for (let i = 0; i < depth; i++)
|
|
590
|
+
moves2.push(center2_js_1.CENTER2_MOVES[moveLog[i]]);
|
|
456
591
|
const s2 = (0, moves_js_1.applyCube4Algorithm)(bridgeState, moves2);
|
|
457
592
|
const uAxis = s2.centers[0] % 3;
|
|
458
593
|
const rAxis = s2.centers[4] % 3;
|
|
459
594
|
const fAxis = s2.centers[8] % 3;
|
|
460
|
-
if (
|
|
595
|
+
if (collectF2Stats) {
|
|
461
596
|
if (depth < exports.f2Stats.minTransversalL2)
|
|
462
597
|
exports.f2Stats.minTransversalL2 = depth;
|
|
463
598
|
}
|
|
464
599
|
if (axisDet(uAxis, rAxis, fAxis) !== 1) {
|
|
465
|
-
if (
|
|
600
|
+
if (collectF2Stats)
|
|
466
601
|
exports.f2Stats.axisRejects++;
|
|
467
602
|
return false;
|
|
468
603
|
}
|
|
469
|
-
if (
|
|
604
|
+
if (collectF2Stats) {
|
|
470
605
|
if (depth < exports.f2Stats.minChiralL2)
|
|
471
606
|
exports.f2Stats.minChiralL2 = depth;
|
|
472
607
|
}
|
|
@@ -486,7 +621,7 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
486
621
|
const moves2Remapped = prep.totalRot.length === 0
|
|
487
622
|
? moves2
|
|
488
623
|
: rotateAlgorithm(moves2, invertRotationSequence(prep.totalRot));
|
|
489
|
-
const hcCoord = tables.hc ? tables.hc
|
|
624
|
+
const hcCoord = tables.hc ? (0, hc_pdb_js_1.hcDenseIndex)(tables.hc, (0, hc_pdb_js_1.encodeHcPdb)(s2.centers)) : -1;
|
|
490
625
|
const hcRaw = hcCoord >= 0 && tables.hc ? tables.hc.distances[hcCoord] : 0;
|
|
491
626
|
const hdist = hcRaw === 0x0f ? 0 : hcRaw;
|
|
492
627
|
let pdist = 0;
|
|
@@ -533,7 +668,6 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
533
668
|
}
|
|
534
669
|
}
|
|
535
670
|
p1CandCount++;
|
|
536
|
-
const maxP1Cand = options?.p1CandCap !== undefined ? options.p1CandCap : (deep ? 96 : 64);
|
|
537
671
|
if (p1CandCount >= maxP1Cand)
|
|
538
672
|
return true;
|
|
539
673
|
}
|
|
@@ -543,27 +677,65 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
543
677
|
}
|
|
544
678
|
if (maxl === 0)
|
|
545
679
|
return false;
|
|
546
|
-
const c2ctMove = tables.center2.ctMove;
|
|
547
|
-
const c2rlMove = tables.center2.rlMove;
|
|
548
680
|
const c2RowC = c * center2_js_1.CENTER2_MOVES.length;
|
|
549
681
|
const c2RowR = r * center2_js_1.CENTER2_MOVES.length;
|
|
550
682
|
for (let mi = 0; mi < C2_SEARCH.length; mi++) {
|
|
551
|
-
const m = p2Order[mi];
|
|
552
|
-
if (lm >= 0 &&
|
|
683
|
+
const m = p2Order === null ? mi : p2Order[mi];
|
|
684
|
+
if (lm >= 0 && cancelFlat[lm * c2Stride + m]) {
|
|
553
685
|
continue;
|
|
554
686
|
}
|
|
555
687
|
const nc = c2ctMove[c2RowC + m];
|
|
556
688
|
const nr = c2rlMove[c2RowR + m];
|
|
557
689
|
const prun = c2pruning[nc * center2_js_1.CENTER2_RL_STATES + nr];
|
|
558
690
|
if (prun >= maxl) {
|
|
559
|
-
if (prun > maxl &&
|
|
691
|
+
if (prun > maxl && canSkipAxis) {
|
|
560
692
|
mi = SKIP_AXIS_2[m] - 1;
|
|
561
693
|
}
|
|
562
694
|
continue;
|
|
563
695
|
}
|
|
696
|
+
let childPosValid = false;
|
|
697
|
+
if (maxl - 1 < TRANSVERSAL_DEPTH) {
|
|
698
|
+
// Lazy fill (like wpStack): replay the path down to this node.
|
|
699
|
+
while (validPosDepth < depth) {
|
|
700
|
+
const src = posStack[validPosDepth];
|
|
701
|
+
const dst = posStack[validPosDepth + 1];
|
|
702
|
+
const row = moveLog[validPosDepth] * 24;
|
|
703
|
+
for (let w = 0; w < 24; w++)
|
|
704
|
+
dst[w] = wingPosMove[row + src[w]];
|
|
705
|
+
validPosDepth++;
|
|
706
|
+
}
|
|
707
|
+
const parentPos = posStack[depth];
|
|
708
|
+
const childPos = posStack[depth + 1];
|
|
709
|
+
const posRow = m * 24;
|
|
710
|
+
const limit = maxl - 1;
|
|
711
|
+
let blocked = false;
|
|
712
|
+
// Group by group (4 wings = 2 pairs each), so a blocked child only
|
|
713
|
+
// pays for the groups actually checked.
|
|
714
|
+
for (let q = 0; q < 24; q += 4) {
|
|
715
|
+
const a0 = wingPosMove[posRow + parentPos[q]];
|
|
716
|
+
const a1 = wingPosMove[posRow + parentPos[q + 1]];
|
|
717
|
+
const b0 = wingPosMove[posRow + parentPos[q + 2]];
|
|
718
|
+
const b1 = wingPosMove[posRow + parentPos[q + 3]];
|
|
719
|
+
if (transversalPdb[pair_transversal_js_1.PAIR_INDEX[a0 * 24 + a1] * pair_transversal_js_1.PAIR_STATES + pair_transversal_js_1.PAIR_INDEX[b0 * 24 + b1]] > limit) {
|
|
720
|
+
blocked = true;
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
childPos[q] = a0;
|
|
724
|
+
childPos[q + 1] = a1;
|
|
725
|
+
childPos[q + 2] = b0;
|
|
726
|
+
childPos[q + 3] = b1;
|
|
727
|
+
}
|
|
728
|
+
if (blocked)
|
|
729
|
+
continue;
|
|
730
|
+
childPosValid = true;
|
|
731
|
+
}
|
|
564
732
|
moveLog[depth] = m;
|
|
565
733
|
if (validWpDepth > depth)
|
|
566
734
|
validWpDepth = depth;
|
|
735
|
+
if (validPosDepth > depth)
|
|
736
|
+
validPosDepth = depth;
|
|
737
|
+
if (childPosValid)
|
|
738
|
+
validPosDepth = depth + 1;
|
|
567
739
|
if (searchPhase2(nc, nr, maxl - 1, m, depth + 1, bridgeState, prep))
|
|
568
740
|
return true;
|
|
569
741
|
}
|
|
@@ -585,7 +757,8 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
585
757
|
continue;
|
|
586
758
|
const totalRot = [...p1.rotAlign, ...bridge.rotation];
|
|
587
759
|
const prefix = p1.moves;
|
|
588
|
-
const
|
|
760
|
+
const ct = chiral ? (0, center2_js_1.encodeCenter2ChiralCt)(bridge.state) : bridge.encoded.ct;
|
|
761
|
+
const prun = c2pruning[ct * center2_js_1.CENTER2_RL_STATES + bridge.encoded.rl];
|
|
589
762
|
// Paired wings lookahead: check how many wings are already paired in bridge.state
|
|
590
763
|
let pairedWings = 0;
|
|
591
764
|
const wp = bridge.state.wp;
|
|
@@ -597,13 +770,13 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
597
770
|
// Lookahead composite score: total estimated centers penalized by already-paired wings
|
|
598
771
|
const estCenters = prefix.length + prun;
|
|
599
772
|
const score = estCenters * 2 - Math.min(4, pairedWings);
|
|
600
|
-
preps.push({ p1, prefix, bridgeState: bridge.state, totalRot, ct
|
|
773
|
+
preps.push({ p1, prefix, bridgeState: bridge.state, totalRot, ct, rl: bridge.encoded.rl, prun, score });
|
|
601
774
|
}
|
|
602
775
|
// Ascending lookahead score (lowest total estimated center moves + wing bonus),
|
|
603
776
|
// tie-broken by lower phase-2 pruning bound then shorter prefix length
|
|
604
777
|
preps.sort((a, b) => (a.score - b.score) || (a.prun - b.prun) || (a.prefix.length - b.prefix.length));
|
|
605
778
|
// Filter: keep the best preps to avoid wasting phase-2 DFS work on suboptimal centers
|
|
606
|
-
const PREP_CAP = options?.prepCap !== undefined ? options.prepCap : (deep ?
|
|
779
|
+
const PREP_CAP = options?.prepCap !== undefined ? options.prepCap : (deep ? 48 : 32);
|
|
607
780
|
if (preps.length > PREP_CAP) {
|
|
608
781
|
preps.length = PREP_CAP;
|
|
609
782
|
}
|
|
@@ -635,6 +808,10 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
635
808
|
searchedL2.add(key);
|
|
636
809
|
p1CandCount = 0;
|
|
637
810
|
wpStack[0].set(prep.bridgeState.wp);
|
|
811
|
+
for (let p = 0; p < 24; p++)
|
|
812
|
+
posStack[0][prep.bridgeState.wp[p]] = p;
|
|
813
|
+
validPosDepth = 0;
|
|
814
|
+
wpMaskStack[0] = pairingMaskOf(prep.bridgeState.wp);
|
|
638
815
|
validWpDepth = 0;
|
|
639
816
|
searchPhase2(prep.ct, prep.rl, length2, -1, 0, prep.bridgeState, prep);
|
|
640
817
|
if (candidates.length >= CAND_CAP &&
|
|
@@ -669,9 +846,13 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
669
846
|
MAX_P2++;
|
|
670
847
|
} while ((candidates.length === 0 && MAX_P2 <= P2_MAX_ABS) || (candidates.length < CAND_MIN && MAX_P2 <= P2_MAX_MIN));
|
|
671
848
|
if (candidates.length === 0) {
|
|
672
|
-
throw new
|
|
849
|
+
throw new Cube4Phase2CapError("4x4 solve: no valid phase 2 candidates found.");
|
|
673
850
|
}
|
|
674
851
|
const tF2End = performance.now();
|
|
852
|
+
// Cheap-fix goal filter (see Cube4SolveOptions.maxFixMoves). The deep
|
|
853
|
+
// fallback widens it back to 4 (legacy, always satisfiable) so a hard
|
|
854
|
+
// state can never fail on the filter alone.
|
|
855
|
+
const maxFixMoves = (options?.maxFixMoves ?? 4) + (deep ? 2 : 0);
|
|
675
856
|
const validReductions = [];
|
|
676
857
|
/**
|
|
677
858
|
* Per-candidate IDA* proofs shared across all sweep stages: the greatest
|
|
@@ -700,31 +881,50 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
700
881
|
return null;
|
|
701
882
|
const rotToStandard = invertRotationSequence(path);
|
|
702
883
|
const standardReduced = applyCubeRotation(reducedOriginal, rotToStandard);
|
|
703
|
-
const
|
|
704
|
-
if (
|
|
705
|
-
console.log(" toCube3State failed!");
|
|
884
|
+
const rotMoves = rotToStandard.flatMap((r) => ROT_MOVES[r]);
|
|
885
|
+
if (rotMoves.length > maxFixMoves)
|
|
706
886
|
return null;
|
|
887
|
+
if (typeof process !== "undefined" && process.env?.CUBE4_ROT_STATS === "1" && exports.rotStats.length < 200) {
|
|
888
|
+
exports.rotStats.push({
|
|
889
|
+
totalRot: totalRot.join(","),
|
|
890
|
+
rotToStandard: rotToStandard.join(","),
|
|
891
|
+
rotLen: rotToStandard.reduce((n, rr) => n + ROT_MOVES[rr].length, 0)
|
|
892
|
+
});
|
|
707
893
|
}
|
|
894
|
+
// The orientation key only covers 3 of the 24 center slots. A reduced
|
|
895
|
+
// arrangement outside the rotation orbit (e.g. a reflected one) can
|
|
896
|
+
// collide with a rotation's key, in which case rotating by `path` does
|
|
897
|
+
// NOT leave standard centers — the 3x3 finish would solve a different
|
|
898
|
+
// color scheme and the resulting solution would be garbage. Reject.
|
|
899
|
+
for (let i = 0; i < 24; i++) {
|
|
900
|
+
if (standardReduced.centers[i] !== SOLVED_CENTERS[i])
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
const cube3State = (0, phase4_js_1.toCube3State)(standardReduced);
|
|
904
|
+
if (!cube3State)
|
|
905
|
+
return null;
|
|
708
906
|
const h3 = tables.cube3Phase1 ? getTriAxialCube3Heuristic(standardReduced, tables.cube3Phase1) : 0;
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
];
|
|
713
|
-
const simplifiedReduction = (0, moves_js_1.simplifyCube4Algorithm)(reductionMoves);
|
|
907
|
+
// The reduction + rotation prefix as it will be emitted (original frame):
|
|
908
|
+
// apply(state, [...moves12, ...moves3Remapped ... rotMoves]) = standardReduced.
|
|
909
|
+
// Priced with the fix at its best position (see placeRotationFix).
|
|
910
|
+
const simplifiedReduction = (0, rotation_fix_js_1.placeRotationFix)([...cand.moves12, ...moves3Remapped], rotToStandard, []);
|
|
714
911
|
return {
|
|
715
912
|
cand,
|
|
716
913
|
moves3,
|
|
717
914
|
moves3Remapped,
|
|
718
915
|
rotPath: path,
|
|
719
916
|
rotToStandard,
|
|
720
|
-
rotMoves
|
|
917
|
+
rotMoves,
|
|
721
918
|
standardReduced,
|
|
722
919
|
h3,
|
|
723
920
|
totalEst: simplifiedReduction.length + h3
|
|
724
921
|
};
|
|
725
922
|
}
|
|
726
|
-
|
|
727
|
-
|
|
923
|
+
// Defaults 1 × 5 (was 3 × 3): with the continuous finish, extra center
|
|
924
|
+
// candidates stopped paying. cc:0..31: 45.19 moves @ 673 ms vs 45.16 @
|
|
925
|
+
// 1042 ms (2×3: 45.25 @ 799; 1×8: 45.25 @ 691).
|
|
926
|
+
const centersBeam = Math.max(1, options?.centersBeam ?? 1);
|
|
927
|
+
const edgesPerCenter = Math.max(1, options?.edgesPerCenter ?? 5);
|
|
728
928
|
const maxReductionsTarget = options?.finishTries !== undefined
|
|
729
929
|
? Math.max(1, options.finishTries)
|
|
730
930
|
: (centersBeam * edgesPerCenter);
|
|
@@ -742,8 +942,14 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
742
942
|
const candReductions = new Map();
|
|
743
943
|
const distinctCenters = new Set();
|
|
744
944
|
let foundAtLength = null;
|
|
945
|
+
// The rotation fix (rotMoves) is priced INTO totalEst and varies 2-4
|
|
946
|
+
// moves per goal PATH (the end scheme depends on the path). A goal one
|
|
947
|
+
// total-step deeper with a 2-move fix beats a shallow goal with a 4-move
|
|
948
|
+
// fix, so keep collecting a few steps past the first find and let
|
|
949
|
+
// totalEst decide (was: foundAtLength + 1 with needed + 3 goals per
|
|
950
|
+
// call, which discarded fix-cheap alternatives).
|
|
745
951
|
OUTER_P3: for (let length123 = pool[0].value; length123 < L123_CAP; length123++) {
|
|
746
|
-
if (foundAtLength !== null && length123 > foundAtLength +
|
|
952
|
+
if (foundAtLength !== null && length123 > foundAtLength + 3)
|
|
747
953
|
break;
|
|
748
954
|
// Sweep the whole (sorted) pool: see sortTop rationale.
|
|
749
955
|
for (let i = 0; i < pool.length; i++) {
|
|
@@ -764,19 +970,34 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
764
970
|
if (distinctCenters.size >= centersBeam && !distinctCenters.has(cand))
|
|
765
971
|
continue;
|
|
766
972
|
if (cand.edist <= length3 && cand.cdist <= length3 && cand.hdist <= length3 && (cand.pdist ?? 0) <= length3) {
|
|
767
|
-
const
|
|
973
|
+
const remainingNodes = phase3NodeBudget - (phase3Solver.epochNodes - phase3StartNodes);
|
|
974
|
+
if (remainingNodes <= 0) {
|
|
975
|
+
if (validReductions.length > 0)
|
|
976
|
+
return;
|
|
977
|
+
throw new Cube4Phase3CapError("4x4 solve: phase 3 total node budget exhausted.");
|
|
978
|
+
}
|
|
979
|
+
const nodeLimit = Math.min(remainingNodes, length3 <= 12 ? 80000 : Math.max(45000, cfg.maxNodes));
|
|
768
980
|
f3calls++;
|
|
769
|
-
const validsBefore = validReductions.length;
|
|
770
981
|
const needed = edgesPerCenter - candCount;
|
|
771
982
|
const candReductionsBuffer = [];
|
|
772
|
-
|
|
773
|
-
|
|
983
|
+
// Collect beyond `needed`: goal paths at the same length carry
|
|
984
|
+
// different rotation fixes (2 vs 4 moves), and the totalEst sort
|
|
985
|
+
// below needs the variety to pick a cheap-fix end.
|
|
986
|
+
const maxToCollect = Math.min(24, needed + 9);
|
|
987
|
+
const result = phase3Solver.solveLengthProven(cand.edge, cand.ct3, cand.edist, length3, tables.center3, tables.edge3.distances, nodeLimit,
|
|
988
|
+
// Goal budget per call: with the cheap-fix filter most goal ends
|
|
989
|
+
// are rejected, so the walk must be allowed to examine more of
|
|
990
|
+
// them before giving up on this (candidate, length3).
|
|
991
|
+
48, (moves3) => {
|
|
774
992
|
const red = buildValidReduction(cand, moves3);
|
|
775
993
|
if (!red)
|
|
776
994
|
return false;
|
|
777
995
|
candReductionsBuffer.push(red);
|
|
778
|
-
|
|
779
|
-
|
|
996
|
+
// <=-search: goals may land shorter than the length3 bound —
|
|
997
|
+
// track the true total for the post-find collection window.
|
|
998
|
+
const total = cand.length12 + moves3.length;
|
|
999
|
+
if (foundAtLength === null || total < foundAtLength)
|
|
1000
|
+
foundAtLength = total;
|
|
780
1001
|
return candReductionsBuffer.length >= maxToCollect;
|
|
781
1002
|
},
|
|
782
1003
|
// hc mid-search prune disabled: on a 44-state corpus the per-node
|
|
@@ -796,20 +1017,18 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
796
1017
|
candReductions.set(cand, candCount + toAdd.length);
|
|
797
1018
|
distinctCenters.add(cand);
|
|
798
1019
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
console.log(`sweep l12=${cand.length12} l3=${length3} nodes=${result.nodesUsed}/${nodeLimit} ` +
|
|
803
|
-
`valid=${validReductions.length > validsBefore} proven=${proven} ` +
|
|
804
|
-
`c=${cand.cdist} e=${cand.edist} h=${cand.hdist}`);
|
|
805
|
-
}
|
|
1020
|
+
// Predicate-scoped proof (same buildValidReduction filter for this
|
|
1021
|
+
// candidate across all its calls): skip every settled length.
|
|
1022
|
+
const proven = result.validatedThrough;
|
|
806
1023
|
if (result.moves === null && proven >= length3) {
|
|
807
1024
|
const prev = p3Proven.get(cand) ?? -1;
|
|
808
1025
|
if (proven > prev)
|
|
809
1026
|
p3Proven.set(cand, proven);
|
|
810
1027
|
}
|
|
811
1028
|
// The finish loop attempts reductions up to maxReductionsTarget
|
|
812
|
-
|
|
1029
|
+
// (with a wider collection cap so cheap-fix ends at later length
|
|
1030
|
+
// steps can still displace expensive-fix ones by totalEst).
|
|
1031
|
+
if (validReductions.length >= maxReductionsTarget + 9) {
|
|
813
1032
|
break OUTER_P3;
|
|
814
1033
|
}
|
|
815
1034
|
}
|
|
@@ -828,10 +1047,10 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
828
1047
|
};
|
|
829
1048
|
// Sweeps with growing budgets; longitudes extend dynamically (reference
|
|
830
1049
|
// style) while nothing is found, instead of fixed caps. Easy states only
|
|
831
|
-
// pay the first sweep.
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
1050
|
+
// pay the first sweep. The length3 caps are NOT redundant re-walks: the
|
|
1051
|
+
// ladder widens the pairing cap ONLY when nothing was found at all, and the
|
|
1052
|
+
// narrower caps keep early sweeps on short pairings (measured: a single
|
|
1053
|
+
// merged sweep with maxLength3 24 is ~20% SLOWER with identical output).
|
|
835
1054
|
// NOTE: lowering these caps was tried (20k/16k) and reverted: capped calls
|
|
836
1055
|
// change which valids are found (±2 moves on the corpus). Only admissible
|
|
837
1056
|
// heuristic strengthenings are length-safe here.
|
|
@@ -874,6 +1093,10 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
874
1093
|
searchedL2.add(key);
|
|
875
1094
|
p1CandCount = 0;
|
|
876
1095
|
wpStack[0].set(prep.bridgeState.wp);
|
|
1096
|
+
for (let p = 0; p < 24; p++)
|
|
1097
|
+
posStack[0][prep.bridgeState.wp[p]] = p;
|
|
1098
|
+
validPosDepth = 0;
|
|
1099
|
+
wpMaskStack[0] = pairingMaskOf(prep.bridgeState.wp);
|
|
877
1100
|
validWpDepth = 0;
|
|
878
1101
|
searchPhase2(prep.ct, prep.rl, length2, -1, 0, prep.bridgeState, prep);
|
|
879
1102
|
if (candidates.length - poolStart >= 40)
|
|
@@ -891,7 +1114,7 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
891
1114
|
}
|
|
892
1115
|
}
|
|
893
1116
|
if (validReductions.length === 0) {
|
|
894
|
-
throw new
|
|
1117
|
+
throw new Cube4Phase3CapError("4x4 solve: phase 3 reduction failed across all candidates.");
|
|
895
1118
|
}
|
|
896
1119
|
// Sort reductions by Total Estimated Moves (Reduction length + 3x3 Phase 1 lower bound)
|
|
897
1120
|
validReductions.sort((a, b) => a.totalEst - b.totalEst);
|
|
@@ -906,28 +1129,56 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
906
1129
|
// Best-of among the first three (phase-4 is cheap now); further
|
|
907
1130
|
// reductions are only attempted when those all failed.
|
|
908
1131
|
const finishTries = options?.finishTries ?? maxReductionsTarget;
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1132
|
+
const collectFinishStats = typeof process !== "undefined" && process.env?.CUBE4_FINISH_STATS === "1";
|
|
1133
|
+
// Empirical finish lower bound (see finishStats): every observed finish
|
|
1134
|
+
// ran 11-16 moves ABOVE `totalEst` (continuous finish: 67 finishes over 16
|
|
1135
|
+
// seeds, gap = totalEst - finalLen in [-16, -11]; the legacy finish was
|
|
1136
|
+
// [-16, -12]) because `h3` only bounds the cube3 PHASE-1 length while the
|
|
1137
|
+
// finish is two-phase. A reduction whose totalEst + 11 cannot strictly
|
|
1138
|
+
// beat the current best therefore cannot win, and its finish is skipped
|
|
1139
|
+
// entirely. If a state ever lands a finish within 11 of the phase-1 bound,
|
|
1140
|
+
// this degrades to a slightly longer (still exact,
|
|
1141
|
+
// still valid) scramble for that state — never an incorrect one.
|
|
1142
|
+
const FINISH_LB_BIAS = 11;
|
|
1143
|
+
const screenTop = Math.max(0, Math.floor(options?.finishScreenTop ?? 0));
|
|
1144
|
+
const screened = [];
|
|
1145
|
+
/** Finishes one reduction; records it as the best when shorter. */
|
|
1146
|
+
async function finishReduction(red, effort) {
|
|
913
1147
|
const cube3State = (0, phase4_js_1.toCube3State)(red.standardReduced);
|
|
914
1148
|
if (!cube3State)
|
|
915
|
-
|
|
1149
|
+
return null;
|
|
916
1150
|
let cube3Solution;
|
|
917
1151
|
try {
|
|
918
|
-
cube3Solution = await solveCube3(cube3State);
|
|
1152
|
+
cube3Solution = await solveCube3(cube3State, effort);
|
|
919
1153
|
}
|
|
920
1154
|
catch {
|
|
921
|
-
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
// Frame invariant (verified): apply(state, [...moves12, ...moves3Remapped])
|
|
1158
|
+
// is exactly `reducedOriginal`, and `reducedOriginal · rotMoves(r)`
|
|
1159
|
+
// (= applyCubeRotation(_, r)) is `standardReduced`, which the cube3
|
|
1160
|
+
// finish C solves. So the solution in the ORIGINAL frame is
|
|
1161
|
+
// X ++ rotMoves(r) ++ C.
|
|
1162
|
+
// Two previously-tried assemblies are WRONG:
|
|
1163
|
+
// - [rot_r(X), C] solves ρ_r(state), not state (the observed adaptive
|
|
1164
|
+
// whole-cube-rotation bug: apply(solved, invert(moves)) = ρ_r(T));
|
|
1165
|
+
// - [X, rot_{-r}(C)] lands on ρ_{-r}(solved) ≠ solved, because a
|
|
1166
|
+
// whole-cube rotation does NOT leave the canonical solved state
|
|
1167
|
+
// invariant (it permutes face colors in the absolute frame).
|
|
1168
|
+
// `placeRotationFix` keeps that effect but may move the rotMoves(r)
|
|
1169
|
+
// block anywhere in the sequence (re-mapping what follows it) where it
|
|
1170
|
+
// cancels against adjacent wide moves.
|
|
1171
|
+
const simplified = (0, rotation_fix_js_1.placeRotationFix)([...red.cand.moves12, ...red.moves3Remapped], red.rotToStandard, cube3Solution.moves);
|
|
1172
|
+
if (collectFinishStats) {
|
|
1173
|
+
exports.finishStats.finishes++;
|
|
1174
|
+
const gap = red.totalEst - simplified.length;
|
|
1175
|
+
if (gap < exports.finishStats.gapMin)
|
|
1176
|
+
exports.finishStats.gapMin = gap;
|
|
1177
|
+
if (gap > exports.finishStats.gapMax)
|
|
1178
|
+
exports.finishStats.gapMax = gap;
|
|
1179
|
+
exports.finishStats.gapSum += gap;
|
|
1180
|
+
exports.finishStats.gapN++;
|
|
922
1181
|
}
|
|
923
|
-
// Rotate the solve algorithm so it is performed from the standard orientation ("hacer los movimientos desde otro lado").
|
|
924
|
-
// This allows the solve to transition directly to the standard solved state WITHOUT ANY WHOLE-CUBE ROTATIONS (rotMoves)!
|
|
925
|
-
const rotatedFullSolution = [
|
|
926
|
-
...rotateAlgorithm(red.cand.moves12, red.rotToStandard),
|
|
927
|
-
...rotateAlgorithm(red.moves3Remapped, red.rotToStandard),
|
|
928
|
-
...cube3Solution.moves
|
|
929
|
-
];
|
|
930
|
-
const simplified = (0, moves_js_1.simplifyCube4Algorithm)(rotatedFullSolution);
|
|
931
1182
|
if (simplified.length < minTotalMoves) {
|
|
932
1183
|
minTotalMoves = simplified.length;
|
|
933
1184
|
bestSolution = simplified;
|
|
@@ -940,9 +1191,31 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
940
1191
|
};
|
|
941
1192
|
bestRed = red;
|
|
942
1193
|
}
|
|
1194
|
+
return simplified;
|
|
1195
|
+
}
|
|
1196
|
+
for (let i = 0; i < validReductions.length; i++) {
|
|
1197
|
+
if (i >= finishTries && bestSolution)
|
|
1198
|
+
break;
|
|
1199
|
+
const red = validReductions[i];
|
|
1200
|
+
if (bestSolution !== null && red.totalEst + FINISH_LB_BIAS >= minTotalMoves) {
|
|
1201
|
+
exports.finishStats.skipped++;
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
const done = await finishReduction(red, screenTop > 0 ? "screen" : "full");
|
|
1205
|
+
if (done)
|
|
1206
|
+
screened.push({ red, total: done.length });
|
|
1207
|
+
}
|
|
1208
|
+
// Stage 2: the best screened reductions get the full-effort finish.
|
|
1209
|
+
if (screenTop > 0) {
|
|
1210
|
+
screened.sort((a, b) => a.total - b.total);
|
|
1211
|
+
for (const { red } of screened.slice(0, screenTop)) {
|
|
1212
|
+
if (red.totalEst + FINISH_LB_BIAS >= minTotalMoves)
|
|
1213
|
+
continue;
|
|
1214
|
+
await finishReduction(red, "full");
|
|
1215
|
+
}
|
|
943
1216
|
}
|
|
944
1217
|
if (!bestSolution || !bestRed) {
|
|
945
|
-
throw new
|
|
1218
|
+
throw new Cube4FinishCapError("4x4 solve: reduction failed during the 3x3 finish across all valid reductions.");
|
|
946
1219
|
}
|
|
947
1220
|
// Polish pass: the sweep takes the first valid sequence per exact
|
|
948
1221
|
// length, which is often one above optimal. Retry the winner one length
|
|
@@ -962,13 +1235,23 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
962
1235
|
// and a full 150k grind on tail states cost seconds for nothing. It is
|
|
963
1236
|
// also skipped entirely unless the total is high enough to matter.
|
|
964
1237
|
const polishBudget = r === bestRed ? 150000 : 30000;
|
|
1238
|
+
// NOTE: with the <=-length phase-3 search the sweep already enumerates
|
|
1239
|
+
// shorter goals, so the polish is a second-chance pass only (measured:
|
|
1240
|
+
// a wider gate/budget found nothing and cost +32% p3 nodes).
|
|
965
1241
|
for (let iter = 0; iter < 4 && (minTotalMoves > 43 || curRed.moves3.length > 11); iter++) {
|
|
966
1242
|
const cand = curRed.cand;
|
|
967
1243
|
const shorterLen = curRed.moves3.length - 1;
|
|
968
1244
|
if (shorterLen < cand.edist || shorterLen < cand.cdist || shorterLen < cand.hdist || shorterLen < (cand.pdist ?? 0))
|
|
969
1245
|
break;
|
|
970
1246
|
f3calls++;
|
|
971
|
-
|
|
1247
|
+
// Unified polish: accept any goal path with a strictly better
|
|
1248
|
+
// totalEst at the same or shorter length — covers both a shorter
|
|
1249
|
+
// phase-3 path and a cheaper rotation fix (the fix is priced into
|
|
1250
|
+
// totalEst and varies 2-4 moves per goal path).
|
|
1251
|
+
const shorter = phase3Solver.solveLengthValid(cand.edge, cand.ct3, cand.edist, curRed.moves3.length, tables.center3, tables.edge3.distances, polishBudget, 64, (moves3) => {
|
|
1252
|
+
const nr = buildValidReduction(cand, moves3);
|
|
1253
|
+
return nr !== null && nr.totalEst < curRed.totalEst;
|
|
1254
|
+
}, tables.hc, cand.hcCoord, tables.c3PairDist);
|
|
972
1255
|
if (!shorter)
|
|
973
1256
|
break;
|
|
974
1257
|
const red = buildValidReduction(cand, shorter);
|
|
@@ -984,11 +1267,8 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
984
1267
|
catch {
|
|
985
1268
|
break;
|
|
986
1269
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
...rotateAlgorithm(red.moves3Remapped, red.rotToStandard),
|
|
990
|
-
...cube3Solution.moves
|
|
991
|
-
]);
|
|
1270
|
+
// Same frame invariant as the finish loop: X ++ rotMoves(r) ++ C.
|
|
1271
|
+
const simplified = (0, rotation_fix_js_1.placeRotationFix)([...red.cand.moves12, ...red.moves3Remapped], red.rotToStandard, cube3Solution.moves);
|
|
992
1272
|
if (simplified.length < minTotalMoves) {
|
|
993
1273
|
minTotalMoves = simplified.length;
|
|
994
1274
|
bestSolution = simplified;
|
|
@@ -1013,13 +1293,19 @@ async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
1013
1293
|
accF3Ms += tFinishStart - tF2End;
|
|
1014
1294
|
accFinishMs += tPolishStart - tFinishStart;
|
|
1015
1295
|
accPolishMs += performance.now() - tPolishStart;
|
|
1296
|
+
// Final invariant: the emitted solution must solve the INPUT state
|
|
1297
|
+
// exactly (no rotation tolerance). Fail loudly rather than return a
|
|
1298
|
+
// solution that does not resolve. One replay of ~45 moves — negligible
|
|
1299
|
+
// next to the search cost.
|
|
1300
|
+
if (!(0, state_js_1.isSolvedCube4State)((0, moves_js_1.applyCube4Algorithm)(state, bestSolution))) {
|
|
1301
|
+
throw new Error("4x4 solve: internal verification failed — the solution does not solve the input state.");
|
|
1302
|
+
}
|
|
1016
1303
|
return { moves: bestSolution, phases: bestPhases };
|
|
1017
1304
|
};
|
|
1018
1305
|
let bestMoves = null;
|
|
1019
1306
|
let bestPhasesOverall;
|
|
1020
1307
|
let lastRestartError = null;
|
|
1021
1308
|
for (let restart = 0; restart < totalRestarts; restart++) {
|
|
1022
|
-
phase3_search_js_1.FastPhase3Solver.nextEpoch();
|
|
1023
1309
|
const rng = totalRestarts > 1
|
|
1024
1310
|
? mulberry32((hashCube4State(state) ^ Math.imul(restart + 1, 0x9e3779b9)) >>> 0)
|
|
1025
1311
|
: null;
|