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