@cubingcenter/scrambler 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/puzzles/cube2/solver/search.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/search.js +3 -3
- package/dist/puzzles/cube2/solver/tables.d.ts +6 -0
- package/dist/puzzles/cube2/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/tables.js +16 -2
- package/dist/puzzles/cube3/index.d.ts +1 -0
- package/dist/puzzles/cube3/index.d.ts.map +1 -1
- package/dist/puzzles/cube3/moves.d.ts.map +1 -1
- package/dist/puzzles/cube3/moves.js +104 -18
- package/dist/puzzles/cube3/scrambler.d.ts +16 -1
- package/dist/puzzles/cube3/scrambler.d.ts.map +1 -1
- package/dist/puzzles/cube3/scrambler.js +1 -1
- package/dist/puzzles/cube3/solver/browser-loader.js +6 -2
- package/dist/puzzles/cube3/solver/index.d.ts +5 -3
- package/dist/puzzles/cube3/solver/index.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/index.js +2 -1
- package/dist/puzzles/cube3/solver/node-loader.js +9 -2
- package/dist/puzzles/cube3/solver/phase1.d.ts +18 -0
- package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase1.js +132 -14
- package/dist/puzzles/cube3/solver/phase2.d.ts +47 -0
- package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase2.js +236 -66
- package/dist/puzzles/cube3/solver/search-utils.d.ts +1 -1
- package/dist/puzzles/cube3/solver/search-utils.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/symmetry.d.ts +24 -0
- package/dist/puzzles/cube3/solver/symmetry.d.ts.map +1 -0
- package/dist/puzzles/cube3/solver/symmetry.js +74 -0
- package/dist/puzzles/cube3/solver/tables.d.ts +6 -1
- package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/cube3/validation.d.ts.map +1 -1
- package/dist/puzzles/cube3/validation.js +15 -7
- package/dist/puzzles/cube4/index.d.ts +3 -2
- package/dist/puzzles/cube4/index.d.ts.map +1 -1
- package/dist/puzzles/cube4/index.js +2 -2
- package/dist/puzzles/cube4/moves.d.ts +5 -0
- package/dist/puzzles/cube4/moves.d.ts.map +1 -1
- package/dist/puzzles/cube4/moves.js +120 -11
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts +3 -0
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/browser-loader.js +34 -3
- package/dist/puzzles/cube4/reduction/center2.d.ts +20 -3
- package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/center2.js +53 -9
- package/dist/puzzles/cube4/reduction/edge3.d.ts +4 -0
- package/dist/puzzles/cube4/reduction/edge3.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/edge3.js +23 -3
- package/dist/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
- package/dist/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
- package/dist/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
- package/dist/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
- package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +38 -0
- package/dist/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +50 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.js +202 -0
- package/dist/puzzles/cube4/reduction/node-loader.d.ts +5 -0
- package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/node-loader.js +48 -8
- package/dist/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts +111 -5
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/phase3-search.js +341 -22
- package/dist/puzzles/cube4/reduction/solve.d.ts +112 -1
- package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/solve.js +920 -181
- package/dist/puzzles/cube4/scrambler.d.ts +105 -4
- package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
- package/dist/puzzles/cube4/scrambler.js +216 -166
- package/dist/puzzles/cube4/state.d.ts.map +1 -1
- package/dist/puzzles/cube4/state.js +4 -1
- package/dist/puzzles/pyraminx/solver/search.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/search.js +3 -6
- package/dist/puzzles/pyraminx/solver/tables.d.ts +6 -0
- package/dist/puzzles/pyraminx/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/tables.js +17 -0
- package/dist/puzzles/skewb/solver/search.js +3 -3
- package/dist/puzzles/skewb/solver/tables.d.ts +6 -0
- package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/skewb/solver/tables.js +17 -0
- package/dist-cjs/puzzles/cube2/solver/search.js +2 -2
- package/dist-cjs/puzzles/cube2/solver/tables.js +17 -3
- package/dist-cjs/puzzles/cube3/moves.js +104 -18
- package/dist-cjs/puzzles/cube3/scrambler.js +1 -1
- package/dist-cjs/puzzles/cube3/solver/browser-loader.js +6 -2
- package/dist-cjs/puzzles/cube3/solver/index.js +8 -1
- package/dist-cjs/puzzles/cube3/solver/node-loader.js +9 -2
- package/dist-cjs/puzzles/cube3/solver/phase1.js +133 -14
- package/dist-cjs/puzzles/cube3/solver/phase2.js +233 -63
- package/dist-cjs/puzzles/cube3/solver/symmetry.js +80 -0
- package/dist-cjs/puzzles/cube3/validation.js +15 -7
- package/dist-cjs/puzzles/cube4/index.js +3 -1
- package/dist-cjs/puzzles/cube4/moves.js +121 -11
- package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +34 -3
- package/dist-cjs/puzzles/cube4/reduction/center2.js +54 -9
- package/dist-cjs/puzzles/cube4/reduction/edge3.js +24 -3
- package/dist-cjs/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +38 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
- package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +213 -0
- package/dist-cjs/puzzles/cube4/reduction/node-loader.js +48 -8
- package/dist-cjs/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
- package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +343 -23
- package/dist-cjs/puzzles/cube4/reduction/solve.js +918 -173
- package/dist-cjs/puzzles/cube4/scrambler.js +214 -163
- package/dist-cjs/puzzles/cube4/state.js +4 -1
- package/dist-cjs/puzzles/pyraminx/solver/search.js +2 -5
- package/dist-cjs/puzzles/pyraminx/solver/tables.js +18 -1
- package/dist-cjs/puzzles/skewb/solver/search.js +2 -2
- package/dist-cjs/puzzles/skewb/solver/tables.js +18 -1
- package/package.json +14 -5
|
@@ -5,17 +5,114 @@
|
|
|
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;
|
|
9
|
+
exports.rotateAlgorithm = rotateAlgorithm;
|
|
10
|
+
exports.invertRotationSequence = invertRotationSequence;
|
|
11
|
+
exports.applyCubeRotation = applyCubeRotation;
|
|
12
|
+
exports.resetF2Stats = resetF2Stats;
|
|
13
|
+
exports.resetStagnationStats = resetStagnationStats;
|
|
8
14
|
exports.solveCube4State = solveCube4State;
|
|
9
15
|
const moves_js_1 = require("../moves.js");
|
|
10
16
|
const center1_js_1 = require("./center1.js");
|
|
11
17
|
const center2_js_1 = require("./center2.js");
|
|
12
18
|
const center3_js_1 = require("./center3.js");
|
|
13
19
|
const pairing_coordinate_js_1 = require("./pairing-coordinate.js");
|
|
20
|
+
const hc_pdb_js_1 = require("./hc-pdb.js");
|
|
14
21
|
const edge3_js_1 = require("./edge3.js");
|
|
15
22
|
const phase3_search_js_1 = require("./phase3-search.js");
|
|
16
23
|
const phase4_js_1 = require("./phase4.js");
|
|
17
|
-
const
|
|
24
|
+
const coordinates_js_1 = require("../../cube3/solver/coordinates.js");
|
|
18
25
|
const state_js_1 = require("../state.js");
|
|
26
|
+
function mulberry32(seed) {
|
|
27
|
+
let a = seed >>> 0;
|
|
28
|
+
return () => {
|
|
29
|
+
a |= 0;
|
|
30
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
31
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
32
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
33
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function hashCube4State(state) {
|
|
37
|
+
let h = 0x811c9dc5;
|
|
38
|
+
for (const arr of [state.cp, state.co, state.wp, state.centers]) {
|
|
39
|
+
for (let i = 0; i < arr.length; i++) {
|
|
40
|
+
h ^= arr[i] & 0xff;
|
|
41
|
+
h = Math.imul(h, 0x01000193);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return h >>> 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fisher-Yates order, or identity when rng is null (restarts=1 keeps the
|
|
48
|
+
* legacy deterministic traversal bit-identical).
|
|
49
|
+
*/
|
|
50
|
+
function restartOrder(n, rng) {
|
|
51
|
+
const order = Array.from({ length: n }, (_, i) => i);
|
|
52
|
+
if (rng) {
|
|
53
|
+
for (let i = n - 1; i > 0; i--) {
|
|
54
|
+
const j = Math.floor(rng() * (i + 1));
|
|
55
|
+
const t = order[i];
|
|
56
|
+
order[i] = order[j];
|
|
57
|
+
order[j] = t;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return order;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Fisher-Yates within equal-key runs of a key-sorted array, so the
|
|
64
|
+
* caller's sorted early-break (`value > length123`) stays valid.
|
|
65
|
+
*/
|
|
66
|
+
function shuffleTies(arr, key, rng) {
|
|
67
|
+
if (!rng)
|
|
68
|
+
return;
|
|
69
|
+
let run = 0;
|
|
70
|
+
for (let i = 1; i <= arr.length; i++) {
|
|
71
|
+
if (i < arr.length && key(arr[i]) === key(arr[run]))
|
|
72
|
+
continue;
|
|
73
|
+
for (let k = i - 1; k > run; k--) {
|
|
74
|
+
const j = run + Math.floor(rng() * (k - run + 1));
|
|
75
|
+
const t = arr[k];
|
|
76
|
+
arr[k] = arr[j];
|
|
77
|
+
arr[j] = t;
|
|
78
|
+
}
|
|
79
|
+
run = i;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function getCube3Phase1Heuristic(cube3State, tables) {
|
|
83
|
+
const co = (0, coordinates_js_1.encodeCornerOrientation)(cube3State);
|
|
84
|
+
const eo = (0, coordinates_js_1.encodeEdgeOrientation)(cube3State);
|
|
85
|
+
const ud = (0, coordinates_js_1.encodeUdSlice)(cube3State);
|
|
86
|
+
const combined = (co << 11) + eo;
|
|
87
|
+
const coeo = (tables.pruningTables.cornerOrientationEdgeOrientation.values[combined >> 1] >>
|
|
88
|
+
((combined & 1) << 2)) &
|
|
89
|
+
0x0f;
|
|
90
|
+
const udd = (tables.pruningTables.udSlice.values[ud >> 1] >> ((ud & 1) << 2)) & 0x0f;
|
|
91
|
+
return Math.max(coeo, udd);
|
|
92
|
+
}
|
|
93
|
+
function getTriAxialCube3Heuristic(standardReduced, tables) {
|
|
94
|
+
const s0 = (0, phase4_js_1.toCube3State)(standardReduced);
|
|
95
|
+
if (!s0)
|
|
96
|
+
return 99;
|
|
97
|
+
let minH = getCube3Phase1Heuristic(s0, tables);
|
|
98
|
+
if (minH <= 3)
|
|
99
|
+
return minH;
|
|
100
|
+
const sx = (0, phase4_js_1.toCube3State)(applyCubeRotation(standardReduced, ["x"]));
|
|
101
|
+
if (sx) {
|
|
102
|
+
const hx = getCube3Phase1Heuristic(sx, tables);
|
|
103
|
+
if (hx < minH)
|
|
104
|
+
minH = hx;
|
|
105
|
+
if (minH <= 3)
|
|
106
|
+
return minH;
|
|
107
|
+
}
|
|
108
|
+
const sz = (0, phase4_js_1.toCube3State)(applyCubeRotation(standardReduced, ["z"]));
|
|
109
|
+
if (sz) {
|
|
110
|
+
const hz = getCube3Phase1Heuristic(sz, tables);
|
|
111
|
+
if (hz < minH)
|
|
112
|
+
minH = hz;
|
|
113
|
+
}
|
|
114
|
+
return minH;
|
|
115
|
+
}
|
|
19
116
|
function cornerPermutationParity(cp) {
|
|
20
117
|
let parity = 0;
|
|
21
118
|
const seen = new Array(8).fill(false);
|
|
@@ -33,13 +130,48 @@ function cornerPermutationParity(cp) {
|
|
|
33
130
|
}
|
|
34
131
|
return parity;
|
|
35
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Computes the spatial determinant (+1 for valid 3D rotations, -1 for reflections)
|
|
135
|
+
* of the 3 axes defined by the center colors on faces U, R, F.
|
|
136
|
+
*/
|
|
137
|
+
function getOrientationDet(uColor, rColor, fColor) {
|
|
138
|
+
const vec = (c) => {
|
|
139
|
+
switch (c) {
|
|
140
|
+
case 0: return [0, 0, 1];
|
|
141
|
+
case 3: return [0, 0, -1];
|
|
142
|
+
case 1: return [1, 0, 0];
|
|
143
|
+
case 4: return [-1, 0, 0];
|
|
144
|
+
case 2: return [0, 1, 0];
|
|
145
|
+
case 5: return [0, -1, 0];
|
|
146
|
+
default: return [0, 0, 0];
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const [ux, uy, uz] = vec(uColor);
|
|
150
|
+
const [rx, ry, rz] = vec(rColor);
|
|
151
|
+
const [fx, fy, fz] = vec(fColor);
|
|
152
|
+
const cx = ry * fz - rz * fy;
|
|
153
|
+
const cy = rz * fx - rx * fz;
|
|
154
|
+
const cz = rx * fy - ry * fx;
|
|
155
|
+
return cx * ux + cy * uy + cz * uz;
|
|
156
|
+
}
|
|
157
|
+
function axisDet(uAxis, rAxis, fAxis) {
|
|
158
|
+
if ((uAxis === 0 && rAxis === 1 && fAxis === 2) ||
|
|
159
|
+
(uAxis === 1 && rAxis === 2 && fAxis === 0) ||
|
|
160
|
+
(uAxis === 2 && rAxis === 0 && fAxis === 1)) {
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
return -1;
|
|
164
|
+
}
|
|
36
165
|
const ROT_MOVES = {
|
|
37
166
|
"x": ["Rw", "Lw'"],
|
|
38
167
|
"y": ["Uw", "Dw'"],
|
|
39
168
|
"z": ["Fw", "Bw'"],
|
|
40
169
|
"x'": ["Rw'", "Lw"],
|
|
41
170
|
"y'": ["Uw'", "Dw"],
|
|
42
|
-
"z'": ["Fw'", "Bw"]
|
|
171
|
+
"z'": ["Fw'", "Bw"],
|
|
172
|
+
"x2": ["Rw2", "Lw2"],
|
|
173
|
+
"y2": ["Uw2", "Dw2"],
|
|
174
|
+
"z2": ["Fw2", "Bw2"]
|
|
43
175
|
};
|
|
44
176
|
const VALID_24_ORIENTATIONS = (() => {
|
|
45
177
|
const map = new Map();
|
|
@@ -66,12 +198,16 @@ const ROT_MAP = {
|
|
|
66
198
|
"y": { "U": "U", "R": "F", "F": "L", "D": "D", "L": "B", "B": "R" },
|
|
67
199
|
"y'": { "U": "U", "R": "B", "F": "R", "D": "D", "L": "F", "B": "L" },
|
|
68
200
|
"z": { "U": "R", "R": "D", "F": "F", "D": "L", "L": "U", "B": "B" },
|
|
69
|
-
"z'": { "U": "L", "R": "U", "F": "F", "D": "R", "L": "D", "B": "B" }
|
|
201
|
+
"z'": { "U": "L", "R": "U", "F": "F", "D": "R", "L": "D", "B": "B" },
|
|
202
|
+
"x2": { "U": "D", "R": "R", "F": "B", "D": "U", "L": "L", "B": "F" },
|
|
203
|
+
"y2": { "U": "U", "R": "L", "F": "B", "D": "D", "L": "R", "B": "F" },
|
|
204
|
+
"z2": { "U": "D", "R": "L", "F": "F", "D": "U", "L": "R", "B": "B" }
|
|
70
205
|
};
|
|
71
206
|
const INVERT_ROT = {
|
|
72
207
|
"x": "x'", "x'": "x",
|
|
73
208
|
"y": "y'", "y'": "y",
|
|
74
|
-
"z": "z'", "z'": "z"
|
|
209
|
+
"z": "z'", "z'": "z",
|
|
210
|
+
"x2": "x2", "y2": "y2", "z2": "z2"
|
|
75
211
|
};
|
|
76
212
|
function rotateMove(move, rot) {
|
|
77
213
|
const map = ROT_MAP[rot];
|
|
@@ -80,11 +216,16 @@ function rotateMove(move, rot) {
|
|
|
80
216
|
return (newFace + move.slice(1));
|
|
81
217
|
}
|
|
82
218
|
function rotateAlgorithm(alg, rotSeq) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
219
|
+
// Single pass over the algorithm (one allocation): applying the rotation
|
|
220
|
+
// sequence per move instead of re-mapping the whole array per rotation.
|
|
221
|
+
if (rotSeq.length === 0)
|
|
222
|
+
return [...alg];
|
|
223
|
+
return alg.map((move) => {
|
|
224
|
+
let current = move;
|
|
225
|
+
for (const rot of rotSeq)
|
|
226
|
+
current = rotateMove(current, rot);
|
|
227
|
+
return current;
|
|
228
|
+
});
|
|
88
229
|
}
|
|
89
230
|
function invertRotationSequence(rotSeq) {
|
|
90
231
|
return [...rotSeq].reverse().map((r) => INVERT_ROT[r]);
|
|
@@ -119,196 +260,800 @@ function isCancelled(previous, current) {
|
|
|
119
260
|
}
|
|
120
261
|
return false;
|
|
121
262
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for (let m = 0; m < center1_js_1.CENTER1_MOVES.length; m++) {
|
|
142
|
-
if (lm >= 0 && Math.floor(lm / 3) === Math.floor(m / 3))
|
|
143
|
-
continue;
|
|
144
|
-
const next = (0, center1_js_1.applyCenter1Move)(mask, tables.moveMaps, m);
|
|
145
|
-
path[depth] = m;
|
|
146
|
-
if (dfs(next, depth + 1, m))
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
return false;
|
|
150
|
-
};
|
|
151
|
-
if (dfs(startMask, 0, -1))
|
|
263
|
+
// Precomputed phase-2 cancellation matrix over CENTER2_SEARCH_MOVES: the
|
|
264
|
+
// search evaluates isCancelled per child per node (~0.3M nodes × 23 moves
|
|
265
|
+
// per attempt with regex + Map lookups); the table makes it one boolean
|
|
266
|
+
// read with a bit-identical traversal.
|
|
267
|
+
exports.P2_CANCEL = center2_js_1.CENTER2_SEARCH_MOVES.map((previous) => center2_js_1.CENTER2_SEARCH_MOVES.map((current) => isCancelled(previous, current)));
|
|
268
|
+
// Same over the wide 28-move set (used when `center2Wide` is set).
|
|
269
|
+
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)));
|
|
270
|
+
const SKIP_AXIS_2 = new Int32Array(center2_js_1.CENTER2_SEARCH_MOVES.length);
|
|
271
|
+
for (let i = 0; i < center2_js_1.CENTER2_SEARCH_MOVES.length; i++) {
|
|
272
|
+
const m1 = center2_js_1.CENTER2_SEARCH_MOVES[i];
|
|
273
|
+
const face1 = m1.replace(/2|'|w/g, "");
|
|
274
|
+
const isWide1 = m1.includes("w");
|
|
275
|
+
SKIP_AXIS_2[i] = center2_js_1.CENTER2_SEARCH_MOVES.length;
|
|
276
|
+
for (let j = i + 1; j < center2_js_1.CENTER2_SEARCH_MOVES.length; j++) {
|
|
277
|
+
const m2 = center2_js_1.CENTER2_SEARCH_MOVES[j];
|
|
278
|
+
const face2 = m2.replace(/2|'|w/g, "");
|
|
279
|
+
const isWide2 = m2.includes("w");
|
|
280
|
+
if (face1 !== face2 || isWide1 !== isWide2) {
|
|
281
|
+
SKIP_AXIS_2[i] = j;
|
|
152
282
|
break;
|
|
283
|
+
}
|
|
153
284
|
}
|
|
154
|
-
return solutions;
|
|
155
285
|
}
|
|
156
286
|
const phase3Solver = new phase3_search_js_1.FastPhase3Solver();
|
|
157
|
-
/**
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
287
|
+
/** Temporary instrumentation for F2 goal statistics (CUBE4_F2_STATS=1). */
|
|
288
|
+
exports.f2Stats = { goals: 0, transversalPass: 0, axisRejects: 0, minTransversalL2: 99, minChiralL2: 99 };
|
|
289
|
+
function resetF2Stats() {
|
|
290
|
+
exports.f2Stats.goals = 0;
|
|
291
|
+
exports.f2Stats.transversalPass = 0;
|
|
292
|
+
exports.f2Stats.axisRejects = 0;
|
|
293
|
+
exports.f2Stats.minTransversalL2 = 99;
|
|
294
|
+
exports.f2Stats.minChiralL2 = 99;
|
|
295
|
+
}
|
|
296
|
+
/** Trigger rate for the stagnationCut early-stop (checks vs actual cuts). */
|
|
297
|
+
exports.stagnationStats = { checks: 0, cuts: 0 };
|
|
298
|
+
function resetStagnationStats() {
|
|
299
|
+
exports.stagnationStats.checks = 0;
|
|
300
|
+
exports.stagnationStats.cuts = 0;
|
|
301
|
+
}
|
|
302
|
+
async function solveCube4State(state, tables, solveCube3, options) {
|
|
303
|
+
// NOTE: no wall-clock deadline inside the search — all loops are bounded
|
|
304
|
+
// by deterministic work caps (node counts, candidate counts, length
|
|
305
|
+
// caps), so the same state always takes the same path. The caller keeps
|
|
306
|
+
// its own backstop (`timeoutMs` handled at the call site).
|
|
162
307
|
const sym = (0, edge3_js_1.getSymTables)();
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
308
|
+
phase3Solver.resetEpochNodes();
|
|
309
|
+
const totalRestarts = Math.max(1, Math.floor(options?.restarts ?? 1));
|
|
310
|
+
// Cross-restart work accumulators (reported in nodes/timings).
|
|
311
|
+
let accP2 = 0;
|
|
312
|
+
let accF3calls = 0;
|
|
313
|
+
let accF1Ms = 0;
|
|
314
|
+
let accF2Ms = 0;
|
|
315
|
+
let accF3Ms = 0;
|
|
316
|
+
let accFinishMs = 0;
|
|
317
|
+
let accPolishMs = 0;
|
|
318
|
+
const runOnce = async (rng) => {
|
|
319
|
+
const tR0 = performance.now();
|
|
320
|
+
const p1Sols = [];
|
|
321
|
+
let minOverallH = 99;
|
|
322
|
+
const urfDistances = [];
|
|
323
|
+
for (const urf of [0, 1, 2]) {
|
|
324
|
+
const mask = (0, center1_js_1.center1Mask)(state.centers, urf);
|
|
325
|
+
const h = (0, center1_js_1.getCenter1Distance)(mask, tables.center1);
|
|
326
|
+
urfDistances.push(h);
|
|
327
|
+
if (h < minOverallH)
|
|
328
|
+
minOverallH = h;
|
|
329
|
+
}
|
|
330
|
+
const path = [];
|
|
331
|
+
const p1Extra = options?.phase1ExtraDepths ?? 3;
|
|
332
|
+
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);
|
|
335
|
+
for (let maxD = minOverallH; maxD <= minOverallH + p1Extra; maxD++) {
|
|
336
|
+
for (const urf of [0, 1, 2]) {
|
|
337
|
+
const minH = urfDistances[urf];
|
|
338
|
+
if (maxD < minH)
|
|
339
|
+
continue;
|
|
340
|
+
const mask = (0, center1_js_1.center1Mask)(state.centers, urf);
|
|
341
|
+
let axisCount = 0;
|
|
342
|
+
const dfs = (m, depth, lm) => {
|
|
343
|
+
if (axisCount >= p1Cap)
|
|
344
|
+
return true;
|
|
345
|
+
const h = (0, center1_js_1.getCenter1Distance)(m, tables.center1);
|
|
346
|
+
if (h === 0) {
|
|
347
|
+
let rotAlign = [];
|
|
348
|
+
if (m === center1_js_1.CENTER1_GOAL_MASKS.rl) {
|
|
349
|
+
rotAlign = [];
|
|
350
|
+
}
|
|
351
|
+
else if (m === center1_js_1.CENTER1_GOAL_MASKS.ud) {
|
|
352
|
+
rotAlign = ["z"];
|
|
353
|
+
}
|
|
354
|
+
else if (m === center1_js_1.CENTER1_GOAL_MASKS.fb) {
|
|
355
|
+
rotAlign = ["y'"];
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
axisCount++;
|
|
361
|
+
p1Sols.push({
|
|
362
|
+
moves: path.slice(0, depth).map((mv) => center1_js_1.CENTER1_MOVES[mv]),
|
|
363
|
+
length: depth,
|
|
364
|
+
rotAlign
|
|
193
365
|
});
|
|
194
|
-
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
if (depth + h > maxD)
|
|
369
|
+
return false;
|
|
370
|
+
for (let mi = 0; mi < center1_js_1.CENTER1_MOVES.length; mi++) {
|
|
371
|
+
const mv = p1Order[mi];
|
|
372
|
+
if (lm >= 0 && Math.floor(lm / 3) === Math.floor(mv / 3))
|
|
373
|
+
continue;
|
|
374
|
+
const next = (0, center1_js_1.applyCenter1Move)(m, tables.center1.moveMaps, mv);
|
|
375
|
+
path[depth] = mv;
|
|
376
|
+
if (dfs(next, depth + 1, mv))
|
|
195
377
|
return true;
|
|
196
378
|
}
|
|
379
|
+
return false;
|
|
380
|
+
};
|
|
381
|
+
dfs(mask, 0, -1);
|
|
382
|
+
}
|
|
383
|
+
if (p1Sols.length >= p1Cap)
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
p1Sols.sort((a, b) => a.moves.length - b.moves.length);
|
|
387
|
+
const minP1 = p1Sols[0]?.moves.length ?? 0;
|
|
388
|
+
const tF1End = performance.now();
|
|
389
|
+
const candidates = [];
|
|
390
|
+
const moveLog = new Array(25);
|
|
391
|
+
// Incremental wing tracking stack for phase-2 goal transversal checks:
|
|
392
|
+
// avoids full 6-7 move replaying from scratch across hundreds of thousands of candidate goals.
|
|
393
|
+
const wpStack = Array.from({ length: 24 }, () => new Uint8Array(24));
|
|
394
|
+
let validWpDepth = 0;
|
|
395
|
+
const wingMaps = (0, center2_js_1.getWingMoveMaps)();
|
|
396
|
+
/**
|
|
397
|
+
* Anti-hang work cap for the phase-2 search (successful attempts use
|
|
398
|
+
* ~0.1-0.3M nodes; this only aborts pathological explorations).
|
|
399
|
+
* Exceeding it degrades gracefully: the call returns false and the
|
|
400
|
+
* remaining (prep, length2) combinations still run.
|
|
401
|
+
*/
|
|
402
|
+
const deep = options?.deep === true;
|
|
403
|
+
// Wide phase-2 set (28 moves) + its pruning table. Both sets are
|
|
404
|
+
// prefixes of CENTER2_MOVES, so search indices stay valid move-table
|
|
405
|
+
// indices in either mode.
|
|
406
|
+
const c2Wide = options?.center2Wide === true;
|
|
407
|
+
const C2_SEARCH = c2Wide ? center2_js_1.CENTER2_SEARCH_MOVES_WIDE : center2_js_1.CENTER2_SEARCH_MOVES;
|
|
408
|
+
const C2_CANCEL = c2Wide ? P2_CANCEL_WIDE : exports.P2_CANCEL;
|
|
409
|
+
// Per-restart child order (identity when rng is null). Entries stay
|
|
410
|
+
// original move-table indices, so all table lookups are unaffected.
|
|
411
|
+
const p2Order = restartOrder(C2_SEARCH.length, rng);
|
|
412
|
+
let c2pruning = tables.center2.pruning;
|
|
413
|
+
if (c2Wide) {
|
|
414
|
+
if (!tables.center2.pruningWide) {
|
|
415
|
+
throw new Error("4x4 solve: center2Wide requested but pruningWide tables are not loaded — regenerate center2 tables.");
|
|
416
|
+
}
|
|
417
|
+
c2pruning = tables.center2.pruningWide;
|
|
418
|
+
}
|
|
419
|
+
const P2_MAX_NODES = deep ? 24_000_000 : 8_000_000;
|
|
420
|
+
// Smaller pool + shorter extension (was 180/100/14/12 on the fast path):
|
|
421
|
+
// the deep length2 levels are where F2 tails burn. Validated on a 44-state
|
|
422
|
+
// corpus: -16% nodes, identical average length (one +1 in 44).
|
|
423
|
+
const CAND_CAP = deep ? 300 : 120;
|
|
424
|
+
const CAND_MIN = deep ? 180 : 35;
|
|
425
|
+
const P2_MAX_ABS = deep ? 18 : 14;
|
|
426
|
+
const P2_MAX_MIN = deep ? 16 : 10;
|
|
427
|
+
const L123_CAP = options?.maxReductionLen
|
|
428
|
+
? Math.min(options.maxReductionLen, deep ? 48 : 45)
|
|
429
|
+
: (deep ? 48 : 45);
|
|
430
|
+
const EXT_MX = deep ? 20 : 16;
|
|
431
|
+
let p2nodes = 0;
|
|
432
|
+
let f3calls = 0;
|
|
433
|
+
function searchPhase2(c, r, maxl, lm, depth, bridgeState, prep) {
|
|
434
|
+
p2nodes++;
|
|
435
|
+
if (p2nodes > P2_MAX_NODES)
|
|
436
|
+
return false;
|
|
437
|
+
if ((0, center2_js_1.isCenter2Goal)(c, r)) {
|
|
438
|
+
if (maxl === 0) {
|
|
439
|
+
while (validWpDepth < depth) {
|
|
440
|
+
const m = moveLog[validWpDepth];
|
|
441
|
+
const base = m * 24;
|
|
442
|
+
const src = wpStack[validWpDepth];
|
|
443
|
+
const dst = wpStack[validWpDepth + 1];
|
|
444
|
+
for (let i = 0; i < 24; i++)
|
|
445
|
+
dst[i] = src[wingMaps[base + i]];
|
|
446
|
+
validWpDepth++;
|
|
447
|
+
}
|
|
448
|
+
const goalWp = wpStack[depth];
|
|
449
|
+
if (process.env.CUBE4_F2_STATS === "1")
|
|
450
|
+
exports.f2Stats.goals++;
|
|
451
|
+
if (!(0, pairing_coordinate_js_1.hasPairingTransversal)(goalWp))
|
|
452
|
+
return false;
|
|
453
|
+
if (process.env.CUBE4_F2_STATS === "1")
|
|
454
|
+
exports.f2Stats.transversalPass++;
|
|
455
|
+
const moves2 = moveLog.slice(0, depth).map((m) => center2_js_1.CENTER2_MOVES[m]);
|
|
456
|
+
const s2 = (0, moves_js_1.applyCube4Algorithm)(bridgeState, moves2);
|
|
457
|
+
const uAxis = s2.centers[0] % 3;
|
|
458
|
+
const rAxis = s2.centers[4] % 3;
|
|
459
|
+
const fAxis = s2.centers[8] % 3;
|
|
460
|
+
if (process.env.CUBE4_F2_STATS === "1") {
|
|
461
|
+
if (depth < exports.f2Stats.minTransversalL2)
|
|
462
|
+
exports.f2Stats.minTransversalL2 = depth;
|
|
463
|
+
}
|
|
464
|
+
if (axisDet(uAxis, rAxis, fAxis) !== 1) {
|
|
465
|
+
if (process.env.CUBE4_F2_STATS === "1")
|
|
466
|
+
exports.f2Stats.axisRejects++;
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
if (process.env.CUBE4_F2_STATS === "1") {
|
|
470
|
+
if (depth < exports.f2Stats.minChiralL2)
|
|
471
|
+
exports.f2Stats.minChiralL2 = depth;
|
|
472
|
+
}
|
|
473
|
+
const pairing = (0, pairing_coordinate_js_1.encodePairingState)(s2.wp);
|
|
474
|
+
if (pairing) {
|
|
475
|
+
const s2CornerParity = cornerPermutationParity(s2.cp);
|
|
476
|
+
const eXc = (pairing.parity ^ s2CornerParity);
|
|
477
|
+
const ct3 = (0, center3_js_1.encodeCenter3)(s2, eXc).coordinate;
|
|
478
|
+
const cdist = tables.center3.pruning[ct3];
|
|
479
|
+
if (cdist !== 0xff && cdist !== undefined) {
|
|
480
|
+
const symIdx = (0, edge3_js_1.getMvSymFrom)(pairing.edge, pairing.edge, 20, sym.sym2Raw, sym.sym2Mask, sym.raw2Sym, sym.mvrot, sym.mvroto);
|
|
481
|
+
const edist = tables.edge3.distances.length >= 31006080
|
|
482
|
+
? tables.edge3.distances[symIdx >> 3]
|
|
483
|
+
: (tables.edge3.distances[(symIdx >> 3) >> 1] >>
|
|
484
|
+
(((symIdx >> 3) & 1) * 4)) &
|
|
485
|
+
0x0f;
|
|
486
|
+
const moves2Remapped = prep.totalRot.length === 0
|
|
487
|
+
? moves2
|
|
488
|
+
: rotateAlgorithm(moves2, invertRotationSequence(prep.totalRot));
|
|
489
|
+
const hcCoord = tables.hc ? tables.hc.index[(0, hc_pdb_js_1.encodeHcPdb)(s2.centers)] : -1;
|
|
490
|
+
const hcRaw = hcCoord >= 0 && tables.hc ? tables.hc.distances[hcCoord] : 0;
|
|
491
|
+
const hdist = hcRaw === 0x0f ? 0 : hcRaw;
|
|
492
|
+
let pdist = 0;
|
|
493
|
+
let pdistHigh = 0;
|
|
494
|
+
if (tables.c3PairDist) {
|
|
495
|
+
const c3Base = ct3 * 144;
|
|
496
|
+
for (let s = 0; s < 12; s++) {
|
|
497
|
+
const idx = c3Base + s * 12 + pairing.edge[s];
|
|
498
|
+
const d = (idx & 1) === 0 ? (tables.c3PairDist[idx >> 1] & 0x0f) : (tables.c3PairDist[idx >> 1] >> 4);
|
|
499
|
+
if (d > pdist)
|
|
500
|
+
pdist = d;
|
|
501
|
+
if (d >= 8)
|
|
502
|
+
pdistHigh++;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const cand = {
|
|
506
|
+
moves12: [...prep.prefix, ...moves2Remapped],
|
|
507
|
+
length12: prep.prefix.length + moves2.length,
|
|
508
|
+
ct3,
|
|
509
|
+
cdist,
|
|
510
|
+
edge: pairing.edge,
|
|
511
|
+
edist,
|
|
512
|
+
hdist,
|
|
513
|
+
pdist,
|
|
514
|
+
hcCoord,
|
|
515
|
+
rotCost: 0,
|
|
516
|
+
value: prep.prefix.length + moves2.length + Math.max(cdist, edist, hdist, pdist) + (pdistHigh >= 4 ? 2 : (pdistHigh >= 2 ? 1 : 0)),
|
|
517
|
+
state: s2,
|
|
518
|
+
p1: prep.p1,
|
|
519
|
+
totalRot: prep.totalRot
|
|
520
|
+
};
|
|
521
|
+
if (candidates.length < CAND_CAP) {
|
|
522
|
+
candidates.push(cand);
|
|
523
|
+
if (candidates.length === CAND_CAP) {
|
|
524
|
+
candidates.sort((a, b) => a.value - b.value);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else if (cand.value < candidates[candidates.length - 1].value) {
|
|
528
|
+
candidates[candidates.length - 1] = cand;
|
|
529
|
+
for (let j = candidates.length - 1; j > 0 && candidates[j].value < candidates[j - 1].value; j--) {
|
|
530
|
+
const tmp = candidates[j];
|
|
531
|
+
candidates[j] = candidates[j - 1];
|
|
532
|
+
candidates[j - 1] = tmp;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
p1CandCount++;
|
|
536
|
+
const maxP1Cand = options?.p1CandCap !== undefined ? options.p1CandCap : (deep ? 96 : 64);
|
|
537
|
+
if (p1CandCount >= maxP1Cand)
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
197
541
|
}
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
if (maxl === 0)
|
|
545
|
+
return false;
|
|
546
|
+
const c2ctMove = tables.center2.ctMove;
|
|
547
|
+
const c2rlMove = tables.center2.rlMove;
|
|
548
|
+
const c2RowC = c * center2_js_1.CENTER2_MOVES.length;
|
|
549
|
+
const c2RowR = r * center2_js_1.CENTER2_MOVES.length;
|
|
550
|
+
for (let mi = 0; mi < C2_SEARCH.length; mi++) {
|
|
551
|
+
const m = p2Order[mi];
|
|
552
|
+
if (lm >= 0 && C2_CANCEL[lm][m]) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const nc = c2ctMove[c2RowC + m];
|
|
556
|
+
const nr = c2rlMove[c2RowR + m];
|
|
557
|
+
const prun = c2pruning[nc * center2_js_1.CENTER2_RL_STATES + nr];
|
|
558
|
+
if (prun >= maxl) {
|
|
559
|
+
if (prun > maxl && rng === null && !c2Wide) {
|
|
560
|
+
mi = SKIP_AXIS_2[m] - 1;
|
|
561
|
+
}
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
moveLog[depth] = m;
|
|
565
|
+
if (validWpDepth > depth)
|
|
566
|
+
validWpDepth = depth;
|
|
567
|
+
if (searchPhase2(nc, nr, maxl - 1, m, depth + 1, bridgeState, prep))
|
|
568
|
+
return true;
|
|
198
569
|
}
|
|
199
570
|
return false;
|
|
200
571
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
572
|
+
let p1CandCount = 0;
|
|
573
|
+
let MAX_P2 = 8;
|
|
574
|
+
// Phase-1 application + the phase-2 bridge depend only on the phase-1
|
|
575
|
+
// solution, not on the phase-2 target length: compute them once instead
|
|
576
|
+
// of redoing the work for every length12 iteration.
|
|
577
|
+
// Alignment is treated purely as a coordinate frame rotation (matching
|
|
578
|
+
// csTimer's sym frame remapping), adding 0 physical moves to prefix.
|
|
579
|
+
const preps = [];
|
|
580
|
+
for (const p1 of p1Sols) {
|
|
581
|
+
const s1 = (0, moves_js_1.applyCube4Algorithm)(state, p1.moves);
|
|
582
|
+
const s1Aligned = p1.rotAlign.length === 0 ? s1 : applyCubeRotation(s1, p1.rotAlign);
|
|
583
|
+
const bridge = (0, center2_js_1.center2Bridge)(s1Aligned);
|
|
584
|
+
if (!bridge)
|
|
205
585
|
continue;
|
|
586
|
+
const totalRot = [...p1.rotAlign, ...bridge.rotation];
|
|
587
|
+
const prefix = p1.moves;
|
|
588
|
+
const prun = c2pruning[bridge.encoded.ct * center2_js_1.CENTER2_RL_STATES + bridge.encoded.rl];
|
|
589
|
+
// Paired wings lookahead: check how many wings are already paired in bridge.state
|
|
590
|
+
let pairedWings = 0;
|
|
591
|
+
const wp = bridge.state.wp;
|
|
592
|
+
for (let pair = 0; pair < 12; pair++) {
|
|
593
|
+
if (Math.floor(wp[pair * 2] / 2) === Math.floor(wp[pair * 2 + 1] / 2)) {
|
|
594
|
+
pairedWings++;
|
|
595
|
+
}
|
|
206
596
|
}
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
continue;
|
|
212
|
-
moveLog[depth] = m;
|
|
213
|
-
if (searchPhase2(nc, nr, maxl - 1, m, depth + 1, bridgeState, p1Moves))
|
|
214
|
-
return true;
|
|
597
|
+
// Lookahead composite score: total estimated centers penalized by already-paired wings
|
|
598
|
+
const estCenters = prefix.length + prun;
|
|
599
|
+
const score = estCenters * 2 - Math.min(4, pairedWings);
|
|
600
|
+
preps.push({ p1, prefix, bridgeState: bridge.state, totalRot, ct: bridge.encoded.ct, rl: bridge.encoded.rl, prun, score });
|
|
215
601
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
602
|
+
// Ascending lookahead score (lowest total estimated center moves + wing bonus),
|
|
603
|
+
// tie-broken by lower phase-2 pruning bound then shorter prefix length
|
|
604
|
+
preps.sort((a, b) => (a.score - b.score) || (a.prun - b.prun) || (a.prefix.length - b.prefix.length));
|
|
605
|
+
// 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 ? 160 : 100);
|
|
607
|
+
if (preps.length > PREP_CAP) {
|
|
608
|
+
preps.length = PREP_CAP;
|
|
609
|
+
}
|
|
610
|
+
// Per-restart tie order only (the ascending order above is preserved).
|
|
611
|
+
shuffleTies(preps, (p) => p.score, rng);
|
|
612
|
+
// Passes after the first one only extend MAX_P2 by one: every smaller
|
|
613
|
+
// length2 was already searched (deterministic — rerunning would push the
|
|
614
|
+
// same goals again as duplicates and crowd the 45 cap), so skip them.
|
|
615
|
+
// Tracked exactly per (prep, length2): this also fills holes left when a
|
|
616
|
+
// pass breaks early at the 45 cap, and is reused by the diversity
|
|
617
|
+
// extension below.
|
|
618
|
+
const searchedL2 = new Set();
|
|
619
|
+
let lastBestValue = 999;
|
|
620
|
+
let stagnantLengths = 0;
|
|
621
|
+
do {
|
|
622
|
+
OUTER: for (let length12 = minP1; length12 < minP1 + 16; length12++) {
|
|
623
|
+
for (let pi = 0; pi < preps.length; pi++) {
|
|
624
|
+
const prep = preps[pi];
|
|
625
|
+
if (prep.prefix.length > length12)
|
|
626
|
+
continue;
|
|
627
|
+
const length2 = length12 - prep.prefix.length;
|
|
628
|
+
if (length2 > MAX_P2 || length2 < 0)
|
|
629
|
+
continue;
|
|
630
|
+
if (prep.prun > length2)
|
|
631
|
+
continue;
|
|
632
|
+
const key = pi * 64 + length2;
|
|
633
|
+
if (searchedL2.has(key))
|
|
634
|
+
continue;
|
|
635
|
+
searchedL2.add(key);
|
|
636
|
+
p1CandCount = 0;
|
|
637
|
+
wpStack[0].set(prep.bridgeState.wp);
|
|
638
|
+
validWpDepth = 0;
|
|
639
|
+
searchPhase2(prep.ct, prep.rl, length2, -1, 0, prep.bridgeState, prep);
|
|
640
|
+
if (candidates.length >= CAND_CAP &&
|
|
641
|
+
candidates[0].value <= length12 + 9 &&
|
|
642
|
+
(candidates[0].edist <= 10 || length12 >= minP1 + 3)) {
|
|
643
|
+
break OUTER;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (options?.stagnationCut !== false && candidates.length >= CAND_MIN) {
|
|
647
|
+
exports.stagnationStats.checks++;
|
|
648
|
+
// NOTE: candidates is only sorted once it reaches CAND_CAP, so
|
|
649
|
+
// candidates[0] is NOT the best below the cap — scan the true min.
|
|
650
|
+
let curBest = Infinity;
|
|
651
|
+
for (let ci = 0; ci < candidates.length; ci++) {
|
|
652
|
+
const v = candidates[ci].value;
|
|
653
|
+
if (v < curBest)
|
|
654
|
+
curBest = v;
|
|
655
|
+
}
|
|
656
|
+
if (curBest >= lastBestValue) {
|
|
657
|
+
stagnantLengths++;
|
|
658
|
+
if (stagnantLengths >= 2) {
|
|
659
|
+
exports.stagnationStats.cuts++;
|
|
660
|
+
break OUTER;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
lastBestValue = curBest;
|
|
665
|
+
stagnantLengths = 0;
|
|
666
|
+
}
|
|
237
667
|
}
|
|
238
668
|
}
|
|
669
|
+
MAX_P2++;
|
|
670
|
+
} while ((candidates.length === 0 && MAX_P2 <= P2_MAX_ABS) || (candidates.length < CAND_MIN && MAX_P2 <= P2_MAX_MIN));
|
|
671
|
+
if (candidates.length === 0) {
|
|
672
|
+
throw new Error("4x4 solve: no valid phase 2 candidates found.");
|
|
239
673
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
674
|
+
const tF2End = performance.now();
|
|
675
|
+
const validReductions = [];
|
|
676
|
+
/**
|
|
677
|
+
* Per-candidate IDA* proofs shared across all sweep stages: the greatest
|
|
678
|
+
* phase-3 length exhaustively proved empty for that candidate. Entries
|
|
679
|
+
* are only written on clean proofs (no cap abort, no goal seen, no TT
|
|
680
|
+
* hit, complete excess info), so skipping those lengths is exact.
|
|
681
|
+
*/
|
|
682
|
+
const p3Proven = new Map();
|
|
683
|
+
/**
|
|
684
|
+
* Sync downstream chain for a phase-3 goal sequence: reduction check,
|
|
685
|
+
* orientation remap, 3x3 conversion + phase-1 heuristic. Returns the
|
|
686
|
+
* ValidReduction or null. Shared by the sweep validator and the polish
|
|
687
|
+
* pass (which retries the winner one length shorter).
|
|
688
|
+
*/
|
|
689
|
+
function buildValidReduction(cand, moves3) {
|
|
690
|
+
const reduced = (0, moves_js_1.applyCube4Algorithm)(cand.state, moves3);
|
|
691
|
+
if ((0, phase4_js_1.checkCube4Reduction)(reduced) !== null)
|
|
692
|
+
return null;
|
|
693
|
+
const totalRot = cand.totalRot;
|
|
694
|
+
const invTotalRot = totalRot.length === 0 ? [] : invertRotationSequence(totalRot);
|
|
695
|
+
const moves3Remapped = totalRot.length === 0 ? moves3 : rotateAlgorithm(moves3, invTotalRot);
|
|
696
|
+
const reducedOriginal = totalRot.length === 0 ? reduced : applyCubeRotation(reduced, invTotalRot);
|
|
697
|
+
const key = `${reducedOriginal.centers[0]},${reducedOriginal.centers[4]},${reducedOriginal.centers[8]}`;
|
|
698
|
+
const path = VALID_24_ORIENTATIONS.get(key);
|
|
699
|
+
if (!path)
|
|
700
|
+
return null;
|
|
701
|
+
const rotToStandard = invertRotationSequence(path);
|
|
702
|
+
const standardReduced = applyCubeRotation(reducedOriginal, rotToStandard);
|
|
703
|
+
const cube3State = (0, phase4_js_1.toCube3State)(standardReduced);
|
|
704
|
+
if (!cube3State) {
|
|
705
|
+
console.log(" toCube3State failed!");
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const h3 = tables.cube3Phase1 ? getTriAxialCube3Heuristic(standardReduced, tables.cube3Phase1) : 0;
|
|
709
|
+
const reductionMoves = [
|
|
710
|
+
...rotateAlgorithm(cand.moves12, rotToStandard),
|
|
711
|
+
...rotateAlgorithm(moves3Remapped, rotToStandard)
|
|
712
|
+
];
|
|
713
|
+
const simplifiedReduction = (0, moves_js_1.simplifyCube4Algorithm)(reductionMoves);
|
|
714
|
+
return {
|
|
715
|
+
cand,
|
|
716
|
+
moves3,
|
|
717
|
+
moves3Remapped,
|
|
718
|
+
rotPath: path,
|
|
719
|
+
rotToStandard,
|
|
720
|
+
rotMoves: rotToStandard.flatMap((r) => ROT_MOVES[r]),
|
|
721
|
+
standardReduced,
|
|
722
|
+
h3,
|
|
723
|
+
totalEst: simplifiedReduction.length + h3
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const centersBeam = Math.max(1, options?.centersBeam ?? 3);
|
|
727
|
+
const edgesPerCenter = Math.max(1, options?.edgesPerCenter ?? 3);
|
|
728
|
+
const maxReductionsTarget = options?.finishTries !== undefined
|
|
729
|
+
? Math.max(1, options.finishTries)
|
|
730
|
+
: (centersBeam * edgesPerCenter);
|
|
731
|
+
/**
|
|
732
|
+
* One exact-length sweep over a value-sorted pool: tries increasing
|
|
733
|
+
* totals, collects up to maxReductionsTarget valid reductions using
|
|
734
|
+
* hierarchical beam search (centersBeam distinct centers x edgesPerCenter edge pairings).
|
|
735
|
+
* Shared across all stages below.
|
|
736
|
+
*
|
|
737
|
+
* Bounded by `maxCalls` solveLength calls (not wall-clock): easy states
|
|
738
|
+
* use ~100, so adversarial pools fail fast deterministically on any
|
|
739
|
+
* machine instead of grinding through the whole pool.
|
|
740
|
+
*/
|
|
741
|
+
function sweepPool(pool, cfg) {
|
|
742
|
+
const candReductions = new Map();
|
|
743
|
+
const distinctCenters = new Set();
|
|
744
|
+
let foundAtLength = null;
|
|
745
|
+
OUTER_P3: for (let length123 = pool[0].value; length123 < L123_CAP; length123++) {
|
|
746
|
+
if (foundAtLength !== null && length123 > foundAtLength + 1)
|
|
258
747
|
break;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
748
|
+
// Sweep the whole (sorted) pool: see sortTop rationale.
|
|
749
|
+
for (let i = 0; i < pool.length; i++) {
|
|
750
|
+
const cand = pool[i];
|
|
751
|
+
if (cand.value > length123)
|
|
752
|
+
break;
|
|
753
|
+
const length3 = length123 - cand.length12;
|
|
754
|
+
const minL3 = Math.max(cand.edist, cand.cdist, cand.hdist, cand.pdist ?? 0);
|
|
755
|
+
if (length3 > cfg.maxLength3 || length3 < minL3)
|
|
267
756
|
continue;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (!path)
|
|
757
|
+
// IDA* threshold propagation: a previous exact-length call may have
|
|
758
|
+
// exhaustively proved this candidate empty through a longer length.
|
|
759
|
+
if ((p3Proven.get(cand) ?? -1) >= length3)
|
|
272
760
|
continue;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
761
|
+
const candCount = candReductions.get(cand) ?? 0;
|
|
762
|
+
if (candCount >= edgesPerCenter)
|
|
763
|
+
continue;
|
|
764
|
+
if (distinctCenters.size >= centersBeam && !distinctCenters.has(cand))
|
|
765
|
+
continue;
|
|
766
|
+
if (cand.edist <= length3 && cand.cdist <= length3 && cand.hdist <= length3 && (cand.pdist ?? 0) <= length3) {
|
|
767
|
+
const nodeLimit = length3 <= 12 ? 80000 : Math.max(45000, cfg.maxNodes);
|
|
768
|
+
f3calls++;
|
|
769
|
+
const validsBefore = validReductions.length;
|
|
770
|
+
const needed = edgesPerCenter - candCount;
|
|
771
|
+
const candReductionsBuffer = [];
|
|
772
|
+
const maxToCollect = Math.min(16, needed + 3);
|
|
773
|
+
const result = phase3Solver.solveLengthProven(cand.edge, cand.ct3, cand.edist, length3, tables.center3, tables.edge3.distances, nodeLimit, 16, (moves3) => {
|
|
774
|
+
const red = buildValidReduction(cand, moves3);
|
|
775
|
+
if (!red)
|
|
776
|
+
return false;
|
|
777
|
+
candReductionsBuffer.push(red);
|
|
778
|
+
if (foundAtLength === null)
|
|
779
|
+
foundAtLength = length123;
|
|
780
|
+
return candReductionsBuffer.length >= maxToCollect;
|
|
781
|
+
},
|
|
782
|
+
// hc mid-search prune disabled: on a 44-state corpus the per-node
|
|
783
|
+
// hc lookups cost more than they prune, and the altered
|
|
784
|
+
// capped/success pattern shifts the stop-at-3 valid selection
|
|
785
|
+
// toward shorter reductions (which finish as well or better on
|
|
786
|
+
// average). The exact hdist call gate above, the candidate hdist
|
|
787
|
+
// ranking, and the polish-pass hc bounds below all stay.
|
|
788
|
+
undefined, -1, tables.c3PairDist);
|
|
789
|
+
if (candReductionsBuffer.length > 0) {
|
|
790
|
+
// Sort solutions for this center candidate by totalEst (cancellation-aware length + tri-axial 3x3 heuristic)
|
|
791
|
+
candReductionsBuffer.sort((a, b) => a.totalEst - b.totalEst);
|
|
792
|
+
const toAdd = candReductionsBuffer.slice(0, needed);
|
|
793
|
+
for (const red of toAdd) {
|
|
794
|
+
validReductions.push(red);
|
|
795
|
+
}
|
|
796
|
+
candReductions.set(cand, candCount + toAdd.length);
|
|
797
|
+
distinctCenters.add(cand);
|
|
798
|
+
}
|
|
799
|
+
const proven = result.provenThrough;
|
|
800
|
+
if (process.env.CUBE4_SWEEP_STATS === "1") {
|
|
801
|
+
// eslint-disable-next-line no-console
|
|
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
|
+
}
|
|
806
|
+
if (result.moves === null && proven >= length3) {
|
|
807
|
+
const prev = p3Proven.get(cand) ?? -1;
|
|
808
|
+
if (proven > prev)
|
|
809
|
+
p3Proven.set(cand, proven);
|
|
810
|
+
}
|
|
811
|
+
// The finish loop attempts reductions up to maxReductionsTarget
|
|
812
|
+
if (validReductions.length >= maxReductionsTarget) {
|
|
813
|
+
break OUTER_P3;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
279
816
|
}
|
|
280
817
|
}
|
|
281
|
-
|
|
818
|
+
}
|
|
819
|
+
const sortTop = (arr) => {
|
|
820
|
+
// No truncation: value misranks solvability (short bounds, hard joints),
|
|
821
|
+
// and truncating demonstrably discards winners (corpus-18/49). Cost lands
|
|
822
|
+
// only on hard states (early-stop rules); easy states stop early anyway.
|
|
823
|
+
const sorted = [...arr].sort((a, b) => a.value - b.value);
|
|
824
|
+
// Per-restart tie order only (ascending value order is preserved, so the
|
|
825
|
+
// sweep's `value > length123` break stays valid).
|
|
826
|
+
shuffleTies(sorted, (c) => c.value, rng);
|
|
827
|
+
return sorted;
|
|
828
|
+
};
|
|
829
|
+
// Sweeps with growing budgets; longitudes extend dynamically (reference
|
|
830
|
+
// style) while nothing is found, instead of fixed caps. Easy states only
|
|
831
|
+
// pay the first sweep.
|
|
832
|
+
// Sweeps with growing budgets; longitudes extend dynamically (reference
|
|
833
|
+
// style) while nothing is found, instead of fixed caps. Easy states only
|
|
834
|
+
// pay the first sweep.
|
|
835
|
+
// NOTE: lowering these caps was tried (20k/16k) and reverted: capped calls
|
|
836
|
+
// change which valids are found (±2 moves on the corpus). Only admissible
|
|
837
|
+
// heuristic strengthenings are length-safe here.
|
|
838
|
+
sweepPool(sortTop(candidates), { maxNodes: 30000, maxLength3: 14 });
|
|
839
|
+
if (validReductions.length === 0) {
|
|
840
|
+
sweepPool(sortTop(candidates), { maxNodes: 30000, maxLength3: 16 });
|
|
841
|
+
}
|
|
842
|
+
if (validReductions.length === 0) {
|
|
843
|
+
sweepPool(sortTop(candidates), { maxNodes: 30000, maxLength3: 18 });
|
|
844
|
+
}
|
|
845
|
+
if (validReductions.length === 0) {
|
|
846
|
+
sweepPool(sortTop(candidates), { maxNodes: 30000, maxLength3: 20 });
|
|
847
|
+
}
|
|
848
|
+
if (validReductions.length === 0) {
|
|
849
|
+
sweepPool(sortTop(candidates), { maxNodes: 30000, maxLength3: 24 });
|
|
850
|
+
}
|
|
851
|
+
if (validReductions.length === 0 && deep) {
|
|
852
|
+
sweepPool(sortTop(candidates), { maxNodes: 60000, maxLength3: 27 });
|
|
853
|
+
}
|
|
854
|
+
// Diversity extension (only when both sweeps found nothing): the pool's
|
|
855
|
+
// joints are jointly hard, so gather DEEPER phase-2 solutions (longer
|
|
856
|
+
// length2 = different wings) and sweep those. Only hard states pay this.
|
|
857
|
+
if (validReductions.length === 0 && !options?.maxReductionLen) {
|
|
858
|
+
const poolStart = candidates.length;
|
|
859
|
+
let MX = MAX_P2;
|
|
860
|
+
EXTEND: while (candidates.length - poolStart < 40 && MX <= EXT_MX) {
|
|
861
|
+
for (let length12 = minP1; length12 < minP1 + 18; length12++) {
|
|
862
|
+
for (let pi = 0; pi < preps.length; pi++) {
|
|
863
|
+
const prep = preps[pi];
|
|
864
|
+
if (prep.prefix.length > length12)
|
|
865
|
+
break;
|
|
866
|
+
const length2 = length12 - prep.prefix.length;
|
|
867
|
+
if (length2 > MX || length2 < 0)
|
|
868
|
+
continue;
|
|
869
|
+
if (prep.prun > length2)
|
|
870
|
+
continue;
|
|
871
|
+
const key = pi * 64 + length2;
|
|
872
|
+
if (searchedL2.has(key))
|
|
873
|
+
continue;
|
|
874
|
+
searchedL2.add(key);
|
|
875
|
+
p1CandCount = 0;
|
|
876
|
+
wpStack[0].set(prep.bridgeState.wp);
|
|
877
|
+
validWpDepth = 0;
|
|
878
|
+
searchPhase2(prep.ct, prep.rl, length2, -1, 0, prep.bridgeState, prep);
|
|
879
|
+
if (candidates.length - poolStart >= 40)
|
|
880
|
+
break EXTEND;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
MX++;
|
|
884
|
+
}
|
|
885
|
+
const fresh = sortTop(candidates.slice(poolStart));
|
|
886
|
+
if (fresh.length > 0) {
|
|
887
|
+
sweepPool(fresh, { maxNodes: 15000, maxLength3: 18 });
|
|
888
|
+
}
|
|
889
|
+
if (validReductions.length === 0 && fresh.length > 0) {
|
|
890
|
+
sweepPool(fresh, { maxNodes: 30000, maxLength3: 20 });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (validReductions.length === 0) {
|
|
894
|
+
throw new Error("4x4 solve: reduction failed across all candidates.");
|
|
895
|
+
}
|
|
896
|
+
// Sort reductions by Total Estimated Moves (Reduction length + 3x3 Phase 1 lower bound)
|
|
897
|
+
validReductions.sort((a, b) => a.totalEst - b.totalEst);
|
|
898
|
+
const tFinishStart = performance.now();
|
|
899
|
+
let bestSolution = null;
|
|
900
|
+
let minTotalMoves = 999;
|
|
901
|
+
let bestPhases;
|
|
902
|
+
let bestRed = null;
|
|
903
|
+
// Best-of among the first two (as before); any further reductions are
|
|
904
|
+
// only attempted when those both failed, so a flaky 3x3 finish cannot
|
|
905
|
+
// sink the whole attempt while the common case costs nothing extra.
|
|
906
|
+
// Best-of among the first three (phase-4 is cheap now); further
|
|
907
|
+
// reductions are only attempted when those all failed.
|
|
908
|
+
const finishTries = options?.finishTries ?? maxReductionsTarget;
|
|
909
|
+
for (let i = 0; i < validReductions.length; i++) {
|
|
910
|
+
if (i >= finishTries && bestSolution)
|
|
282
911
|
break;
|
|
912
|
+
const red = validReductions[i];
|
|
913
|
+
const cube3State = (0, phase4_js_1.toCube3State)(red.standardReduced);
|
|
914
|
+
if (!cube3State)
|
|
915
|
+
continue;
|
|
916
|
+
let cube3Solution;
|
|
917
|
+
try {
|
|
918
|
+
cube3Solution = await solveCube3(cube3State);
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
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
|
+
if (simplified.length < minTotalMoves) {
|
|
932
|
+
minTotalMoves = simplified.length;
|
|
933
|
+
bestSolution = simplified;
|
|
934
|
+
bestPhases = {
|
|
935
|
+
length12: red.cand.length12,
|
|
936
|
+
length1: red.cand.p1.moves.length,
|
|
937
|
+
length2: red.cand.length12 - red.cand.p1.moves.length,
|
|
938
|
+
length3: red.moves3.length,
|
|
939
|
+
finish: cube3Solution.moves.length
|
|
940
|
+
};
|
|
941
|
+
bestRed = red;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (!bestSolution || !bestRed) {
|
|
945
|
+
throw new Error("4x4 solve: reduction failed across all candidates.");
|
|
946
|
+
}
|
|
947
|
+
// Polish pass: the sweep takes the first valid sequence per exact
|
|
948
|
+
// length, which is often one above optimal. Retry the winner one length
|
|
949
|
+
// shorter with a large budget (up to 2 iterations). Gated on totals
|
|
950
|
+
// above target so easy states never pay it.
|
|
951
|
+
const tPolishStart = performance.now();
|
|
952
|
+
// Polish the winner plus the runner-up by totalEst (was: winner only).
|
|
953
|
+
// Runner-up shortenings get their own finish and can only lower the min
|
|
954
|
+
// over finished totals; easy states skip the inner loop via the length
|
|
955
|
+
// gate below, so the extra pass costs ~nothing there.
|
|
956
|
+
const redToPolish = options?.deepPolish
|
|
957
|
+
? validReductions.slice(0, 3)
|
|
958
|
+
: [bestRed, ...validReductions.filter((r) => r !== bestRed)].slice(0, 2);
|
|
959
|
+
for (const r of redToPolish) {
|
|
960
|
+
let curRed = r;
|
|
961
|
+
// The runner-up gets a cheap probe budget: its shortening is a bonus,
|
|
962
|
+
// and a full 150k grind on tail states cost seconds for nothing. It is
|
|
963
|
+
// also skipped entirely unless the total is high enough to matter.
|
|
964
|
+
const polishBudget = r === bestRed ? 150000 : 30000;
|
|
965
|
+
for (let iter = 0; iter < 4 && (minTotalMoves > 43 || curRed.moves3.length > 11); iter++) {
|
|
966
|
+
const cand = curRed.cand;
|
|
967
|
+
const shorterLen = curRed.moves3.length - 1;
|
|
968
|
+
if (shorterLen < cand.edist || shorterLen < cand.cdist || shorterLen < cand.hdist || shorterLen < (cand.pdist ?? 0))
|
|
969
|
+
break;
|
|
970
|
+
f3calls++;
|
|
971
|
+
const shorter = phase3Solver.solveLengthValid(cand.edge, cand.ct3, cand.edist, shorterLen, tables.center3, tables.edge3.distances, polishBudget, 64, (moves3) => buildValidReduction(cand, moves3) !== null, tables.hc, cand.hcCoord, tables.c3PairDist);
|
|
972
|
+
if (!shorter)
|
|
973
|
+
break;
|
|
974
|
+
const red = buildValidReduction(cand, shorter);
|
|
975
|
+
if (!red)
|
|
976
|
+
break;
|
|
977
|
+
const cube3State = (0, phase4_js_1.toCube3State)(red.standardReduced);
|
|
978
|
+
if (!cube3State)
|
|
979
|
+
break;
|
|
980
|
+
let cube3Solution;
|
|
981
|
+
try {
|
|
982
|
+
cube3Solution = await solveCube3(cube3State);
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
break;
|
|
986
|
+
}
|
|
987
|
+
const simplified = (0, moves_js_1.simplifyCube4Algorithm)([
|
|
988
|
+
...rotateAlgorithm(red.cand.moves12, red.rotToStandard),
|
|
989
|
+
...rotateAlgorithm(red.moves3Remapped, red.rotToStandard),
|
|
990
|
+
...cube3Solution.moves
|
|
991
|
+
]);
|
|
992
|
+
if (simplified.length < minTotalMoves) {
|
|
993
|
+
minTotalMoves = simplified.length;
|
|
994
|
+
bestSolution = simplified;
|
|
995
|
+
curRed = red;
|
|
996
|
+
bestPhases = {
|
|
997
|
+
length12: red.cand.length12,
|
|
998
|
+
length1: red.cand.p1.moves.length,
|
|
999
|
+
length2: red.cand.length12 - red.cand.p1.moves.length,
|
|
1000
|
+
length3: red.moves3.length,
|
|
1001
|
+
finish: cube3Solution.moves.length
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
accP2 += p2nodes;
|
|
1010
|
+
accF3calls += f3calls;
|
|
1011
|
+
accF1Ms += tF1End - tR0;
|
|
1012
|
+
accF2Ms += tF2End - tF1End;
|
|
1013
|
+
accF3Ms += tFinishStart - tF2End;
|
|
1014
|
+
accFinishMs += tPolishStart - tFinishStart;
|
|
1015
|
+
accPolishMs += performance.now() - tPolishStart;
|
|
1016
|
+
return { moves: bestSolution, phases: bestPhases };
|
|
1017
|
+
};
|
|
1018
|
+
let bestMoves = null;
|
|
1019
|
+
let bestPhasesOverall;
|
|
1020
|
+
let lastRestartError = null;
|
|
1021
|
+
for (let restart = 0; restart < totalRestarts; restart++) {
|
|
1022
|
+
phase3_search_js_1.FastPhase3Solver.nextEpoch();
|
|
1023
|
+
const rng = totalRestarts > 1
|
|
1024
|
+
? mulberry32((hashCube4State(state) ^ Math.imul(restart + 1, 0x9e3779b9)) >>> 0)
|
|
1025
|
+
: null;
|
|
1026
|
+
try {
|
|
1027
|
+
const one = await runOnce(rng);
|
|
1028
|
+
if (!bestMoves || one.moves.length < bestMoves.length) {
|
|
1029
|
+
bestMoves = one.moves;
|
|
1030
|
+
bestPhasesOverall = one.phases;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
catch (error) {
|
|
1034
|
+
lastRestartError = error;
|
|
283
1035
|
}
|
|
284
|
-
MAX_LENGTH3++;
|
|
285
|
-
} while (!solvedCand && MAX_LENGTH3 <= 18);
|
|
286
|
-
if (!solvedCand || !solvedMoves3 || !standardReduced) {
|
|
287
|
-
throw new Error("4x4 solve: reduction failed across all candidates.");
|
|
288
|
-
}
|
|
289
|
-
const cube3State = (0, phase4_js_1.toCube3State)(standardReduced);
|
|
290
|
-
if (!cube3State) {
|
|
291
|
-
throw new Error("4x4 solve: failed to convert reduced 4x4 state to 3x3 state.");
|
|
292
1036
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
1037
|
+
if (!bestMoves) {
|
|
1038
|
+
throw lastRestartError instanceof Error
|
|
1039
|
+
? lastRestartError
|
|
1040
|
+
: new Error(`4x4 solve: all ${totalRestarts} restarts failed: ${String(lastRestartError)}`);
|
|
296
1041
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
1042
|
+
const scramble = (0, moves_js_1.invertCube4Algorithm)(bestMoves);
|
|
1043
|
+
const result = {
|
|
1044
|
+
moves: bestMoves,
|
|
1045
|
+
scramble,
|
|
1046
|
+
nodes: { p2: accP2, p3: phase3Solver.epochNodes, f3calls: accF3calls },
|
|
1047
|
+
timings: {
|
|
1048
|
+
reductionMs: accF1Ms + accF2Ms + accF3Ms,
|
|
1049
|
+
f1Ms: accF1Ms,
|
|
1050
|
+
f2Ms: accF2Ms,
|
|
1051
|
+
f3Ms: accF3Ms,
|
|
1052
|
+
finishMs: accFinishMs,
|
|
1053
|
+
polishMs: accPolishMs
|
|
300
1054
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
...solvedMoves3,
|
|
306
|
-
...rotateAlgorithm(cube3Solution.moves, rotPath)
|
|
307
|
-
];
|
|
308
|
-
const fullSolToStandard = [
|
|
309
|
-
...fullSolInOriginal,
|
|
310
|
-
...rotToStandard.flatMap((r) => ROT_MOVES[r])
|
|
311
|
-
];
|
|
312
|
-
const scramble = (0, moves_js_1.invertCube4Algorithm)(fullSolToStandard);
|
|
313
|
-
return { moves: fullSolToStandard, scramble };
|
|
1055
|
+
};
|
|
1056
|
+
if (bestPhasesOverall)
|
|
1057
|
+
result.phases = bestPhasesOverall;
|
|
1058
|
+
return result;
|
|
314
1059
|
}
|