@cubingcenter/scrambler 0.6.3 → 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 +17 -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/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.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/browser-loader.js +48 -22
- 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/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 +37 -0
- package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase1.js +156 -15
- package/dist/puzzles/cube3/solver/phase2.d.ts +75 -0
- package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase2.js +279 -68
- 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/solver/tables.js +14 -12
- package/dist/puzzles/cube3/validation.d.ts.map +1 -1
- package/dist/puzzles/cube3/validation.js +15 -7
- 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 +10 -2
- package/dist/puzzles/cube4/index.d.ts.map +1 -1
- package/dist/puzzles/cube4/index.js +6 -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 +212 -11
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts +29 -1
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/browser-loader.js +227 -32
- package/dist/puzzles/cube4/reduction/center2.d.ts +44 -3
- package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/center2.js +152 -10
- 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/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/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 +44 -0
- package/dist/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +52 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.js +220 -0
- package/dist/puzzles/cube4/reduction/node-loader.d.ts +15 -1
- package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/node-loader.js +199 -25
- 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/pairing-coordinate.js +5 -5
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts +116 -7
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/phase3-search.js +458 -32
- 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 +172 -2
- package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/solve.js +1201 -180
- 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 +201 -6
- package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
- package/dist/puzzles/cube4/scrambler.js +414 -176
- package/dist/puzzles/cube4/state.d.ts.map +1 -1
- package/dist/puzzles/cube4/state.js +4 -1
- 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/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/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/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/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/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 +48 -22
- package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
- 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 +157 -15
- package/dist-cjs/puzzles/cube3/solver/phase2.js +276 -65
- package/dist-cjs/puzzles/cube3/solver/symmetry.js +80 -0
- package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
- package/dist-cjs/puzzles/cube3/validation.js +15 -7
- package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
- package/dist-cjs/puzzles/cube4/index.js +23 -1
- package/dist-cjs/puzzles/cube4/moves.js +213 -11
- package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +227 -32
- package/dist-cjs/puzzles/cube4/reduction/center2.js +155 -10
- package/dist-cjs/puzzles/cube4/reduction/edge3.js +24 -3
- package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
- 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 +44 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
- package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +232 -0
- package/dist-cjs/puzzles/cube4/reduction/node-loader.js +200 -25
- package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
- package/dist-cjs/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
- package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +460 -33
- package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
- package/dist-cjs/puzzles/cube4/reduction/solve.js +1203 -172
- package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
- package/dist-cjs/puzzles/cube4/scrambler.js +416 -174
- package/dist-cjs/puzzles/cube4/state.js +4 -1
- 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/pyraminx/solver/search.js +2 -5
- package/dist-cjs/puzzles/pyraminx/solver/tables.js +18 -1
- 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/skewb/solver/search.js +2 -2
- package/dist-cjs/puzzles/skewb/solver/tables.js +18 -1
- package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
- package/docs/development.md +19 -7
- package/package.json +17 -7
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* The 4x4 scramble generator: sample a uniform random state, solve it with
|
|
4
|
-
* the reduction solver (phases 1-4), and invert the solution. The
|
|
5
|
-
*
|
|
4
|
+
* the reduction solver (phases 1-4), and invert the solution. The emitted
|
|
5
|
+
* scramble must reproduce the sampled target state EXACTLY — enforced by
|
|
6
|
+
* `assertCube4ScrambleReproducesTarget` on every generated result (unless
|
|
7
|
+
* `skipValidation` is set, which is only for the independent verification
|
|
8
|
+
* harness in `scripts/verify-scrambles.ts`).
|
|
6
9
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* On a target mismatch the error is retryable, so the existing retry loop
|
|
11
|
+
* re-runs the solve over the SAME sampled target (deep attempt) and throws
|
|
12
|
+
* once attempts are exhausted. Re-sampling a different target to "make it
|
|
13
|
+
* fit" and degrading to random moves are forbidden: failing loudly (WCA 4g)
|
|
14
|
+
* is strictly preferable to emitting a scramble that lies about its seed.
|
|
15
|
+
*
|
|
16
|
+
* ── Determinism contract: what defines the output ──────────────────────
|
|
17
|
+
* `generateCube4Scramble` / `generateCube4Attempt` are pure functions of
|
|
18
|
+
* (seed, complete options): the emitted `moves` do not depend on wall-clock
|
|
19
|
+
* time, CPU load, warm-up, batch order, or process identity. This holds
|
|
20
|
+
* because every budget that can change the search outcome is a
|
|
21
|
+
* deterministic work counter (nodes visited), never a wall deadline:
|
|
22
|
+
* - reduction phases: node budgets inside `reduction/solve.ts`;
|
|
23
|
+
* - the 3x3 finish: node caps below (`finish.phase2MaxNodes`,
|
|
24
|
+
* `finish.improveMaxNodes`, per-candidate cap), which replace the old
|
|
25
|
+
* 150/50 ms wall budgets that made the same seed yield different move
|
|
26
|
+
* counts under CPU contention.
|
|
27
|
+
* Options are PART of the key: `restarts`/`bestOf`, `threeAxes`, `deep`,
|
|
28
|
+
* the beam knobs and `finish.*` legitimately change the result for the same
|
|
29
|
+
* seed — the output is reproducible within each configuration (documented
|
|
30
|
+
* behavior, not nondeterminism).
|
|
31
|
+
*
|
|
32
|
+
* Residual wall-clock backstop (hang guard only; never observed to bind —
|
|
33
|
+
* max measured solve 36.5 s): the 90 s
|
|
34
|
+
* `Promise.race` below. It cannot preempt the synchronous search — it
|
|
35
|
+
* can only fire at an event-loop tick — so if one ever fired, that attempt
|
|
36
|
+
* fails loudly (as a retryable error) instead of hanging. Making that bound
|
|
37
|
+
* exact deadline support would require cooperative search checkpoints.
|
|
10
38
|
*/
|
|
11
39
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
40
|
if (k2 === undefined) k2 = k;
|
|
@@ -42,107 +70,59 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
42
70
|
};
|
|
43
71
|
})();
|
|
44
72
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.cube4ScrambleGenerator = void 0;
|
|
73
|
+
exports.Cube4WallTimeoutError = exports.Cube4TargetMismatchError = exports.cube4ScrambleGenerator = void 0;
|
|
74
|
+
exports.assertCube4ScrambleReproducesTarget = assertCube4ScrambleReproducesTarget;
|
|
46
75
|
exports.generateCube4Scramble = generateCube4Scramble;
|
|
76
|
+
exports.generateCube4Attempt = generateCube4Attempt;
|
|
47
77
|
const moves_js_1 = require("./moves.js");
|
|
48
78
|
const random_state_js_1 = require("./random-state.js");
|
|
49
79
|
const state_js_1 = require("./state.js");
|
|
50
80
|
const facelets_js_1 = require("./facelets.js");
|
|
51
81
|
const solve_js_1 = require("./reduction/solve.js");
|
|
52
82
|
const index_js_1 = require("../cube3/solver/index.js");
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
83
|
+
const tables_js_1 = require("../cube3/solver/tables.js");
|
|
84
|
+
const finish3_js_1 = require("./reduction/finish3.js");
|
|
85
|
+
const rotation_fix_js_1 = require("./reduction/rotation-fix.js");
|
|
56
86
|
// ────────────────────────────────────────────────────────────
|
|
57
87
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
88
|
+
* Deterministic work caps for the 3x3 finish — nodes visited, never
|
|
89
|
+
* wall-clock. Calibrated against the former wall budgets (phase-2 50 ms,
|
|
90
|
+
* improvement 150 ms at a measured ~1.75k nodes/ms warm) so typical
|
|
91
|
+
* latency and finish length are unchanged, while the stopping point
|
|
92
|
+
* becomes independent of CPU load: same (seed, options) ⇒ same moves.
|
|
93
|
+
* A failing first finish call retries once with every cap doubled
|
|
94
|
+
* (equivalent of the former 150→300 / 50→100 ms retry).
|
|
61
95
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// ────────────────────────────────────────────────────────────
|
|
91
|
-
const CUBE4_FACES = ["U", "R", "F", "D", "L", "B"];
|
|
92
|
-
// Opposite face indices (U↔D, R↔L, F↔B)
|
|
93
|
-
const CUBE4_OPPOSITE = { U: "D", D: "U", R: "L", L: "R", F: "B", B: "F" };
|
|
94
|
-
const CUBE4_SUFFIXES = ["", "2", "'"];
|
|
95
|
-
const CUBE4_WIDE_FACES = ["Uw", "Rw", "Fw", "Dw", "Lw", "Bw"];
|
|
96
|
-
// Opposite wide-move faces
|
|
97
|
-
const CUBE4_WIDE_OPPOSITE = {
|
|
98
|
-
Uw: "Dw", Dw: "Uw", Rw: "Lw", Lw: "Rw", Fw: "Bw", Bw: "Fw"
|
|
99
|
-
};
|
|
100
|
-
// All available move bases (outer + wide). Outer and wide turns are treated
|
|
101
|
-
// independently for the same-face/opposite-face check using their base letter.
|
|
102
|
-
const ALL_MOVE_BASES = [...CUBE4_FACES, ...CUBE4_WIDE_FACES];
|
|
103
|
-
function generateCube4RandomMoves(options = {}) {
|
|
104
|
-
const rng = (0, index_js_2.createRandomSource)(options.seed);
|
|
105
|
-
const moveCount = 40;
|
|
106
|
-
const moves = [];
|
|
107
|
-
let lastFace = null;
|
|
108
|
-
let secondLastFace = null;
|
|
109
|
-
const getBaseLetter = (face) => face.length > 1 ? face[0] : face;
|
|
110
|
-
for (let i = 0; i < moveCount; i++) {
|
|
111
|
-
let face;
|
|
112
|
-
// Reject same face as last or opposite face when the last two moves
|
|
113
|
-
// share the same axis (to avoid redundant move pairs like R L R).
|
|
114
|
-
do {
|
|
115
|
-
face = ALL_MOVE_BASES[rng.nextInt(ALL_MOVE_BASES.length)];
|
|
116
|
-
} while (face === lastFace ||
|
|
117
|
-
(lastFace !== null &&
|
|
118
|
-
(CUBE4_OPPOSITE[face] === lastFace || CUBE4_WIDE_OPPOSITE[face] === lastFace) &&
|
|
119
|
-
secondLastFace !== null &&
|
|
120
|
-
getBaseLetter(secondLastFace) === getBaseLetter(face)));
|
|
121
|
-
const suffix = CUBE4_SUFFIXES[rng.nextInt(3)];
|
|
122
|
-
moves.push(`${face}${suffix}`);
|
|
123
|
-
secondLastFace = lastFace;
|
|
124
|
-
lastFace = face;
|
|
125
|
-
}
|
|
126
|
-
const scramble = moves.join(" ");
|
|
127
|
-
return {
|
|
128
|
-
eventId: "444",
|
|
129
|
-
scramble,
|
|
130
|
-
moves,
|
|
131
|
-
quality: "random-moves",
|
|
132
|
-
generator: "cube4-random-moves-fallback",
|
|
133
|
-
metadata: {
|
|
134
|
-
generator: "cube4-random-moves-fallback",
|
|
135
|
-
distribution: "random-moves",
|
|
136
|
-
note: "Reduction solver and cubing.js both failed; using random-moves.",
|
|
137
|
-
...options.metadata
|
|
138
|
-
}
|
|
139
|
-
};
|
|
96
|
+
const FINISH_PHASE2_MAX_NODES = 90_000;
|
|
97
|
+
const FINISH_IMPROVE_MAX_NODES = 260_000;
|
|
98
|
+
const FINISH_RETRY_SCALE = 2;
|
|
99
|
+
// Continuous finish defaults (the production finish). Measured on cc:0..15:
|
|
100
|
+
// 6 views × 200k nodes = 44.88 moves @ 1257 ms vs legacy 46.06 @ 1288 ms
|
|
101
|
+
// (50k: 45.50 @ 1102; 100k: 45.31; 300k: 44.88 @ 1371 — no gain past 200k).
|
|
102
|
+
const FINISH3_MAX_NODES = 200_000;
|
|
103
|
+
const FINISH3_HARD_MAX_NODES = 20_000_000;
|
|
104
|
+
// Screening budget for the two-stage finish (every candidate reduction gets
|
|
105
|
+
// this; only the best `finishScreenTop` get FINISH3_MAX_NODES).
|
|
106
|
+
const FINISH3_SCREEN_NODES = 30_000;
|
|
107
|
+
const FINISH3_VIEWS = 6;
|
|
108
|
+
const FINISH3_MAX_PHASE2_LENGTH = 12;
|
|
109
|
+
let cube4TablesPromise;
|
|
110
|
+
/**
|
|
111
|
+
* Loads the 4x4 reduction tables with a promise-deduplicating cache: the
|
|
112
|
+
* cache stores the in-flight PROMISE (not a value), so concurrent first
|
|
113
|
+
* calls share one load instead of racing; a rejected load is purged so the
|
|
114
|
+
* next call can retry instead of caching the failure forever.
|
|
115
|
+
*/
|
|
116
|
+
function loadCube4Tables(baseUrl) {
|
|
117
|
+
if (cube4TablesPromise !== undefined)
|
|
118
|
+
return cube4TablesPromise;
|
|
119
|
+
cube4TablesPromise = loadCube4TablesOnce(baseUrl).catch((error) => {
|
|
120
|
+
cube4TablesPromise = undefined;
|
|
121
|
+
throw error;
|
|
122
|
+
});
|
|
123
|
+
return cube4TablesPromise;
|
|
140
124
|
}
|
|
141
|
-
|
|
142
|
-
let cachedTables;
|
|
143
|
-
async function loadCube4Tables(baseUrl) {
|
|
144
|
-
if (cachedTables)
|
|
145
|
-
return cachedTables;
|
|
125
|
+
async function loadCube4TablesOnce(baseUrl) {
|
|
146
126
|
const isBrowser = typeof window !== "undefined" ||
|
|
147
127
|
typeof document !== "undefined" ||
|
|
148
128
|
(typeof self !== "undefined" && typeof self.importScripts === "function") ||
|
|
@@ -155,20 +135,15 @@ async function loadCube4Tables(baseUrl) {
|
|
|
155
135
|
"./reduction/browser-loader.js")));
|
|
156
136
|
const cached = loader.getCachedCube4Tables();
|
|
157
137
|
if (cached) {
|
|
158
|
-
cachedTables = cached;
|
|
159
138
|
return cached;
|
|
160
139
|
}
|
|
161
|
-
|
|
162
|
-
cachedTables = tables;
|
|
163
|
-
return tables;
|
|
140
|
+
return await loader.loadCube4ReductionTablesBrowser(baseUrl);
|
|
164
141
|
}
|
|
165
142
|
const loader = await Promise.resolve().then(() => __importStar(require(
|
|
166
143
|
/* webpackIgnore: true */
|
|
167
144
|
/* @vite-ignore */
|
|
168
145
|
"./reduction/node-loader.js")));
|
|
169
|
-
|
|
170
|
-
cachedTables = tables;
|
|
171
|
-
return tables;
|
|
146
|
+
return await loader.loadCube4ReductionTables();
|
|
172
147
|
}
|
|
173
148
|
exports.cube4ScrambleGenerator = {
|
|
174
149
|
info: {
|
|
@@ -177,98 +152,365 @@ exports.cube4ScrambleGenerator = {
|
|
|
177
152
|
puzzle: "cube-4x4",
|
|
178
153
|
quality: "random-state",
|
|
179
154
|
generator: "cube4-reduction",
|
|
180
|
-
description: "Random-state 4x4 scramble via the reduction solver (centers + pairing + 3x3 finish).
|
|
155
|
+
description: "Random-state 4x4 scramble via the reduction solver (centers + pairing + 3x3 finish)."
|
|
181
156
|
},
|
|
182
157
|
generate(options = {}) {
|
|
183
158
|
return generateCube4Scramble(options);
|
|
184
159
|
}
|
|
185
160
|
};
|
|
161
|
+
/**
|
|
162
|
+
* Thrown by the target gate when the emitted scramble does not reproduce
|
|
163
|
+
* the independently sampled state. Retryable: the retry loop re-solves the
|
|
164
|
+
* SAME sampled target (deep attempt) — never re-samples.
|
|
165
|
+
*/
|
|
166
|
+
class Cube4TargetMismatchError extends Error {
|
|
167
|
+
code = "cube4-target-mismatch";
|
|
168
|
+
constructor(message) {
|
|
169
|
+
super(message);
|
|
170
|
+
this.name = "Cube4TargetMismatchError";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exports.Cube4TargetMismatchError = Cube4TargetMismatchError;
|
|
174
|
+
/** Thrown by the 90 s hang guard (wall-clock, fires only at a tick). */
|
|
175
|
+
class Cube4WallTimeoutError extends Error {
|
|
176
|
+
code = "cube4-wall-timeout";
|
|
177
|
+
constructor(message) {
|
|
178
|
+
super(message);
|
|
179
|
+
this.name = "Cube4WallTimeoutError";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.Cube4WallTimeoutError = Cube4WallTimeoutError;
|
|
183
|
+
/**
|
|
184
|
+
* Message patterns for errors thrown by `reduction/solve.ts`, which cannot
|
|
185
|
+
* be classified by type/code without editing that file (reported: it should
|
|
186
|
+
* grow error codes). Every other retryable path is typed below.
|
|
187
|
+
*/
|
|
188
|
+
const SOLVER_RETRYABLE_MESSAGE_PATTERNS = [
|
|
189
|
+
"reduction failed", // no candidates / node budget exhausted in a reduction phase
|
|
190
|
+
"no valid phase 2" // empty phase-2 candidate set
|
|
191
|
+
];
|
|
192
|
+
/**
|
|
193
|
+
* Retry classification. Typed first (error class / `code`), message
|
|
194
|
+
* patterns last and only for the reduction solver's own errors. Semantics:
|
|
195
|
+
* a retry re-runs the solve over the SAME sampled target with deeper work
|
|
196
|
+
* caps; it never changes the seed's target state.
|
|
197
|
+
*/
|
|
198
|
+
function isFinish3Cap(error) {
|
|
199
|
+
return error instanceof Error &&
|
|
200
|
+
error.code === "cube4-finish3-cap" &&
|
|
201
|
+
(error instanceof finish3_js_1.Finish3SearchError || error.name === "Finish3SearchError");
|
|
202
|
+
}
|
|
203
|
+
function isRetryableCube4Error(error) {
|
|
204
|
+
if (!(error instanceof Error))
|
|
205
|
+
return false;
|
|
206
|
+
const code = error.code;
|
|
207
|
+
if (code === "cube4-target-mismatch" || code === "cube4-wall-timeout")
|
|
208
|
+
return true;
|
|
209
|
+
if (isFinish3Cap(error))
|
|
210
|
+
return true;
|
|
211
|
+
if (error instanceof solve_js_1.Cube4Phase2CapError ||
|
|
212
|
+
error instanceof solve_js_1.Cube4Phase3CapError ||
|
|
213
|
+
error instanceof solve_js_1.Cube4FinishCapError)
|
|
214
|
+
return true;
|
|
215
|
+
// Budget/depth failures of the 3x3 finish phase 2: a deeper attempt may
|
|
216
|
+
// succeed (historical behavior: any Cube3Phase2* error was retryable).
|
|
217
|
+
if (error instanceof index_js_1.Cube3Phase2SearchError || error.name === "Cube3Phase2SearchError") {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
// Phase-1 failures are deterministic depth failures (phase 1 runs
|
|
221
|
+
// uncapped in the 444 finish): retrying the same target cannot help.
|
|
222
|
+
if (error instanceof index_js_1.Cube3Phase1SearchError || error.name === "Cube3Phase1SearchError") {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return SOLVER_RETRYABLE_MESSAGE_PATTERNS.some((pattern) => error.message.includes(pattern));
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The real random-state contract for 444 (non-tautological): replaying the
|
|
229
|
+
* emitted scramble on the solved cube must reproduce the exact state that
|
|
230
|
+
* was sampled for the seed — the reference the sampler produced, never a
|
|
231
|
+
* state derived from the scramble itself. Throws when it does not.
|
|
232
|
+
*
|
|
233
|
+
* Used by the production validation path in `solveCube4Attempt` and
|
|
234
|
+
* exported so tests can prove the gate rejects corrupted scrambles.
|
|
235
|
+
*/
|
|
236
|
+
function assertCube4ScrambleReproducesTarget(scrambleMoves, targetState, seed) {
|
|
237
|
+
const replayed = (0, moves_js_1.applyCube4Algorithm)((0, state_js_1.createSolvedCube4State)(), scrambleMoves);
|
|
238
|
+
if (!(0, state_js_1.cube4StatesEqual)(replayed, targetState)) {
|
|
239
|
+
throw new Cube4TargetMismatchError("[cube4-scrambler] scramble does not reproduce the sampled target state" +
|
|
240
|
+
(seed !== undefined ? ` (seed=${seed})` : "") +
|
|
241
|
+
": applying the emitted scramble to the solved cube must yield the state " +
|
|
242
|
+
"sampled for this seed. Refusing to emit it (no re-sampling, no random-move fallback).");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
186
245
|
async function generateCube4Scramble(options = {}) {
|
|
187
246
|
let tables;
|
|
247
|
+
// Optional pair tables are unnecessary for the 444 finish. In browsers,
|
|
248
|
+
// probing for absent tables on every generation adds a network dependency
|
|
249
|
+
// before the search even starts (outside its timeout).
|
|
250
|
+
const pairTables = undefined;
|
|
188
251
|
try {
|
|
189
|
-
|
|
252
|
+
const continuousFinish = options.finish?.mode !== "legacy";
|
|
253
|
+
const [loadedTables, cube3P1Tables, cube3P2Tables] = await Promise.all([
|
|
254
|
+
loadCube4Tables(),
|
|
255
|
+
(0, tables_js_1.loadCube3Phase1Tables)(),
|
|
256
|
+
continuousFinish ? (0, tables_js_1.loadCube3Phase2Tables)() : Promise.resolve(undefined)
|
|
257
|
+
]);
|
|
258
|
+
tables = loadedTables;
|
|
259
|
+
tables.cube3Phase1 = cube3P1Tables;
|
|
260
|
+
if (cube3P2Tables !== undefined)
|
|
261
|
+
tables.cube3Phase2 = cube3P2Tables;
|
|
190
262
|
}
|
|
191
263
|
catch (error) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (cubingJsResult)
|
|
195
|
-
return cubingJsResult;
|
|
196
|
-
return generateCube4RandomMoves(options);
|
|
264
|
+
throw new Error("[cube4-scrambler] Failed to load reduction tables; run pnpm cube4:center1:tables etc. " +
|
|
265
|
+
`(${error instanceof Error ? error.message : String(error)})`);
|
|
197
266
|
}
|
|
267
|
+
const totalRestarts = Math.max(1, Math.floor(options.restarts ?? options.bestOf ?? 1) || 1);
|
|
268
|
+
const targetState = (0, random_state_js_1.generateRandomCube4State)(options);
|
|
198
269
|
let lastError;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
270
|
+
try {
|
|
271
|
+
return await solveCube4Attempt(tables, pairTables, {
|
|
272
|
+
...options,
|
|
273
|
+
restarts: totalRestarts
|
|
274
|
+
}, targetState);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
if (!isRetryableCube4Error(error))
|
|
278
|
+
throw error;
|
|
279
|
+
lastError = error;
|
|
280
|
+
}
|
|
281
|
+
if (!options.deep) {
|
|
282
|
+
try {
|
|
283
|
+
return await solveCube4Attempt(tables, pairTables, {
|
|
284
|
+
...options,
|
|
285
|
+
restarts: totalRestarts,
|
|
286
|
+
deep: true
|
|
287
|
+
}, targetState);
|
|
206
288
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
else {
|
|
213
|
-
delete attemptOptions.seed;
|
|
214
|
-
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (isRetryableCube4Error(error))
|
|
291
|
+
lastError = error;
|
|
292
|
+
else
|
|
293
|
+
throw error;
|
|
215
294
|
}
|
|
216
|
-
|
|
295
|
+
}
|
|
296
|
+
throw lastError instanceof Error
|
|
297
|
+
? lastError
|
|
298
|
+
: new Error(`[cube4-scrambler] Reduction solver failed for restarts=${totalRestarts}: ${String(lastError)}`);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Single-attempt 444 solve: samples ONE state (from `options.seed`) and
|
|
302
|
+
* solves it with the reduction solver. Throws on failure — including a
|
|
303
|
+
* target mismatch ("scramble does not reproduce the sampled target state")
|
|
304
|
+
* — no resampling; hosts may retry with the same seed, which re-uses the
|
|
305
|
+
* same target.
|
|
306
|
+
* Fan-out primitive for hosts: run with seeds `seed`, `seed:bestN`, ...
|
|
307
|
+
* across workers and keep the shortest valid result.
|
|
308
|
+
*/
|
|
309
|
+
async function generateCube4Attempt(tables, pairTables, options = {}) {
|
|
310
|
+
return solveCube4Attempt(tables, pairTables, options, (0, random_state_js_1.generateRandomCube4State)(options));
|
|
311
|
+
}
|
|
312
|
+
async function solveCube4Attempt(tables, pairTables, options, attemptState) {
|
|
313
|
+
const axes = options.threeAxes ? [[], ["z"], ["y'"]] : [[]];
|
|
314
|
+
let bestScrambleMoves = null;
|
|
315
|
+
let bestSolutionMoves = null;
|
|
316
|
+
let bestPhases;
|
|
317
|
+
let bestNodes;
|
|
318
|
+
let bestTimings;
|
|
319
|
+
let bestReductionLen;
|
|
320
|
+
let lastError;
|
|
321
|
+
const tAttemptStart = performance.now();
|
|
322
|
+
const finishOpts = options.finish ?? {};
|
|
323
|
+
for (const rot of axes) {
|
|
217
324
|
try {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
325
|
+
const sRot = rot.length === 0 ? attemptState : (0, solve_js_1.applyCubeRotation)(attemptState, rot);
|
|
326
|
+
// The reduction search is bounded by deterministic work caps (see
|
|
327
|
+
// solve.ts), so these timeouts are only wall-clock backstops for
|
|
328
|
+
// pathological cases.
|
|
329
|
+
const solvePromise = (0, solve_js_1.solveCube4State)(sRot, tables, async (cube3State, effort) => {
|
|
330
|
+
// Legacy batch two-phase finish. Budgets are deterministic node
|
|
331
|
+
// caps (see FINISH_* constants); `capScale` doubles every cap on
|
|
332
|
+
// the one flaky-finish retry.
|
|
333
|
+
const solveLegacyFinish = async (capScale) => {
|
|
334
|
+
const solution = await (0, index_js_1.solveCube3State)(cube3State, {
|
|
335
|
+
orientations: finishOpts.orientations ?? 3,
|
|
336
|
+
phase1ExtraDepth: finishOpts.phase1ExtraDepth ?? 1,
|
|
337
|
+
phase1SolutionLimit: finishOpts.phase1SolutionLimit ?? 24,
|
|
338
|
+
// 18 (not 20): the improve loop only runs when the first
|
|
339
|
+
// answer exceeds the target, so 20 skipped it for 19-20 move
|
|
340
|
+
// answers. Measured: -0.13 moves at ~+3% time.
|
|
341
|
+
targetLength: finishOpts.targetLength ?? 18,
|
|
342
|
+
improveMaxNodes: Math.round((finishOpts.improveMaxNodes ?? FINISH_IMPROVE_MAX_NODES) * capScale),
|
|
343
|
+
...(finishOpts.improvePhase2MaxNodes !== undefined
|
|
344
|
+
? {
|
|
345
|
+
improvePhase2MaxNodes: Math.round(finishOpts.improvePhase2MaxNodes * capScale)
|
|
346
|
+
}
|
|
347
|
+
: {}),
|
|
348
|
+
// The 54MB pair tables are skipped for the 444 finish: pair
|
|
349
|
+
// pruning changes nothing measurable here (identical lengths
|
|
350
|
+
// on a 44-state corpus, zero extra finish retries), and
|
|
351
|
+
// loading them dominates cold-start I/O. Hosts that already
|
|
352
|
+
// hold pairTables can still pass them via generateCube4Attempt.
|
|
353
|
+
phase2: {
|
|
354
|
+
pairTables,
|
|
355
|
+
skipPairTables: pairTables === undefined,
|
|
356
|
+
maxNodes: Math.round((finishOpts.phase2MaxNodes ?? FINISH_PHASE2_MAX_NODES) * capScale)
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return { moves: solution.moves };
|
|
360
|
+
};
|
|
361
|
+
if (finishOpts.mode !== "legacy") {
|
|
362
|
+
// Callers of generateCube4Attempt pass their own table bag,
|
|
363
|
+
// which may predate the continuous finish: load what is missing.
|
|
364
|
+
tables.cube3Phase1 ??= await (0, tables_js_1.loadCube3Phase1Tables)();
|
|
365
|
+
tables.cube3Phase2 ??= await (0, tables_js_1.loadCube3Phase2Tables)();
|
|
366
|
+
const views = finishOpts.continuousViews ?? FINISH3_VIEWS;
|
|
367
|
+
const runContinuous = (capScale) => {
|
|
368
|
+
if (tables.cube3Phase1 === undefined || tables.cube3Phase2 === undefined) {
|
|
369
|
+
throw new Error("[cube4-scrambler] continuous finish requires cube3 phase-1 and phase-2 tables");
|
|
370
|
+
}
|
|
371
|
+
return (0, finish3_js_1.solveFinish3)(cube3State, { phase1: tables.cube3Phase1, phase2: tables.cube3Phase2 }, {
|
|
372
|
+
targetLength: finishOpts.targetLength ?? 18,
|
|
373
|
+
maxNodes: Math.round((effort === "screen"
|
|
374
|
+
? finishOpts.continuousScreenNodes ?? FINISH3_SCREEN_NODES
|
|
375
|
+
: finishOpts.continuousMaxNodes ?? FINISH3_MAX_NODES) * capScale),
|
|
376
|
+
hardMaxNodes: Math.round((finishOpts.continuousHardMaxNodes ?? FINISH3_HARD_MAX_NODES) * capScale),
|
|
377
|
+
maxPhase2Length: finishOpts.continuousMaxPhase2Length ?? FINISH3_MAX_PHASE2_LENGTH,
|
|
378
|
+
views
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
try {
|
|
382
|
+
return runContinuous(1);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
if (!isFinish3Cap(error))
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
return runContinuous(FINISH_RETRY_SCALE);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
if (!isFinish3Cap(error))
|
|
393
|
+
throw error;
|
|
394
|
+
return solveLegacyFinish(1);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// The first legacy call seeds everything, so a flaky finish gets
|
|
398
|
+
// one retry with doubled caps before the whole finish is declared
|
|
399
|
+
// failed (formerly 150→300 / 50→100 wall budgets).
|
|
400
|
+
try {
|
|
401
|
+
return await solveLegacyFinish(1);
|
|
225
402
|
}
|
|
403
|
+
catch {
|
|
404
|
+
return solveLegacyFinish(FINISH_RETRY_SCALE);
|
|
405
|
+
}
|
|
406
|
+
}, {
|
|
407
|
+
deep: options.deep === true,
|
|
408
|
+
deepPolish: options.deepPolish ?? options.threeAxes,
|
|
409
|
+
...(options.finishTries !== undefined ? { finishTries: options.finishTries } : {}),
|
|
410
|
+
...(options.centersBeam !== undefined ? { centersBeam: options.centersBeam } : {}),
|
|
411
|
+
...(options.edgesPerCenter !== undefined ? { edgesPerCenter: options.edgesPerCenter } : {}),
|
|
412
|
+
...(options.phase1ExtraDepths !== undefined ? { phase1ExtraDepths: options.phase1ExtraDepths } : {}),
|
|
413
|
+
...(options.phase1Cap !== undefined ? { phase1Cap: options.phase1Cap } : {}),
|
|
414
|
+
...(options.center2Wide !== undefined ? { center2Wide: options.center2Wide } : {}),
|
|
415
|
+
...(options.restarts !== undefined ? { restarts: options.restarts } : {}),
|
|
416
|
+
finishScreenTop: finishOpts.mode === "legacy" ? 0 : finishOpts.screenTop ?? 0,
|
|
417
|
+
...(options.maxFixMoves !== undefined ? { maxFixMoves: options.maxFixMoves } : {}),
|
|
418
|
+
...(options.prepCap !== undefined ? { prepCap: options.prepCap } : {}),
|
|
419
|
+
...(options.p1CandCap !== undefined ? { p1CandCap: options.p1CandCap } : {}),
|
|
420
|
+
...(options.stagnationCut !== undefined ? { stagnationCut: options.stagnationCut } : {}),
|
|
421
|
+
maxReductionLen: bestReductionLen
|
|
422
|
+
});
|
|
423
|
+
let timeoutHandle;
|
|
424
|
+
// Honest scope: this race cannot interrupt the synchronous CPU-bound
|
|
425
|
+
// search — a setTimeout callback only runs at an event-loop tick, so
|
|
426
|
+
// the guard can fire only if the async solve chain yields for 90 s
|
|
427
|
+
// total. It exists solely so a pathological hang fails loudly
|
|
428
|
+
// (retryable Cube4WallTimeoutError) instead of wedging the host; it
|
|
429
|
+
// has never bound (max measured solve 36.5 s).
|
|
430
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
431
|
+
timeoutHandle = setTimeout(() => reject(new Cube4WallTimeoutError("4x4 solve: timeout")), 90000);
|
|
432
|
+
});
|
|
433
|
+
let solveOutcome;
|
|
434
|
+
try {
|
|
435
|
+
solveOutcome = await Promise.race([solvePromise, timeoutPromise]);
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
// The search is work-capped and always settles first: clearing the
|
|
439
|
+
// backstop avoids leaking a live 90s timer per axis (which kept the
|
|
440
|
+
// event loop — and CLI/test processes — alive needlessly).
|
|
441
|
+
clearTimeout(timeoutHandle);
|
|
226
442
|
}
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
443
|
+
const { scramble: scrambleMoves, moves: solutionMoves, phases, timings, nodes } = solveOutcome;
|
|
444
|
+
const fullSolution = rot.length === 0
|
|
445
|
+
? solutionMoves
|
|
446
|
+
: (0, rotation_fix_js_1.placeRotationFix)([], rot, solutionMoves);
|
|
447
|
+
const fullScramble = rot.length === 0
|
|
448
|
+
? scrambleMoves
|
|
449
|
+
: (0, moves_js_1.invertCube4Algorithm)(fullSolution);
|
|
450
|
+
if (!bestScrambleMoves || fullScramble.length < bestScrambleMoves.length) {
|
|
451
|
+
bestScrambleMoves = fullScramble;
|
|
452
|
+
bestSolutionMoves = fullSolution;
|
|
453
|
+
bestPhases = phases;
|
|
454
|
+
bestTimings = timings;
|
|
455
|
+
bestNodes = nodes;
|
|
456
|
+
if (phases) {
|
|
457
|
+
bestReductionLen = phases.length12 + phases.length3;
|
|
241
458
|
}
|
|
242
|
-
};
|
|
243
|
-
if (options.seed !== undefined) {
|
|
244
|
-
result.seed = options.seed;
|
|
245
459
|
}
|
|
246
|
-
return result;
|
|
247
460
|
}
|
|
248
|
-
catch (
|
|
249
|
-
lastError =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
461
|
+
catch (err) {
|
|
462
|
+
lastError = err;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (!bestScrambleMoves || !bestSolutionMoves) {
|
|
466
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
467
|
+
}
|
|
468
|
+
const scrambleMoves = bestScrambleMoves;
|
|
469
|
+
const scrambledState = (0, moves_js_1.applyCube4Algorithm)((0, state_js_1.createSolvedCube4State)(), scrambleMoves);
|
|
470
|
+
if (!options.skipValidation) {
|
|
471
|
+
// Non-tautological gate: compare against `attemptState` — the state
|
|
472
|
+
// sampled for this seed (the same reference the sampler produced),
|
|
473
|
+
// NOT a state derived from the scramble. The former check
|
|
474
|
+
// (`apply(scramble, invert(scramble)) === solved`) passed by
|
|
475
|
+
// construction and hid frame bugs. A mismatch throws an error the
|
|
476
|
+
// retry loop classifies as retryable: `generateCube4Scramble` then
|
|
477
|
+
// re-runs the solve over this SAME `attemptState` (deep) and finally
|
|
478
|
+
// throws — it never re-samples another target.
|
|
479
|
+
assertCube4ScrambleReproducesTarget(scrambleMoves, attemptState, options.seed);
|
|
480
|
+
}
|
|
481
|
+
const facelets = (0, facelets_js_1.toCube4Facelets)(scrambledState);
|
|
482
|
+
const result = {
|
|
483
|
+
eventId: "444",
|
|
484
|
+
scramble: (0, moves_js_1.formatCube4Algorithm)(scrambleMoves),
|
|
485
|
+
moves: scrambleMoves,
|
|
486
|
+
quality: "random-state",
|
|
487
|
+
generator: "cube4-reduction",
|
|
488
|
+
metadata: {
|
|
489
|
+
generator: "cube4-reduction",
|
|
490
|
+
// The sampled target — the independent reference the seed stands for
|
|
491
|
+
// (consistent with the 222/333/pymx/skwb/fto scramblers). When
|
|
492
|
+
// validation ran, this is exactly `apply(solved, moves)`; with
|
|
493
|
+
// `skipValidation` it stays the sampled truth so external checks can
|
|
494
|
+
// compare the emitted scramble against it instead of a derived state.
|
|
495
|
+
state: attemptState,
|
|
496
|
+
facelets,
|
|
497
|
+
faceletString: (0, facelets_js_1.formatCube4Facelets)(facelets),
|
|
498
|
+
distribution: "random-state",
|
|
499
|
+
// The scramble's inverse: by construction it solves the state the
|
|
500
|
+
// scramble actually produces, and (validation on) that state is
|
|
501
|
+
// `attemptState`.
|
|
502
|
+
solutionMoves: (0, moves_js_1.invertCube4Algorithm)(scrambleMoves),
|
|
503
|
+
// Winning-reduction breakdown (unsimplified lengths) + wall-clock
|
|
504
|
+
// split, for tuning and A/B comparisons.
|
|
505
|
+
phases: bestPhases,
|
|
506
|
+
phaseTimings: bestTimings,
|
|
507
|
+
nodes: bestNodes,
|
|
508
|
+
attemptMs: performance.now() - tAttemptStart,
|
|
509
|
+
...options.metadata
|
|
265
510
|
}
|
|
511
|
+
};
|
|
512
|
+
if (options.seed !== undefined) {
|
|
513
|
+
result.seed = options.seed;
|
|
266
514
|
}
|
|
267
|
-
|
|
268
|
-
console.warn("[cube4-scrambler] Reduction solver failed after 5 attempts; trying cubing.js fallback.", lastError);
|
|
269
|
-
const cubingJsResult = await generateCube4WithCubingJs();
|
|
270
|
-
if (cubingJsResult)
|
|
271
|
-
return cubingJsResult;
|
|
272
|
-
console.warn("[cube4-scrambler] cubing.js fallback also failed; using random-moves.");
|
|
273
|
-
return generateCube4RandomMoves(options);
|
|
515
|
+
return result;
|
|
274
516
|
}
|