@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
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
* (phase 4), returning the move sequence that solves the given state.
|
|
4
4
|
* The scramble for a sampled state is the inverted solution.
|
|
5
5
|
*/
|
|
6
|
-
import { applyCube4Algorithm, invertCube4Algorithm
|
|
6
|
+
import { applyCube4Algorithm, invertCube4Algorithm } from "../moves.js";
|
|
7
7
|
import { center1Mask, getCenter1Distance, applyCenter1Move, CENTER1_MOVES, CENTER1_GOAL_MASKS } from "./center1.js";
|
|
8
|
-
import { center2Bridge, getWingMoveMaps,
|
|
8
|
+
import { center2Bridge, getWingMoveMaps, CENTER2_GOAL_RL, CENTER2_MOVES, CENTER2_SEARCH_MOVES, CENTER2_SEARCH_MOVES_WIDE, CENTER2_RL_STATES, encodeCenter2ChiralCt, generateCenter2ChiralTables } from "./center2.js";
|
|
9
9
|
import { encodeCenter3 } from "./center3.js";
|
|
10
|
-
import { encodePairingState,
|
|
11
|
-
import { encodeHcPdb } from "./hc-pdb.js";
|
|
10
|
+
import { encodePairingState, PAIRING_SECOND_HALVES } from "./pairing-coordinate.js";
|
|
11
|
+
import { encodeHcPdb, hcDenseIndex } from "./hc-pdb.js";
|
|
12
12
|
import { getSymTables, getMvSymFrom } from "./edge3.js";
|
|
13
13
|
import { FastPhase3Solver } from "./phase3-search.js";
|
|
14
|
+
import { placeRotationFix } from "./rotation-fix.js";
|
|
15
|
+
import { getTransversalPairPdb, getWingPositionMoves, PAIR_INDEX, PAIR_STATES } from "./pair-transversal.js";
|
|
14
16
|
import { checkCube4Reduction, toCube3State } from "./phase4.js";
|
|
15
17
|
import { encodeCornerOrientation, encodeEdgeOrientation, encodeUdSlice } from "../../cube3/solver/coordinates.js";
|
|
16
|
-
import { createSolvedCube4State } from "../state.js";
|
|
18
|
+
import { createSolvedCube4State, isSolvedCube4State } from "../state.js";
|
|
17
19
|
function mulberry32(seed) {
|
|
18
20
|
let a = seed >>> 0;
|
|
19
21
|
return () => {
|
|
@@ -104,6 +106,29 @@ function getTriAxialCube3Heuristic(standardReduced, tables) {
|
|
|
104
106
|
}
|
|
105
107
|
return minH;
|
|
106
108
|
}
|
|
109
|
+
class Cube4SolveError extends Error {
|
|
110
|
+
}
|
|
111
|
+
export class Cube4Phase2CapError extends Cube4SolveError {
|
|
112
|
+
code = "cube4-phase2-cap";
|
|
113
|
+
constructor(message) {
|
|
114
|
+
super(message);
|
|
115
|
+
this.name = "Cube4Phase2CapError";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export class Cube4Phase3CapError extends Cube4SolveError {
|
|
119
|
+
code = "cube4-phase3-cap";
|
|
120
|
+
constructor(message) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.name = "Cube4Phase3CapError";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export class Cube4FinishCapError extends Cube4SolveError {
|
|
126
|
+
code = "cube4-finish-cap";
|
|
127
|
+
constructor(message) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.name = "Cube4FinishCapError";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
107
132
|
function cornerPermutationParity(cp) {
|
|
108
133
|
let parity = 0;
|
|
109
134
|
const seen = new Array(8).fill(false);
|
|
@@ -183,6 +208,8 @@ const VALID_24_ORIENTATIONS = (() => {
|
|
|
183
208
|
}
|
|
184
209
|
return map;
|
|
185
210
|
})();
|
|
211
|
+
/** Centers of the solved state — used to verify a full (not just partial) standard orientation. */
|
|
212
|
+
const SOLVED_CENTERS = createSolvedCube4State().centers;
|
|
186
213
|
const ROT_MAP = {
|
|
187
214
|
"x": { "U": "B", "R": "R", "F": "U", "D": "F", "L": "L", "B": "D" },
|
|
188
215
|
"x'": { "U": "F", "R": "R", "F": "D", "D": "B", "L": "L", "B": "U" },
|
|
@@ -218,6 +245,10 @@ export function rotateAlgorithm(alg, rotSeq) {
|
|
|
218
245
|
return current;
|
|
219
246
|
});
|
|
220
247
|
}
|
|
248
|
+
/** Physical wide-move sequence implementing a whole-cube rotation. */
|
|
249
|
+
export function cubeRotationMoves(rotSeq) {
|
|
250
|
+
return rotSeq.flatMap((rotation) => ROT_MOVES[rotation]);
|
|
251
|
+
}
|
|
221
252
|
export function invertRotationSequence(rotSeq) {
|
|
222
253
|
return [...rotSeq].reverse().map((r) => INVERT_ROT[r]);
|
|
223
254
|
}
|
|
@@ -258,6 +289,49 @@ function isCancelled(previous, current) {
|
|
|
258
289
|
export const P2_CANCEL = CENTER2_SEARCH_MOVES.map((previous) => CENTER2_SEARCH_MOVES.map((current) => isCancelled(previous, current)));
|
|
259
290
|
// Same over the wide 28-move set (used when `center2Wide` is set).
|
|
260
291
|
const P2_CANCEL_WIDE = CENTER2_SEARCH_MOVES_WIDE.map((previous) => CENTER2_SEARCH_MOVES_WIDE.map((current) => isCancelled(previous, current)));
|
|
292
|
+
/** Flat (row-major) cancel tables for the hot DFS loop; same semantics. */
|
|
293
|
+
function flattenCancel(table) {
|
|
294
|
+
const n = table.length;
|
|
295
|
+
const flat = new Uint8Array(n * n);
|
|
296
|
+
for (let i = 0; i < n; i++) {
|
|
297
|
+
const row = table[i];
|
|
298
|
+
for (let j = 0; j < n; j++)
|
|
299
|
+
flat[i * n + j] = row[j] ? 1 : 0;
|
|
300
|
+
}
|
|
301
|
+
return flat;
|
|
302
|
+
}
|
|
303
|
+
const P2_CANCEL_FLAT = flattenCancel(P2_CANCEL);
|
|
304
|
+
const P2_CANCEL_WIDE_FLAT = flattenCancel(P2_CANCEL_WIDE);
|
|
305
|
+
/**
|
|
306
|
+
* Wing transversal mask: bit k set iff pair k occupies a second-half slot.
|
|
307
|
+
* `hasPairingTransversal(wp)` is equivalent to `pairingMaskOf(wp) === 0xfff`
|
|
308
|
+
* (12 slots, 12 pair ids in 0..11: all distinct ⟺ all 12 bits set). The mask
|
|
309
|
+
* form is O(1) at the millions of phase-2 goal leaves.
|
|
310
|
+
*/
|
|
311
|
+
function pairingMaskOf(wp) {
|
|
312
|
+
let mask = 0;
|
|
313
|
+
for (let i = 0; i < 12; i++) {
|
|
314
|
+
mask |= 1 << (wp[PAIRING_SECOND_HALVES[i]] >> 1);
|
|
315
|
+
}
|
|
316
|
+
return mask;
|
|
317
|
+
}
|
|
318
|
+
/** rl side of the phase-2 goal test (same flags as `isCenter2Goal`). */
|
|
319
|
+
const P2_GOAL_RL = (() => {
|
|
320
|
+
const flags = new Uint8Array(CENTER2_RL_STATES);
|
|
321
|
+
for (const rl of CENTER2_GOAL_RL)
|
|
322
|
+
flags[rl] = 1;
|
|
323
|
+
return flags;
|
|
324
|
+
})();
|
|
325
|
+
/** Chiral phase-2 tables, generated once per loaded center2 table set. */
|
|
326
|
+
const chiralTablesCache = new WeakMap();
|
|
327
|
+
function getCenter2ChiralTables(center2) {
|
|
328
|
+
let chiral = chiralTablesCache.get(center2);
|
|
329
|
+
if (!chiral) {
|
|
330
|
+
chiral = generateCenter2ChiralTables(center2.rlMove);
|
|
331
|
+
chiralTablesCache.set(center2, chiral);
|
|
332
|
+
}
|
|
333
|
+
return chiral;
|
|
334
|
+
}
|
|
261
335
|
const SKIP_AXIS_2 = new Int32Array(CENTER2_SEARCH_MOVES.length);
|
|
262
336
|
for (let i = 0; i < CENTER2_SEARCH_MOVES.length; i++) {
|
|
263
337
|
const m1 = CENTER2_SEARCH_MOVES[i];
|
|
@@ -274,7 +348,6 @@ for (let i = 0; i < CENTER2_SEARCH_MOVES.length; i++) {
|
|
|
274
348
|
}
|
|
275
349
|
}
|
|
276
350
|
}
|
|
277
|
-
const phase3Solver = new FastPhase3Solver();
|
|
278
351
|
/** Temporary instrumentation for F2 goal statistics (CUBE4_F2_STATS=1). */
|
|
279
352
|
export const f2Stats = { goals: 0, transversalPass: 0, axisRejects: 0, minTransversalL2: 99, minChiralL2: 99 };
|
|
280
353
|
export function resetF2Stats() {
|
|
@@ -290,13 +363,38 @@ export function resetStagnationStats() {
|
|
|
290
363
|
stagnationStats.checks = 0;
|
|
291
364
|
stagnationStats.cuts = 0;
|
|
292
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Temporary instrumentation for the finish-skip bound (CUBE4_FINISH_STATS=1):
|
|
368
|
+
* `gap = totalEst - finalLen` per finished reduction. A skip
|
|
369
|
+
* `totalEst - SLACK >= minTotalMoves` is only sound while SLACK covers the
|
|
370
|
+
* observed gap maximum (junction cancellation + finish-vs-h3 slack).
|
|
371
|
+
*/
|
|
372
|
+
export const finishStats = {
|
|
373
|
+
gapMin: 99,
|
|
374
|
+
gapMax: -99,
|
|
375
|
+
gapSum: 0,
|
|
376
|
+
gapN: 0,
|
|
377
|
+
finishes: 0,
|
|
378
|
+
skipped: 0
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Temporary instrumentation for the rotMoves cost (CUBE4_ROT_STATS=1):
|
|
382
|
+
* per built reduction records the prep frame rotation, the derived
|
|
383
|
+
* normalization rotation and the emitted fix length.
|
|
384
|
+
*/
|
|
385
|
+
export const rotStats = [];
|
|
293
386
|
export async function solveCube4State(state, tables, solveCube3, options) {
|
|
294
387
|
// NOTE: no wall-clock deadline inside the search — all loops are bounded
|
|
295
|
-
// by deterministic work caps (node counts, candidate counts, length
|
|
296
|
-
//
|
|
297
|
-
// its own backstop (`timeoutMs` handled at the call site).
|
|
388
|
+
// by deterministic work caps (node counts, candidate counts, length caps),
|
|
389
|
+
// so the same state always takes the same path.
|
|
298
390
|
const sym = getSymTables();
|
|
391
|
+
// All mutable phase-3 state (TT, epoch-node count, call IDs and search
|
|
392
|
+
// stacks) belongs to this invocation. The same instance spans its
|
|
393
|
+
// deterministic restarts, retaining useful goal-free TT entries without
|
|
394
|
+
// allowing concurrent solves to contaminate one another.
|
|
395
|
+
const phase3Solver = new FastPhase3Solver();
|
|
299
396
|
phase3Solver.resetEpochNodes();
|
|
397
|
+
const collectF2Stats = typeof process !== "undefined" && process.env?.CUBE4_F2_STATS === "1";
|
|
300
398
|
const totalRestarts = Math.max(1, Math.floor(options?.restarts ?? 1));
|
|
301
399
|
// Cross-restart work accumulators (reported in nodes/timings).
|
|
302
400
|
let accP2 = 0;
|
|
@@ -321,8 +419,8 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
321
419
|
const path = [];
|
|
322
420
|
const p1Extra = options?.phase1ExtraDepths ?? 3;
|
|
323
421
|
const p1Cap = options?.phase1Cap ?? 400;
|
|
324
|
-
// Per-restart DFS traversal order (identity when rng is null).
|
|
325
|
-
const p1Order = restartOrder(CENTER1_MOVES.length, rng);
|
|
422
|
+
// Per-restart DFS traversal order (null = identity fast path when rng is null).
|
|
423
|
+
const p1Order = rng === null ? null : Int32Array.from(restartOrder(CENTER1_MOVES.length, rng));
|
|
326
424
|
for (let maxD = minOverallH; maxD <= minOverallH + p1Extra; maxD++) {
|
|
327
425
|
for (const urf of [0, 1, 2]) {
|
|
328
426
|
const minH = urfDistances[urf];
|
|
@@ -359,7 +457,7 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
359
457
|
if (depth + h > maxD)
|
|
360
458
|
return false;
|
|
361
459
|
for (let mi = 0; mi < CENTER1_MOVES.length; mi++) {
|
|
362
|
-
const mv = p1Order[mi];
|
|
460
|
+
const mv = p1Order === null ? mi : p1Order[mi];
|
|
363
461
|
if (lm >= 0 && Math.floor(lm / 3) === Math.floor(mv / 3))
|
|
364
462
|
continue;
|
|
365
463
|
const next = applyCenter1Move(m, tables.center1.moveMaps, mv);
|
|
@@ -378,12 +476,24 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
378
476
|
const minP1 = p1Sols[0]?.moves.length ?? 0;
|
|
379
477
|
const tF1End = performance.now();
|
|
380
478
|
const candidates = [];
|
|
381
|
-
const moveLog = new
|
|
479
|
+
const moveLog = new Int32Array(48);
|
|
382
480
|
// Incremental wing tracking stack for phase-2 goal transversal checks:
|
|
383
481
|
// avoids full 6-7 move replaying from scratch across hundreds of thousands of candidate goals.
|
|
384
|
-
|
|
482
|
+
// `wpMaskStack[d]` = pairingMaskOf(wpStack[d]) maintained in the same lazy
|
|
483
|
+
// fill, so the per-leaf transversal test is a single compare.
|
|
484
|
+
const wpStack = Array.from({ length: 32 }, () => new Uint8Array(24));
|
|
485
|
+
const wpMaskStack = new Int32Array(32);
|
|
385
486
|
let validWpDepth = 0;
|
|
386
487
|
const wingMaps = getWingMoveMaps();
|
|
488
|
+
// Transversal pruning (see pair-transversal.ts): wing positions per DFS
|
|
489
|
+
// depth (`posStack[d][wing]`), checked against the two-pair PDB over 6
|
|
490
|
+
// disjoint pair-of-pairs in the last TRANSVERSAL_DEPTH levels (the PDB's
|
|
491
|
+
// maximum). Prunes only branches with no transversal goal, so the
|
|
492
|
+
// accepted candidates are unchanged.
|
|
493
|
+
const posStack = Array.from({ length: 33 }, () => new Uint8Array(24));
|
|
494
|
+
const wingPosMove = getWingPositionMoves();
|
|
495
|
+
const TRANSVERSAL_DEPTH = 3;
|
|
496
|
+
let validPosDepth = 0;
|
|
387
497
|
/**
|
|
388
498
|
* Anti-hang work cap for the phase-2 search (successful attempts use
|
|
389
499
|
* ~0.1-0.3M nodes; this only aborts pathological explorations).
|
|
@@ -396,11 +506,21 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
396
506
|
// indices in either mode.
|
|
397
507
|
const c2Wide = options?.center2Wide === true;
|
|
398
508
|
const C2_SEARCH = c2Wide ? CENTER2_SEARCH_MOVES_WIDE : CENTER2_SEARCH_MOVES;
|
|
399
|
-
const
|
|
400
|
-
|
|
509
|
+
const cancelFlat = c2Wide ? P2_CANCEL_WIDE_FLAT : P2_CANCEL_FLAT;
|
|
510
|
+
const c2Stride = C2_SEARCH.length;
|
|
511
|
+
const transversalPdb = getTransversalPairPdb(C2_SEARCH.length);
|
|
512
|
+
const canSkipAxis = rng === null && !c2Wide;
|
|
513
|
+
// Per-restart child order (null = identity fast path). Entries stay
|
|
401
514
|
// original move-table indices, so all table lookups are unaffected.
|
|
402
|
-
const p2Order = restartOrder(C2_SEARCH.length, rng);
|
|
403
|
-
|
|
515
|
+
const p2Order = rng === null ? null : Int32Array.from(restartOrder(C2_SEARCH.length, rng));
|
|
516
|
+
// Default set: chiral tables (goal = ct == 0 && axisDet == 1 exactly, so
|
|
517
|
+
// wrong-chirality goal paths are pruned instead of enumerated). The wide
|
|
518
|
+
// set keeps the relative tables + the axisDet filter at the goal.
|
|
519
|
+
const chiral = c2Wide ? null : getCenter2ChiralTables(tables.center2);
|
|
520
|
+
const c2ctMove = chiral ? chiral.ctMove : tables.center2.ctMove;
|
|
521
|
+
const c2rlMove = tables.center2.rlMove;
|
|
522
|
+
const c2GoalCt = chiral ? chiral.goalCt : 0;
|
|
523
|
+
let c2pruning = chiral ? chiral.pruning : tables.center2.pruning;
|
|
404
524
|
if (c2Wide) {
|
|
405
525
|
if (!tables.center2.pruningWide) {
|
|
406
526
|
throw new Error("4x4 solve: center2Wide requested but pruningWide tables are not loaded — regenerate center2 tables.");
|
|
@@ -408,6 +528,8 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
408
528
|
c2pruning = tables.center2.pruningWide;
|
|
409
529
|
}
|
|
410
530
|
const P2_MAX_NODES = deep ? 24_000_000 : 8_000_000;
|
|
531
|
+
// Hoisted out of the goal path (was read per goal leaf).
|
|
532
|
+
const maxP1Cand = options?.p1CandCap !== undefined ? options.p1CandCap : (deep ? 24 : 16);
|
|
411
533
|
// Smaller pool + shorter extension (was 180/100/14/12 on the fast path):
|
|
412
534
|
// the deep length2 levels are where F2 tails burn. Validated on a 44-state
|
|
413
535
|
// corpus: -16% nodes, identical average length (one +1 in 44).
|
|
@@ -421,11 +543,16 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
421
543
|
const EXT_MX = deep ? 20 : 16;
|
|
422
544
|
let p2nodes = 0;
|
|
423
545
|
let f3calls = 0;
|
|
546
|
+
// Bound the whole sweep, not just each individual IDA* call. A hard
|
|
547
|
+
// candidate pool can otherwise spend thousands of capped calls before
|
|
548
|
+
// reaching the deep retry (corpus-73: 114M nodes in the fast attempt).
|
|
549
|
+
const phase3StartNodes = phase3Solver.epochNodes;
|
|
550
|
+
const phase3NodeBudget = deep ? 16_000_000 : 4_000_000;
|
|
424
551
|
function searchPhase2(c, r, maxl, lm, depth, bridgeState, prep) {
|
|
425
552
|
p2nodes++;
|
|
426
553
|
if (p2nodes > P2_MAX_NODES)
|
|
427
554
|
return false;
|
|
428
|
-
if (
|
|
555
|
+
if (c === c2GoalCt && P2_GOAL_RL[r] === 1) {
|
|
429
556
|
if (maxl === 0) {
|
|
430
557
|
while (validWpDepth < depth) {
|
|
431
558
|
const m = moveLog[validWpDepth];
|
|
@@ -434,30 +561,34 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
434
561
|
const dst = wpStack[validWpDepth + 1];
|
|
435
562
|
for (let i = 0; i < 24; i++)
|
|
436
563
|
dst[i] = src[wingMaps[base + i]];
|
|
564
|
+
wpMaskStack[validWpDepth + 1] = pairingMaskOf(dst);
|
|
437
565
|
validWpDepth++;
|
|
438
566
|
}
|
|
439
|
-
|
|
440
|
-
if (process.env.CUBE4_F2_STATS === "1")
|
|
567
|
+
if (collectF2Stats)
|
|
441
568
|
f2Stats.goals++;
|
|
442
|
-
|
|
569
|
+
// Bit-identical to `hasPairingTransversal(goalWp)`: all 12 pair bits
|
|
570
|
+
// distinct ⟺ the 12-bit mask is full (0xfff).
|
|
571
|
+
if (wpMaskStack[depth] !== 0xfff)
|
|
443
572
|
return false;
|
|
444
|
-
if (
|
|
573
|
+
if (collectF2Stats)
|
|
445
574
|
f2Stats.transversalPass++;
|
|
446
|
-
const moves2 =
|
|
575
|
+
const moves2 = [];
|
|
576
|
+
for (let i = 0; i < depth; i++)
|
|
577
|
+
moves2.push(CENTER2_MOVES[moveLog[i]]);
|
|
447
578
|
const s2 = applyCube4Algorithm(bridgeState, moves2);
|
|
448
579
|
const uAxis = s2.centers[0] % 3;
|
|
449
580
|
const rAxis = s2.centers[4] % 3;
|
|
450
581
|
const fAxis = s2.centers[8] % 3;
|
|
451
|
-
if (
|
|
582
|
+
if (collectF2Stats) {
|
|
452
583
|
if (depth < f2Stats.minTransversalL2)
|
|
453
584
|
f2Stats.minTransversalL2 = depth;
|
|
454
585
|
}
|
|
455
586
|
if (axisDet(uAxis, rAxis, fAxis) !== 1) {
|
|
456
|
-
if (
|
|
587
|
+
if (collectF2Stats)
|
|
457
588
|
f2Stats.axisRejects++;
|
|
458
589
|
return false;
|
|
459
590
|
}
|
|
460
|
-
if (
|
|
591
|
+
if (collectF2Stats) {
|
|
461
592
|
if (depth < f2Stats.minChiralL2)
|
|
462
593
|
f2Stats.minChiralL2 = depth;
|
|
463
594
|
}
|
|
@@ -477,7 +608,7 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
477
608
|
const moves2Remapped = prep.totalRot.length === 0
|
|
478
609
|
? moves2
|
|
479
610
|
: rotateAlgorithm(moves2, invertRotationSequence(prep.totalRot));
|
|
480
|
-
const hcCoord = tables.hc ? tables.hc
|
|
611
|
+
const hcCoord = tables.hc ? hcDenseIndex(tables.hc, encodeHcPdb(s2.centers)) : -1;
|
|
481
612
|
const hcRaw = hcCoord >= 0 && tables.hc ? tables.hc.distances[hcCoord] : 0;
|
|
482
613
|
const hdist = hcRaw === 0x0f ? 0 : hcRaw;
|
|
483
614
|
let pdist = 0;
|
|
@@ -524,7 +655,6 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
524
655
|
}
|
|
525
656
|
}
|
|
526
657
|
p1CandCount++;
|
|
527
|
-
const maxP1Cand = options?.p1CandCap !== undefined ? options.p1CandCap : (deep ? 96 : 64);
|
|
528
658
|
if (p1CandCount >= maxP1Cand)
|
|
529
659
|
return true;
|
|
530
660
|
}
|
|
@@ -534,27 +664,65 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
534
664
|
}
|
|
535
665
|
if (maxl === 0)
|
|
536
666
|
return false;
|
|
537
|
-
const c2ctMove = tables.center2.ctMove;
|
|
538
|
-
const c2rlMove = tables.center2.rlMove;
|
|
539
667
|
const c2RowC = c * CENTER2_MOVES.length;
|
|
540
668
|
const c2RowR = r * CENTER2_MOVES.length;
|
|
541
669
|
for (let mi = 0; mi < C2_SEARCH.length; mi++) {
|
|
542
|
-
const m = p2Order[mi];
|
|
543
|
-
if (lm >= 0 &&
|
|
670
|
+
const m = p2Order === null ? mi : p2Order[mi];
|
|
671
|
+
if (lm >= 0 && cancelFlat[lm * c2Stride + m]) {
|
|
544
672
|
continue;
|
|
545
673
|
}
|
|
546
674
|
const nc = c2ctMove[c2RowC + m];
|
|
547
675
|
const nr = c2rlMove[c2RowR + m];
|
|
548
676
|
const prun = c2pruning[nc * CENTER2_RL_STATES + nr];
|
|
549
677
|
if (prun >= maxl) {
|
|
550
|
-
if (prun > maxl &&
|
|
678
|
+
if (prun > maxl && canSkipAxis) {
|
|
551
679
|
mi = SKIP_AXIS_2[m] - 1;
|
|
552
680
|
}
|
|
553
681
|
continue;
|
|
554
682
|
}
|
|
683
|
+
let childPosValid = false;
|
|
684
|
+
if (maxl - 1 < TRANSVERSAL_DEPTH) {
|
|
685
|
+
// Lazy fill (like wpStack): replay the path down to this node.
|
|
686
|
+
while (validPosDepth < depth) {
|
|
687
|
+
const src = posStack[validPosDepth];
|
|
688
|
+
const dst = posStack[validPosDepth + 1];
|
|
689
|
+
const row = moveLog[validPosDepth] * 24;
|
|
690
|
+
for (let w = 0; w < 24; w++)
|
|
691
|
+
dst[w] = wingPosMove[row + src[w]];
|
|
692
|
+
validPosDepth++;
|
|
693
|
+
}
|
|
694
|
+
const parentPos = posStack[depth];
|
|
695
|
+
const childPos = posStack[depth + 1];
|
|
696
|
+
const posRow = m * 24;
|
|
697
|
+
const limit = maxl - 1;
|
|
698
|
+
let blocked = false;
|
|
699
|
+
// Group by group (4 wings = 2 pairs each), so a blocked child only
|
|
700
|
+
// pays for the groups actually checked.
|
|
701
|
+
for (let q = 0; q < 24; q += 4) {
|
|
702
|
+
const a0 = wingPosMove[posRow + parentPos[q]];
|
|
703
|
+
const a1 = wingPosMove[posRow + parentPos[q + 1]];
|
|
704
|
+
const b0 = wingPosMove[posRow + parentPos[q + 2]];
|
|
705
|
+
const b1 = wingPosMove[posRow + parentPos[q + 3]];
|
|
706
|
+
if (transversalPdb[PAIR_INDEX[a0 * 24 + a1] * PAIR_STATES + PAIR_INDEX[b0 * 24 + b1]] > limit) {
|
|
707
|
+
blocked = true;
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
childPos[q] = a0;
|
|
711
|
+
childPos[q + 1] = a1;
|
|
712
|
+
childPos[q + 2] = b0;
|
|
713
|
+
childPos[q + 3] = b1;
|
|
714
|
+
}
|
|
715
|
+
if (blocked)
|
|
716
|
+
continue;
|
|
717
|
+
childPosValid = true;
|
|
718
|
+
}
|
|
555
719
|
moveLog[depth] = m;
|
|
556
720
|
if (validWpDepth > depth)
|
|
557
721
|
validWpDepth = depth;
|
|
722
|
+
if (validPosDepth > depth)
|
|
723
|
+
validPosDepth = depth;
|
|
724
|
+
if (childPosValid)
|
|
725
|
+
validPosDepth = depth + 1;
|
|
558
726
|
if (searchPhase2(nc, nr, maxl - 1, m, depth + 1, bridgeState, prep))
|
|
559
727
|
return true;
|
|
560
728
|
}
|
|
@@ -576,7 +744,8 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
576
744
|
continue;
|
|
577
745
|
const totalRot = [...p1.rotAlign, ...bridge.rotation];
|
|
578
746
|
const prefix = p1.moves;
|
|
579
|
-
const
|
|
747
|
+
const ct = chiral ? encodeCenter2ChiralCt(bridge.state) : bridge.encoded.ct;
|
|
748
|
+
const prun = c2pruning[ct * CENTER2_RL_STATES + bridge.encoded.rl];
|
|
580
749
|
// Paired wings lookahead: check how many wings are already paired in bridge.state
|
|
581
750
|
let pairedWings = 0;
|
|
582
751
|
const wp = bridge.state.wp;
|
|
@@ -588,13 +757,13 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
588
757
|
// Lookahead composite score: total estimated centers penalized by already-paired wings
|
|
589
758
|
const estCenters = prefix.length + prun;
|
|
590
759
|
const score = estCenters * 2 - Math.min(4, pairedWings);
|
|
591
|
-
preps.push({ p1, prefix, bridgeState: bridge.state, totalRot, ct
|
|
760
|
+
preps.push({ p1, prefix, bridgeState: bridge.state, totalRot, ct, rl: bridge.encoded.rl, prun, score });
|
|
592
761
|
}
|
|
593
762
|
// Ascending lookahead score (lowest total estimated center moves + wing bonus),
|
|
594
763
|
// tie-broken by lower phase-2 pruning bound then shorter prefix length
|
|
595
764
|
preps.sort((a, b) => (a.score - b.score) || (a.prun - b.prun) || (a.prefix.length - b.prefix.length));
|
|
596
765
|
// Filter: keep the best preps to avoid wasting phase-2 DFS work on suboptimal centers
|
|
597
|
-
const PREP_CAP = options?.prepCap !== undefined ? options.prepCap : (deep ?
|
|
766
|
+
const PREP_CAP = options?.prepCap !== undefined ? options.prepCap : (deep ? 48 : 32);
|
|
598
767
|
if (preps.length > PREP_CAP) {
|
|
599
768
|
preps.length = PREP_CAP;
|
|
600
769
|
}
|
|
@@ -626,6 +795,10 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
626
795
|
searchedL2.add(key);
|
|
627
796
|
p1CandCount = 0;
|
|
628
797
|
wpStack[0].set(prep.bridgeState.wp);
|
|
798
|
+
for (let p = 0; p < 24; p++)
|
|
799
|
+
posStack[0][prep.bridgeState.wp[p]] = p;
|
|
800
|
+
validPosDepth = 0;
|
|
801
|
+
wpMaskStack[0] = pairingMaskOf(prep.bridgeState.wp);
|
|
629
802
|
validWpDepth = 0;
|
|
630
803
|
searchPhase2(prep.ct, prep.rl, length2, -1, 0, prep.bridgeState, prep);
|
|
631
804
|
if (candidates.length >= CAND_CAP &&
|
|
@@ -660,9 +833,13 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
660
833
|
MAX_P2++;
|
|
661
834
|
} while ((candidates.length === 0 && MAX_P2 <= P2_MAX_ABS) || (candidates.length < CAND_MIN && MAX_P2 <= P2_MAX_MIN));
|
|
662
835
|
if (candidates.length === 0) {
|
|
663
|
-
throw new
|
|
836
|
+
throw new Cube4Phase2CapError("4x4 solve: no valid phase 2 candidates found.");
|
|
664
837
|
}
|
|
665
838
|
const tF2End = performance.now();
|
|
839
|
+
// Cheap-fix goal filter (see Cube4SolveOptions.maxFixMoves). The deep
|
|
840
|
+
// fallback widens it back to 4 (legacy, always satisfiable) so a hard
|
|
841
|
+
// state can never fail on the filter alone.
|
|
842
|
+
const maxFixMoves = (options?.maxFixMoves ?? 4) + (deep ? 2 : 0);
|
|
666
843
|
const validReductions = [];
|
|
667
844
|
/**
|
|
668
845
|
* Per-candidate IDA* proofs shared across all sweep stages: the greatest
|
|
@@ -691,31 +868,50 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
691
868
|
return null;
|
|
692
869
|
const rotToStandard = invertRotationSequence(path);
|
|
693
870
|
const standardReduced = applyCubeRotation(reducedOriginal, rotToStandard);
|
|
694
|
-
const
|
|
695
|
-
if (
|
|
696
|
-
console.log(" toCube3State failed!");
|
|
871
|
+
const rotMoves = rotToStandard.flatMap((r) => ROT_MOVES[r]);
|
|
872
|
+
if (rotMoves.length > maxFixMoves)
|
|
697
873
|
return null;
|
|
874
|
+
if (typeof process !== "undefined" && process.env?.CUBE4_ROT_STATS === "1" && rotStats.length < 200) {
|
|
875
|
+
rotStats.push({
|
|
876
|
+
totalRot: totalRot.join(","),
|
|
877
|
+
rotToStandard: rotToStandard.join(","),
|
|
878
|
+
rotLen: rotToStandard.reduce((n, rr) => n + ROT_MOVES[rr].length, 0)
|
|
879
|
+
});
|
|
698
880
|
}
|
|
881
|
+
// The orientation key only covers 3 of the 24 center slots. A reduced
|
|
882
|
+
// arrangement outside the rotation orbit (e.g. a reflected one) can
|
|
883
|
+
// collide with a rotation's key, in which case rotating by `path` does
|
|
884
|
+
// NOT leave standard centers — the 3x3 finish would solve a different
|
|
885
|
+
// color scheme and the resulting solution would be garbage. Reject.
|
|
886
|
+
for (let i = 0; i < 24; i++) {
|
|
887
|
+
if (standardReduced.centers[i] !== SOLVED_CENTERS[i])
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
const cube3State = toCube3State(standardReduced);
|
|
891
|
+
if (!cube3State)
|
|
892
|
+
return null;
|
|
699
893
|
const h3 = tables.cube3Phase1 ? getTriAxialCube3Heuristic(standardReduced, tables.cube3Phase1) : 0;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
];
|
|
704
|
-
const simplifiedReduction = simplifyCube4Algorithm(reductionMoves);
|
|
894
|
+
// The reduction + rotation prefix as it will be emitted (original frame):
|
|
895
|
+
// apply(state, [...moves12, ...moves3Remapped ... rotMoves]) = standardReduced.
|
|
896
|
+
// Priced with the fix at its best position (see placeRotationFix).
|
|
897
|
+
const simplifiedReduction = placeRotationFix([...cand.moves12, ...moves3Remapped], rotToStandard, []);
|
|
705
898
|
return {
|
|
706
899
|
cand,
|
|
707
900
|
moves3,
|
|
708
901
|
moves3Remapped,
|
|
709
902
|
rotPath: path,
|
|
710
903
|
rotToStandard,
|
|
711
|
-
rotMoves
|
|
904
|
+
rotMoves,
|
|
712
905
|
standardReduced,
|
|
713
906
|
h3,
|
|
714
907
|
totalEst: simplifiedReduction.length + h3
|
|
715
908
|
};
|
|
716
909
|
}
|
|
717
|
-
|
|
718
|
-
|
|
910
|
+
// Defaults 1 × 5 (was 3 × 3): with the continuous finish, extra center
|
|
911
|
+
// candidates stopped paying. cc:0..31: 45.19 moves @ 673 ms vs 45.16 @
|
|
912
|
+
// 1042 ms (2×3: 45.25 @ 799; 1×8: 45.25 @ 691).
|
|
913
|
+
const centersBeam = Math.max(1, options?.centersBeam ?? 1);
|
|
914
|
+
const edgesPerCenter = Math.max(1, options?.edgesPerCenter ?? 5);
|
|
719
915
|
const maxReductionsTarget = options?.finishTries !== undefined
|
|
720
916
|
? Math.max(1, options.finishTries)
|
|
721
917
|
: (centersBeam * edgesPerCenter);
|
|
@@ -733,8 +929,14 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
733
929
|
const candReductions = new Map();
|
|
734
930
|
const distinctCenters = new Set();
|
|
735
931
|
let foundAtLength = null;
|
|
932
|
+
// The rotation fix (rotMoves) is priced INTO totalEst and varies 2-4
|
|
933
|
+
// moves per goal PATH (the end scheme depends on the path). A goal one
|
|
934
|
+
// total-step deeper with a 2-move fix beats a shallow goal with a 4-move
|
|
935
|
+
// fix, so keep collecting a few steps past the first find and let
|
|
936
|
+
// totalEst decide (was: foundAtLength + 1 with needed + 3 goals per
|
|
937
|
+
// call, which discarded fix-cheap alternatives).
|
|
736
938
|
OUTER_P3: for (let length123 = pool[0].value; length123 < L123_CAP; length123++) {
|
|
737
|
-
if (foundAtLength !== null && length123 > foundAtLength +
|
|
939
|
+
if (foundAtLength !== null && length123 > foundAtLength + 3)
|
|
738
940
|
break;
|
|
739
941
|
// Sweep the whole (sorted) pool: see sortTop rationale.
|
|
740
942
|
for (let i = 0; i < pool.length; i++) {
|
|
@@ -755,19 +957,34 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
755
957
|
if (distinctCenters.size >= centersBeam && !distinctCenters.has(cand))
|
|
756
958
|
continue;
|
|
757
959
|
if (cand.edist <= length3 && cand.cdist <= length3 && cand.hdist <= length3 && (cand.pdist ?? 0) <= length3) {
|
|
758
|
-
const
|
|
960
|
+
const remainingNodes = phase3NodeBudget - (phase3Solver.epochNodes - phase3StartNodes);
|
|
961
|
+
if (remainingNodes <= 0) {
|
|
962
|
+
if (validReductions.length > 0)
|
|
963
|
+
return;
|
|
964
|
+
throw new Cube4Phase3CapError("4x4 solve: phase 3 total node budget exhausted.");
|
|
965
|
+
}
|
|
966
|
+
const nodeLimit = Math.min(remainingNodes, length3 <= 12 ? 80000 : Math.max(45000, cfg.maxNodes));
|
|
759
967
|
f3calls++;
|
|
760
|
-
const validsBefore = validReductions.length;
|
|
761
968
|
const needed = edgesPerCenter - candCount;
|
|
762
969
|
const candReductionsBuffer = [];
|
|
763
|
-
|
|
764
|
-
|
|
970
|
+
// Collect beyond `needed`: goal paths at the same length carry
|
|
971
|
+
// different rotation fixes (2 vs 4 moves), and the totalEst sort
|
|
972
|
+
// below needs the variety to pick a cheap-fix end.
|
|
973
|
+
const maxToCollect = Math.min(24, needed + 9);
|
|
974
|
+
const result = phase3Solver.solveLengthProven(cand.edge, cand.ct3, cand.edist, length3, tables.center3, tables.edge3.distances, nodeLimit,
|
|
975
|
+
// Goal budget per call: with the cheap-fix filter most goal ends
|
|
976
|
+
// are rejected, so the walk must be allowed to examine more of
|
|
977
|
+
// them before giving up on this (candidate, length3).
|
|
978
|
+
48, (moves3) => {
|
|
765
979
|
const red = buildValidReduction(cand, moves3);
|
|
766
980
|
if (!red)
|
|
767
981
|
return false;
|
|
768
982
|
candReductionsBuffer.push(red);
|
|
769
|
-
|
|
770
|
-
|
|
983
|
+
// <=-search: goals may land shorter than the length3 bound —
|
|
984
|
+
// track the true total for the post-find collection window.
|
|
985
|
+
const total = cand.length12 + moves3.length;
|
|
986
|
+
if (foundAtLength === null || total < foundAtLength)
|
|
987
|
+
foundAtLength = total;
|
|
771
988
|
return candReductionsBuffer.length >= maxToCollect;
|
|
772
989
|
},
|
|
773
990
|
// hc mid-search prune disabled: on a 44-state corpus the per-node
|
|
@@ -787,20 +1004,18 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
787
1004
|
candReductions.set(cand, candCount + toAdd.length);
|
|
788
1005
|
distinctCenters.add(cand);
|
|
789
1006
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
console.log(`sweep l12=${cand.length12} l3=${length3} nodes=${result.nodesUsed}/${nodeLimit} ` +
|
|
794
|
-
`valid=${validReductions.length > validsBefore} proven=${proven} ` +
|
|
795
|
-
`c=${cand.cdist} e=${cand.edist} h=${cand.hdist}`);
|
|
796
|
-
}
|
|
1007
|
+
// Predicate-scoped proof (same buildValidReduction filter for this
|
|
1008
|
+
// candidate across all its calls): skip every settled length.
|
|
1009
|
+
const proven = result.validatedThrough;
|
|
797
1010
|
if (result.moves === null && proven >= length3) {
|
|
798
1011
|
const prev = p3Proven.get(cand) ?? -1;
|
|
799
1012
|
if (proven > prev)
|
|
800
1013
|
p3Proven.set(cand, proven);
|
|
801
1014
|
}
|
|
802
1015
|
// The finish loop attempts reductions up to maxReductionsTarget
|
|
803
|
-
|
|
1016
|
+
// (with a wider collection cap so cheap-fix ends at later length
|
|
1017
|
+
// steps can still displace expensive-fix ones by totalEst).
|
|
1018
|
+
if (validReductions.length >= maxReductionsTarget + 9) {
|
|
804
1019
|
break OUTER_P3;
|
|
805
1020
|
}
|
|
806
1021
|
}
|
|
@@ -819,10 +1034,10 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
819
1034
|
};
|
|
820
1035
|
// Sweeps with growing budgets; longitudes extend dynamically (reference
|
|
821
1036
|
// style) while nothing is found, instead of fixed caps. Easy states only
|
|
822
|
-
// pay the first sweep.
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
1037
|
+
// pay the first sweep. The length3 caps are NOT redundant re-walks: the
|
|
1038
|
+
// ladder widens the pairing cap ONLY when nothing was found at all, and the
|
|
1039
|
+
// narrower caps keep early sweeps on short pairings (measured: a single
|
|
1040
|
+
// merged sweep with maxLength3 24 is ~20% SLOWER with identical output).
|
|
826
1041
|
// NOTE: lowering these caps was tried (20k/16k) and reverted: capped calls
|
|
827
1042
|
// change which valids are found (±2 moves on the corpus). Only admissible
|
|
828
1043
|
// heuristic strengthenings are length-safe here.
|
|
@@ -865,6 +1080,10 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
865
1080
|
searchedL2.add(key);
|
|
866
1081
|
p1CandCount = 0;
|
|
867
1082
|
wpStack[0].set(prep.bridgeState.wp);
|
|
1083
|
+
for (let p = 0; p < 24; p++)
|
|
1084
|
+
posStack[0][prep.bridgeState.wp[p]] = p;
|
|
1085
|
+
validPosDepth = 0;
|
|
1086
|
+
wpMaskStack[0] = pairingMaskOf(prep.bridgeState.wp);
|
|
868
1087
|
validWpDepth = 0;
|
|
869
1088
|
searchPhase2(prep.ct, prep.rl, length2, -1, 0, prep.bridgeState, prep);
|
|
870
1089
|
if (candidates.length - poolStart >= 40)
|
|
@@ -882,7 +1101,7 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
882
1101
|
}
|
|
883
1102
|
}
|
|
884
1103
|
if (validReductions.length === 0) {
|
|
885
|
-
throw new
|
|
1104
|
+
throw new Cube4Phase3CapError("4x4 solve: phase 3 reduction failed across all candidates.");
|
|
886
1105
|
}
|
|
887
1106
|
// Sort reductions by Total Estimated Moves (Reduction length + 3x3 Phase 1 lower bound)
|
|
888
1107
|
validReductions.sort((a, b) => a.totalEst - b.totalEst);
|
|
@@ -897,28 +1116,56 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
897
1116
|
// Best-of among the first three (phase-4 is cheap now); further
|
|
898
1117
|
// reductions are only attempted when those all failed.
|
|
899
1118
|
const finishTries = options?.finishTries ?? maxReductionsTarget;
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1119
|
+
const collectFinishStats = typeof process !== "undefined" && process.env?.CUBE4_FINISH_STATS === "1";
|
|
1120
|
+
// Empirical finish lower bound (see finishStats): every observed finish
|
|
1121
|
+
// ran 11-16 moves ABOVE `totalEst` (continuous finish: 67 finishes over 16
|
|
1122
|
+
// seeds, gap = totalEst - finalLen in [-16, -11]; the legacy finish was
|
|
1123
|
+
// [-16, -12]) because `h3` only bounds the cube3 PHASE-1 length while the
|
|
1124
|
+
// finish is two-phase. A reduction whose totalEst + 11 cannot strictly
|
|
1125
|
+
// beat the current best therefore cannot win, and its finish is skipped
|
|
1126
|
+
// entirely. If a state ever lands a finish within 11 of the phase-1 bound,
|
|
1127
|
+
// this degrades to a slightly longer (still exact,
|
|
1128
|
+
// still valid) scramble for that state — never an incorrect one.
|
|
1129
|
+
const FINISH_LB_BIAS = 11;
|
|
1130
|
+
const screenTop = Math.max(0, Math.floor(options?.finishScreenTop ?? 0));
|
|
1131
|
+
const screened = [];
|
|
1132
|
+
/** Finishes one reduction; records it as the best when shorter. */
|
|
1133
|
+
async function finishReduction(red, effort) {
|
|
904
1134
|
const cube3State = toCube3State(red.standardReduced);
|
|
905
1135
|
if (!cube3State)
|
|
906
|
-
|
|
1136
|
+
return null;
|
|
907
1137
|
let cube3Solution;
|
|
908
1138
|
try {
|
|
909
|
-
cube3Solution = await solveCube3(cube3State);
|
|
1139
|
+
cube3Solution = await solveCube3(cube3State, effort);
|
|
910
1140
|
}
|
|
911
1141
|
catch {
|
|
912
|
-
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
// Frame invariant (verified): apply(state, [...moves12, ...moves3Remapped])
|
|
1145
|
+
// is exactly `reducedOriginal`, and `reducedOriginal · rotMoves(r)`
|
|
1146
|
+
// (= applyCubeRotation(_, r)) is `standardReduced`, which the cube3
|
|
1147
|
+
// finish C solves. So the solution in the ORIGINAL frame is
|
|
1148
|
+
// X ++ rotMoves(r) ++ C.
|
|
1149
|
+
// Two previously-tried assemblies are WRONG:
|
|
1150
|
+
// - [rot_r(X), C] solves ρ_r(state), not state (the observed adaptive
|
|
1151
|
+
// whole-cube-rotation bug: apply(solved, invert(moves)) = ρ_r(T));
|
|
1152
|
+
// - [X, rot_{-r}(C)] lands on ρ_{-r}(solved) ≠ solved, because a
|
|
1153
|
+
// whole-cube rotation does NOT leave the canonical solved state
|
|
1154
|
+
// invariant (it permutes face colors in the absolute frame).
|
|
1155
|
+
// `placeRotationFix` keeps that effect but may move the rotMoves(r)
|
|
1156
|
+
// block anywhere in the sequence (re-mapping what follows it) where it
|
|
1157
|
+
// cancels against adjacent wide moves.
|
|
1158
|
+
const simplified = placeRotationFix([...red.cand.moves12, ...red.moves3Remapped], red.rotToStandard, cube3Solution.moves);
|
|
1159
|
+
if (collectFinishStats) {
|
|
1160
|
+
finishStats.finishes++;
|
|
1161
|
+
const gap = red.totalEst - simplified.length;
|
|
1162
|
+
if (gap < finishStats.gapMin)
|
|
1163
|
+
finishStats.gapMin = gap;
|
|
1164
|
+
if (gap > finishStats.gapMax)
|
|
1165
|
+
finishStats.gapMax = gap;
|
|
1166
|
+
finishStats.gapSum += gap;
|
|
1167
|
+
finishStats.gapN++;
|
|
913
1168
|
}
|
|
914
|
-
// Rotate the solve algorithm so it is performed from the standard orientation ("hacer los movimientos desde otro lado").
|
|
915
|
-
// This allows the solve to transition directly to the standard solved state WITHOUT ANY WHOLE-CUBE ROTATIONS (rotMoves)!
|
|
916
|
-
const rotatedFullSolution = [
|
|
917
|
-
...rotateAlgorithm(red.cand.moves12, red.rotToStandard),
|
|
918
|
-
...rotateAlgorithm(red.moves3Remapped, red.rotToStandard),
|
|
919
|
-
...cube3Solution.moves
|
|
920
|
-
];
|
|
921
|
-
const simplified = simplifyCube4Algorithm(rotatedFullSolution);
|
|
922
1169
|
if (simplified.length < minTotalMoves) {
|
|
923
1170
|
minTotalMoves = simplified.length;
|
|
924
1171
|
bestSolution = simplified;
|
|
@@ -931,9 +1178,31 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
931
1178
|
};
|
|
932
1179
|
bestRed = red;
|
|
933
1180
|
}
|
|
1181
|
+
return simplified;
|
|
1182
|
+
}
|
|
1183
|
+
for (let i = 0; i < validReductions.length; i++) {
|
|
1184
|
+
if (i >= finishTries && bestSolution)
|
|
1185
|
+
break;
|
|
1186
|
+
const red = validReductions[i];
|
|
1187
|
+
if (bestSolution !== null && red.totalEst + FINISH_LB_BIAS >= minTotalMoves) {
|
|
1188
|
+
finishStats.skipped++;
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const done = await finishReduction(red, screenTop > 0 ? "screen" : "full");
|
|
1192
|
+
if (done)
|
|
1193
|
+
screened.push({ red, total: done.length });
|
|
1194
|
+
}
|
|
1195
|
+
// Stage 2: the best screened reductions get the full-effort finish.
|
|
1196
|
+
if (screenTop > 0) {
|
|
1197
|
+
screened.sort((a, b) => a.total - b.total);
|
|
1198
|
+
for (const { red } of screened.slice(0, screenTop)) {
|
|
1199
|
+
if (red.totalEst + FINISH_LB_BIAS >= minTotalMoves)
|
|
1200
|
+
continue;
|
|
1201
|
+
await finishReduction(red, "full");
|
|
1202
|
+
}
|
|
934
1203
|
}
|
|
935
1204
|
if (!bestSolution || !bestRed) {
|
|
936
|
-
throw new
|
|
1205
|
+
throw new Cube4FinishCapError("4x4 solve: reduction failed during the 3x3 finish across all valid reductions.");
|
|
937
1206
|
}
|
|
938
1207
|
// Polish pass: the sweep takes the first valid sequence per exact
|
|
939
1208
|
// length, which is often one above optimal. Retry the winner one length
|
|
@@ -953,13 +1222,23 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
953
1222
|
// and a full 150k grind on tail states cost seconds for nothing. It is
|
|
954
1223
|
// also skipped entirely unless the total is high enough to matter.
|
|
955
1224
|
const polishBudget = r === bestRed ? 150000 : 30000;
|
|
1225
|
+
// NOTE: with the <=-length phase-3 search the sweep already enumerates
|
|
1226
|
+
// shorter goals, so the polish is a second-chance pass only (measured:
|
|
1227
|
+
// a wider gate/budget found nothing and cost +32% p3 nodes).
|
|
956
1228
|
for (let iter = 0; iter < 4 && (minTotalMoves > 43 || curRed.moves3.length > 11); iter++) {
|
|
957
1229
|
const cand = curRed.cand;
|
|
958
1230
|
const shorterLen = curRed.moves3.length - 1;
|
|
959
1231
|
if (shorterLen < cand.edist || shorterLen < cand.cdist || shorterLen < cand.hdist || shorterLen < (cand.pdist ?? 0))
|
|
960
1232
|
break;
|
|
961
1233
|
f3calls++;
|
|
962
|
-
|
|
1234
|
+
// Unified polish: accept any goal path with a strictly better
|
|
1235
|
+
// totalEst at the same or shorter length — covers both a shorter
|
|
1236
|
+
// phase-3 path and a cheaper rotation fix (the fix is priced into
|
|
1237
|
+
// totalEst and varies 2-4 moves per goal path).
|
|
1238
|
+
const shorter = phase3Solver.solveLengthValid(cand.edge, cand.ct3, cand.edist, curRed.moves3.length, tables.center3, tables.edge3.distances, polishBudget, 64, (moves3) => {
|
|
1239
|
+
const nr = buildValidReduction(cand, moves3);
|
|
1240
|
+
return nr !== null && nr.totalEst < curRed.totalEst;
|
|
1241
|
+
}, tables.hc, cand.hcCoord, tables.c3PairDist);
|
|
963
1242
|
if (!shorter)
|
|
964
1243
|
break;
|
|
965
1244
|
const red = buildValidReduction(cand, shorter);
|
|
@@ -975,11 +1254,8 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
975
1254
|
catch {
|
|
976
1255
|
break;
|
|
977
1256
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
...rotateAlgorithm(red.moves3Remapped, red.rotToStandard),
|
|
981
|
-
...cube3Solution.moves
|
|
982
|
-
]);
|
|
1257
|
+
// Same frame invariant as the finish loop: X ++ rotMoves(r) ++ C.
|
|
1258
|
+
const simplified = placeRotationFix([...red.cand.moves12, ...red.moves3Remapped], red.rotToStandard, cube3Solution.moves);
|
|
983
1259
|
if (simplified.length < minTotalMoves) {
|
|
984
1260
|
minTotalMoves = simplified.length;
|
|
985
1261
|
bestSolution = simplified;
|
|
@@ -1004,13 +1280,19 @@ export async function solveCube4State(state, tables, solveCube3, options) {
|
|
|
1004
1280
|
accF3Ms += tFinishStart - tF2End;
|
|
1005
1281
|
accFinishMs += tPolishStart - tFinishStart;
|
|
1006
1282
|
accPolishMs += performance.now() - tPolishStart;
|
|
1283
|
+
// Final invariant: the emitted solution must solve the INPUT state
|
|
1284
|
+
// exactly (no rotation tolerance). Fail loudly rather than return a
|
|
1285
|
+
// solution that does not resolve. One replay of ~45 moves — negligible
|
|
1286
|
+
// next to the search cost.
|
|
1287
|
+
if (!isSolvedCube4State(applyCube4Algorithm(state, bestSolution))) {
|
|
1288
|
+
throw new Error("4x4 solve: internal verification failed — the solution does not solve the input state.");
|
|
1289
|
+
}
|
|
1007
1290
|
return { moves: bestSolution, phases: bestPhases };
|
|
1008
1291
|
};
|
|
1009
1292
|
let bestMoves = null;
|
|
1010
1293
|
let bestPhasesOverall;
|
|
1011
1294
|
let lastRestartError = null;
|
|
1012
1295
|
for (let restart = 0; restart < totalRestarts; restart++) {
|
|
1013
|
-
FastPhase3Solver.nextEpoch();
|
|
1014
1296
|
const rng = totalRestarts > 1
|
|
1015
1297
|
? mulberry32((hashCube4State(state) ^ Math.imul(restart + 1, 0x9e3779b9)) >>> 0)
|
|
1016
1298
|
: null;
|