@cubingcenter/scrambler 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/dist/core/api.d.ts.map +1 -1
- package/dist/core/api.js +10 -2
- package/dist/puzzles/cube2/solver/browser-loader.d.ts +4 -7
- package/dist/puzzles/cube2/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/browser-loader.js +42 -14
- package/dist/puzzles/cube2/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/cube2/solver/generated-assets.js +8 -3
- package/dist/puzzles/cube3/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/browser-loader.js +42 -20
- package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
- package/dist/puzzles/cube3/solver/phase1.d.ts +19 -0
- package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase1.js +29 -6
- package/dist/puzzles/cube3/solver/phase2.d.ts +28 -0
- package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/phase2.js +56 -15
- package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
- package/dist/puzzles/cube3/solver/tables.js +14 -12
- package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
- package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
- package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
- package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
- package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
- package/dist/puzzles/cube4/browser-worker.js +31 -0
- package/dist/puzzles/cube4/index.d.ts +7 -0
- package/dist/puzzles/cube4/index.d.ts.map +1 -1
- package/dist/puzzles/cube4/index.js +4 -0
- package/dist/puzzles/cube4/moves.d.ts.map +1 -1
- package/dist/puzzles/cube4/moves.js +122 -30
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts +26 -1
- package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/browser-loader.js +212 -48
- package/dist/puzzles/cube4/reduction/center2.d.ts +24 -0
- package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/center2.js +99 -1
- package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
- package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/finish3.js +393 -0
- package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +5 -3
- package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/hc-pdb.js +30 -12
- package/dist/puzzles/cube4/reduction/node-loader.d.ts +10 -1
- package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/node-loader.js +170 -36
- package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
- package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts +33 -30
- package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/phase3-search.js +211 -104
- package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
- package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
- package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
- package/dist/puzzles/cube4/reduction/solve.d.ts +72 -13
- package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
- package/dist/puzzles/cube4/reduction/solve.js +374 -92
- package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
- package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
- package/dist/puzzles/cube4/scramble-cache.js +150 -0
- package/dist/puzzles/cube4/scrambler.d.ts +112 -18
- package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
- package/dist/puzzles/cube4/scrambler.js +271 -83
- package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
- package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
- package/dist/puzzles/cube4/worker-pool.js +405 -0
- package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
- package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
- package/dist/puzzles/cube4/worker-protocol.js +58 -0
- package/dist/puzzles/cube4/worker.d.ts +26 -0
- package/dist/puzzles/cube4/worker.d.ts.map +1 -0
- package/dist/puzzles/cube4/worker.js +49 -0
- package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
- package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
- package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
- package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
- package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
- package/dist/puzzles/shared/compressed-fetch.js +226 -31
- package/dist/puzzles/shared/load-once.d.ts +16 -0
- package/dist/puzzles/shared/load-once.d.ts.map +1 -0
- package/dist/puzzles/shared/load-once.js +11 -0
- package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
- package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
- package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
- package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
- package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
- package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
- package/dist-cjs/core/api.js +10 -2
- package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
- package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
- package/dist-cjs/puzzles/cube3/solver/browser-loader.js +42 -20
- package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
- package/dist-cjs/puzzles/cube3/solver/phase1.js +29 -6
- package/dist-cjs/puzzles/cube3/solver/phase2.js +56 -15
- package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
- package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
- package/dist-cjs/puzzles/cube4/index.js +21 -1
- package/dist-cjs/puzzles/cube4/moves.js +122 -30
- package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +212 -48
- package/dist-cjs/puzzles/cube4/reduction/center2.js +102 -2
- package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
- package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
- package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +31 -12
- package/dist-cjs/puzzles/cube4/reduction/node-loader.js +171 -36
- package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
- package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +211 -104
- package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
- package/dist-cjs/puzzles/cube4/reduction/solve.js +374 -88
- package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
- package/dist-cjs/puzzles/cube4/scrambler.js +271 -80
- package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
- package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
- package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
- package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
- package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
- package/dist-cjs/puzzles/shared/load-once.js +14 -0
- package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
- package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
- package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
- package/docs/development.md +19 -7
- package/package.json +4 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Background prefetch buffer for 4x4 scrambles — the Node-side analogue of
|
|
4
|
+
* TNoodle's `ScrambleCacher`.
|
|
5
|
+
*
|
|
6
|
+
* The cache keeps up to `size` scrambles generating AHEAD of consumption:
|
|
7
|
+
* - `take()` shifts the OLDEST slot; if it is already settled it resolves
|
|
8
|
+
* immediately (no waiting for later slots), otherwise it awaits only
|
|
9
|
+
* that slot. Order is FIFO of START (not of completion), so a seeded
|
|
10
|
+
* stream hands out `seed:0`, `seed:1`, … in order even when the
|
|
11
|
+
* underlying generator finishes out of order (parallel pool).
|
|
12
|
+
* - every shift triggers a refill, so the buffer repopulates in the
|
|
13
|
+
* background — wire `generate: (o) => pool.generate(o)` and the refill
|
|
14
|
+
* itself runs across worker threads (see the pool integration test).
|
|
15
|
+
* - failures are FIRST-CLASS slots: a slot that rejects is delivered as a
|
|
16
|
+
* rejection to the next `take()` (consumed exactly once) and the cache
|
|
17
|
+
* keeps going with the following slots. Every slot promise is wrapped
|
|
18
|
+
* at creation so a background failure can NEVER surface as an
|
|
19
|
+
* unhandledRejection, and `take()` never hangs: slots always settle
|
|
20
|
+
* (the generator contract is "settles eventually").
|
|
21
|
+
* - `dispose()` stops refilling and drops buffered slots. In-flight
|
|
22
|
+
* generations are left to settle silently (results discarded). There
|
|
23
|
+
* are NO timers anywhere, so a disposed (or even forgotten) cache never
|
|
24
|
+
* keeps a process alive.
|
|
25
|
+
*
|
|
26
|
+
* Seeding: when `options.seed` is defined, slot n is generated with
|
|
27
|
+
* `${seed}:${n}` — byte-identical to core `generateScrambles("444", …)`
|
|
28
|
+
* for any count > 1 (slot 0 ↔ item 0 of a batch). Without a seed every
|
|
29
|
+
* slot uses the base options unchanged (fresh randomness).
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.Cube4ScrambleCache = void 0;
|
|
33
|
+
exports.deriveCube4StreamOptions = deriveCube4StreamOptions;
|
|
34
|
+
exports.createCube4ScrambleCache = createCube4ScrambleCache;
|
|
35
|
+
const scrambler_js_1 = require("./scrambler.js");
|
|
36
|
+
/**
|
|
37
|
+
* Stream slot derivation (cache analogue of core's batch derivation).
|
|
38
|
+
* Exported for tests and for hosts that precompute the same sequence.
|
|
39
|
+
*/
|
|
40
|
+
function deriveCube4StreamOptions(options, index) {
|
|
41
|
+
if (options.seed === undefined)
|
|
42
|
+
return options;
|
|
43
|
+
return { ...options, seed: `${options.seed}:${index}` };
|
|
44
|
+
}
|
|
45
|
+
class Cube4ScrambleCache {
|
|
46
|
+
capacity;
|
|
47
|
+
baseOptions;
|
|
48
|
+
generator;
|
|
49
|
+
/** FIFO of slots (start order); settled or still pending. */
|
|
50
|
+
slots = [];
|
|
51
|
+
nextIndex = 0;
|
|
52
|
+
disposed = false;
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
const size = options.size ?? 4;
|
|
55
|
+
if (!Number.isInteger(size) || size < 1) {
|
|
56
|
+
throw new RangeError(`[cube4] cache size must be an integer >= 1 (got ${size}).`);
|
|
57
|
+
}
|
|
58
|
+
this.capacity = size;
|
|
59
|
+
this.baseOptions = options.options ?? {};
|
|
60
|
+
this.generator = options.generate ?? scrambler_js_1.generateCube4Scramble;
|
|
61
|
+
// Prefetch starts immediately: that is the point of constructing a cache.
|
|
62
|
+
this.pump();
|
|
63
|
+
}
|
|
64
|
+
/** Configured buffer capacity. */
|
|
65
|
+
get size() {
|
|
66
|
+
return this.capacity;
|
|
67
|
+
}
|
|
68
|
+
/** Slots currently buffered (settled + still generating). */
|
|
69
|
+
get buffered() {
|
|
70
|
+
return this.slots.length;
|
|
71
|
+
}
|
|
72
|
+
/** Buffered slots that have already settled (ready for an instant take()). */
|
|
73
|
+
get ready() {
|
|
74
|
+
let count = 0;
|
|
75
|
+
for (const carrier of this.slots)
|
|
76
|
+
if (carrier.slot !== undefined)
|
|
77
|
+
count++;
|
|
78
|
+
return count;
|
|
79
|
+
}
|
|
80
|
+
/** Slots currently generating in the background. */
|
|
81
|
+
get inFlight() {
|
|
82
|
+
let count = 0;
|
|
83
|
+
for (const carrier of this.slots)
|
|
84
|
+
if (carrier.slot === undefined)
|
|
85
|
+
count++;
|
|
86
|
+
return count;
|
|
87
|
+
}
|
|
88
|
+
get isDisposed() {
|
|
89
|
+
return this.disposed;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolves once the buffer holds `size` settled results (i.e. a
|
|
93
|
+
* subsequent `take()` is served instantly). Safe to call at any time;
|
|
94
|
+
* rejects only if called after `dispose()`.
|
|
95
|
+
*/
|
|
96
|
+
warmup() {
|
|
97
|
+
if (this.disposed) {
|
|
98
|
+
return Promise.reject(new Error("[cube4] scramble cache is disposed (warmup)."));
|
|
99
|
+
}
|
|
100
|
+
this.pump();
|
|
101
|
+
return Promise.all(this.slots.map((carrier) => carrier.promise)).then(() => undefined);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Next scramble in FIFO order. Instant when the front slot is settled;
|
|
105
|
+
* otherwise waits only for the front slot (never for later ones).
|
|
106
|
+
* A failed background generation surfaces here as a rejection — exactly
|
|
107
|
+
* once — and the cache continues with subsequent slots.
|
|
108
|
+
*/
|
|
109
|
+
take() {
|
|
110
|
+
if (this.disposed) {
|
|
111
|
+
return Promise.reject(new Error("[cube4] scramble cache is disposed (take)."));
|
|
112
|
+
}
|
|
113
|
+
const carrier = this.slots.shift();
|
|
114
|
+
this.pump(); // refill BEFORE awaiting, so generation overlaps consumption
|
|
115
|
+
if (carrier === undefined) {
|
|
116
|
+
// Only reachable when capacity was somehow drained concurrently;
|
|
117
|
+
// generate one on the spot rather than hang.
|
|
118
|
+
return this.generateNow();
|
|
119
|
+
}
|
|
120
|
+
return carrier.promise.then((slot) => {
|
|
121
|
+
if (slot.ok)
|
|
122
|
+
return slot.result;
|
|
123
|
+
throw slot.error;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Stops prefetching, drops buffered slots and ignores in-flight
|
|
128
|
+
* completions. No timers are involved, so nothing can keep a process
|
|
129
|
+
* alive afterwards. Idempotent.
|
|
130
|
+
*/
|
|
131
|
+
dispose() {
|
|
132
|
+
this.disposed = true;
|
|
133
|
+
this.slots.length = 0;
|
|
134
|
+
}
|
|
135
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
136
|
+
pump() {
|
|
137
|
+
while (!this.disposed && this.slots.length < this.capacity) {
|
|
138
|
+
const index = this.nextIndex++;
|
|
139
|
+
const promise = this.generator(deriveCube4StreamOptions(this.baseOptions, index)).then((result) => ({ ok: true, result }), (error) => ({ ok: false, error }));
|
|
140
|
+
const carrier = { promise, slot: undefined };
|
|
141
|
+
void promise.then((slot) => {
|
|
142
|
+
carrier.slot = slot;
|
|
143
|
+
});
|
|
144
|
+
this.slots.push(carrier);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
generateNow() {
|
|
148
|
+
const index = this.nextIndex++;
|
|
149
|
+
return this.generator(deriveCube4StreamOptions(this.baseOptions, index));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.Cube4ScrambleCache = Cube4ScrambleCache;
|
|
153
|
+
/** Convenience factory mirroring the other cube4 constructors. */
|
|
154
|
+
function createCube4ScrambleCache(options = {}) {
|
|
155
|
+
return new Cube4ScrambleCache(options);
|
|
156
|
+
}
|
|
@@ -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,7 +70,8 @@ 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;
|
|
47
76
|
exports.generateCube4Attempt = generateCube4Attempt;
|
|
48
77
|
const moves_js_1 = require("./moves.js");
|
|
@@ -52,12 +81,48 @@ const facelets_js_1 = require("./facelets.js");
|
|
|
52
81
|
const solve_js_1 = require("./reduction/solve.js");
|
|
53
82
|
const index_js_1 = require("../cube3/solver/index.js");
|
|
54
83
|
const tables_js_1 = require("../cube3/solver/tables.js");
|
|
55
|
-
const
|
|
84
|
+
const finish3_js_1 = require("./reduction/finish3.js");
|
|
85
|
+
const rotation_fix_js_1 = require("./reduction/rotation-fix.js");
|
|
56
86
|
// ────────────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
/**
|
|
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).
|
|
95
|
+
*/
|
|
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;
|
|
124
|
+
}
|
|
125
|
+
async function loadCube4TablesOnce(baseUrl) {
|
|
61
126
|
const isBrowser = typeof window !== "undefined" ||
|
|
62
127
|
typeof document !== "undefined" ||
|
|
63
128
|
(typeof self !== "undefined" && typeof self.importScripts === "function") ||
|
|
@@ -70,20 +135,15 @@ async function loadCube4Tables(baseUrl) {
|
|
|
70
135
|
"./reduction/browser-loader.js")));
|
|
71
136
|
const cached = loader.getCachedCube4Tables();
|
|
72
137
|
if (cached) {
|
|
73
|
-
cachedTables = cached;
|
|
74
138
|
return cached;
|
|
75
139
|
}
|
|
76
|
-
|
|
77
|
-
cachedTables = tables;
|
|
78
|
-
return tables;
|
|
140
|
+
return await loader.loadCube4ReductionTablesBrowser(baseUrl);
|
|
79
141
|
}
|
|
80
142
|
const loader = await Promise.resolve().then(() => __importStar(require(
|
|
81
143
|
/* webpackIgnore: true */
|
|
82
144
|
/* @vite-ignore */
|
|
83
145
|
"./reduction/node-loader.js")));
|
|
84
|
-
|
|
85
|
-
cachedTables = tables;
|
|
86
|
-
return tables;
|
|
146
|
+
return await loader.loadCube4ReductionTables();
|
|
87
147
|
}
|
|
88
148
|
exports.cube4ScrambleGenerator = {
|
|
89
149
|
info: {
|
|
@@ -98,65 +158,120 @@ exports.cube4ScrambleGenerator = {
|
|
|
98
158
|
return generateCube4Scramble(options);
|
|
99
159
|
}
|
|
100
160
|
};
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
}
|
|
103
172
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
}
|
|
109
181
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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;
|
|
116
219
|
}
|
|
117
|
-
|
|
118
|
-
|
|
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;
|
|
119
224
|
}
|
|
120
|
-
return
|
|
225
|
+
return SOLVER_RETRYABLE_MESSAGE_PATTERNS.some((pattern) => error.message.includes(pattern));
|
|
121
226
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
}
|
|
133
244
|
}
|
|
134
245
|
async function generateCube4Scramble(options = {}) {
|
|
135
|
-
const bestOf = Math.max(1, Math.floor(options.bestOf ?? 1) || 1);
|
|
136
246
|
let tables;
|
|
137
|
-
|
|
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;
|
|
138
251
|
try {
|
|
139
|
-
const
|
|
252
|
+
const continuousFinish = options.finish?.mode !== "legacy";
|
|
253
|
+
const [loadedTables, cube3P1Tables, cube3P2Tables] = await Promise.all([
|
|
140
254
|
loadCube4Tables(),
|
|
141
255
|
(0, tables_js_1.loadCube3Phase1Tables)(),
|
|
142
|
-
(0,
|
|
256
|
+
continuousFinish ? (0, tables_js_1.loadCube3Phase2Tables)() : Promise.resolve(undefined)
|
|
143
257
|
]);
|
|
144
258
|
tables = loadedTables;
|
|
145
259
|
tables.cube3Phase1 = cube3P1Tables;
|
|
146
|
-
|
|
147
|
-
|
|
260
|
+
if (cube3P2Tables !== undefined)
|
|
261
|
+
tables.cube3Phase2 = cube3P2Tables;
|
|
148
262
|
}
|
|
149
263
|
catch (error) {
|
|
150
264
|
throw new Error("[cube4-scrambler] Failed to load reduction tables; run pnpm cube4:center1:tables etc. " +
|
|
151
265
|
`(${error instanceof Error ? error.message : String(error)})`);
|
|
152
266
|
}
|
|
153
267
|
const totalRestarts = Math.max(1, Math.floor(options.restarts ?? options.bestOf ?? 1) || 1);
|
|
268
|
+
const targetState = (0, random_state_js_1.generateRandomCube4State)(options);
|
|
154
269
|
let lastError;
|
|
155
270
|
try {
|
|
156
|
-
return await
|
|
271
|
+
return await solveCube4Attempt(tables, pairTables, {
|
|
157
272
|
...options,
|
|
158
273
|
restarts: totalRestarts
|
|
159
|
-
});
|
|
274
|
+
}, targetState);
|
|
160
275
|
}
|
|
161
276
|
catch (error) {
|
|
162
277
|
if (!isRetryableCube4Error(error))
|
|
@@ -165,11 +280,11 @@ async function generateCube4Scramble(options = {}) {
|
|
|
165
280
|
}
|
|
166
281
|
if (!options.deep) {
|
|
167
282
|
try {
|
|
168
|
-
return await
|
|
283
|
+
return await solveCube4Attempt(tables, pairTables, {
|
|
169
284
|
...options,
|
|
170
285
|
restarts: totalRestarts,
|
|
171
286
|
deep: true
|
|
172
|
-
});
|
|
287
|
+
}, targetState);
|
|
173
288
|
}
|
|
174
289
|
catch (error) {
|
|
175
290
|
if (isRetryableCube4Error(error))
|
|
@@ -184,12 +299,17 @@ async function generateCube4Scramble(options = {}) {
|
|
|
184
299
|
}
|
|
185
300
|
/**
|
|
186
301
|
* Single-attempt 444 solve: samples ONE state (from `options.seed`) and
|
|
187
|
-
* solves it with the reduction solver. Throws on failure —
|
|
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.
|
|
188
306
|
* Fan-out primitive for hosts: run with seeds `seed`, `seed:bestN`, ...
|
|
189
307
|
* across workers and keep the shortest valid result.
|
|
190
308
|
*/
|
|
191
309
|
async function generateCube4Attempt(tables, pairTables, options = {}) {
|
|
192
|
-
|
|
310
|
+
return solveCube4Attempt(tables, pairTables, options, (0, random_state_js_1.generateRandomCube4State)(options));
|
|
311
|
+
}
|
|
312
|
+
async function solveCube4Attempt(tables, pairTables, options, attemptState) {
|
|
193
313
|
const axes = options.threeAxes ? [[], ["z"], ["y'"]] : [[]];
|
|
194
314
|
let bestScrambleMoves = null;
|
|
195
315
|
let bestSolutionMoves = null;
|
|
@@ -206,36 +326,84 @@ async function generateCube4Attempt(tables, pairTables, options = {}) {
|
|
|
206
326
|
// The reduction search is bounded by deterministic work caps (see
|
|
207
327
|
// solve.ts), so these timeouts are only wall-clock backstops for
|
|
208
328
|
// pathological cases.
|
|
209
|
-
const solvePromise = (0, solve_js_1.solveCube4State)(sRot, tables, async (cube3State) => {
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
const
|
|
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) => {
|
|
214
334
|
const solution = await (0, index_js_1.solveCube3State)(cube3State, {
|
|
215
335
|
orientations: finishOpts.orientations ?? 3,
|
|
216
336
|
phase1ExtraDepth: finishOpts.phase1ExtraDepth ?? 1,
|
|
217
337
|
phase1SolutionLimit: finishOpts.phase1SolutionLimit ?? 24,
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
: {}),
|
|
220
348
|
// The 54MB pair tables are skipped for the 444 finish: pair
|
|
221
349
|
// pruning changes nothing measurable here (identical lengths
|
|
222
350
|
// on a 44-state corpus, zero extra finish retries), and
|
|
223
351
|
// loading them dominates cold-start I/O. Hosts that already
|
|
224
352
|
// hold pairTables can still pass them via generateCube4Attempt.
|
|
225
|
-
phase2: {
|
|
353
|
+
phase2: {
|
|
354
|
+
pairTables,
|
|
355
|
+
skipPairTables: pairTables === undefined,
|
|
356
|
+
maxNodes: Math.round((finishOpts.phase2MaxNodes ?? FINISH_PHASE2_MAX_NODES) * capScale)
|
|
357
|
+
}
|
|
226
358
|
});
|
|
227
359
|
return { moves: solution.moves };
|
|
228
360
|
};
|
|
229
|
-
|
|
230
|
-
|
|
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).
|
|
231
400
|
try {
|
|
232
|
-
return await
|
|
401
|
+
return await solveLegacyFinish(1);
|
|
233
402
|
}
|
|
234
403
|
catch {
|
|
235
|
-
return
|
|
404
|
+
return solveLegacyFinish(FINISH_RETRY_SCALE);
|
|
236
405
|
}
|
|
237
406
|
}, {
|
|
238
|
-
timeoutMs: 75000,
|
|
239
407
|
deep: options.deep === true,
|
|
240
408
|
deepPolish: options.deepPolish ?? options.threeAxes,
|
|
241
409
|
...(options.finishTries !== undefined ? { finishTries: options.finishTries } : {}),
|
|
@@ -245,14 +413,22 @@ async function generateCube4Attempt(tables, pairTables, options = {}) {
|
|
|
245
413
|
...(options.phase1Cap !== undefined ? { phase1Cap: options.phase1Cap } : {}),
|
|
246
414
|
...(options.center2Wide !== undefined ? { center2Wide: options.center2Wide } : {}),
|
|
247
415
|
...(options.restarts !== undefined ? { restarts: options.restarts } : {}),
|
|
416
|
+
finishScreenTop: finishOpts.mode === "legacy" ? 0 : finishOpts.screenTop ?? 0,
|
|
417
|
+
...(options.maxFixMoves !== undefined ? { maxFixMoves: options.maxFixMoves } : {}),
|
|
248
418
|
...(options.prepCap !== undefined ? { prepCap: options.prepCap } : {}),
|
|
249
419
|
...(options.p1CandCap !== undefined ? { p1CandCap: options.p1CandCap } : {}),
|
|
250
420
|
...(options.stagnationCut !== undefined ? { stagnationCut: options.stagnationCut } : {}),
|
|
251
421
|
maxReductionLen: bestReductionLen
|
|
252
422
|
});
|
|
253
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).
|
|
254
430
|
const timeoutPromise = new Promise((_, reject) => {
|
|
255
|
-
timeoutHandle = setTimeout(() => reject(new
|
|
431
|
+
timeoutHandle = setTimeout(() => reject(new Cube4WallTimeoutError("4x4 solve: timeout")), 90000);
|
|
256
432
|
});
|
|
257
433
|
let solveOutcome;
|
|
258
434
|
try {
|
|
@@ -265,9 +441,12 @@ async function generateCube4Attempt(tables, pairTables, options = {}) {
|
|
|
265
441
|
clearTimeout(timeoutHandle);
|
|
266
442
|
}
|
|
267
443
|
const { scramble: scrambleMoves, moves: solutionMoves, phases, timings, nodes } = solveOutcome;
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
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);
|
|
271
450
|
if (!bestScrambleMoves || fullScramble.length < bestScrambleMoves.length) {
|
|
272
451
|
bestScrambleMoves = fullScramble;
|
|
273
452
|
bestSolutionMoves = fullSolution;
|
|
@@ -287,13 +466,17 @@ async function generateCube4Attempt(tables, pairTables, options = {}) {
|
|
|
287
466
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
288
467
|
}
|
|
289
468
|
const scrambleMoves = bestScrambleMoves;
|
|
290
|
-
const solutionMoves = bestSolutionMoves;
|
|
291
469
|
const scrambledState = (0, moves_js_1.applyCube4Algorithm)((0, state_js_1.createSolvedCube4State)(), scrambleMoves);
|
|
292
470
|
if (!options.skipValidation) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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);
|
|
297
480
|
}
|
|
298
481
|
const facelets = (0, facelets_js_1.toCube4Facelets)(scrambledState);
|
|
299
482
|
const result = {
|
|
@@ -304,10 +487,18 @@ async function generateCube4Attempt(tables, pairTables, options = {}) {
|
|
|
304
487
|
generator: "cube4-reduction",
|
|
305
488
|
metadata: {
|
|
306
489
|
generator: "cube4-reduction",
|
|
307
|
-
|
|
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,
|
|
308
496
|
facelets,
|
|
309
497
|
faceletString: (0, facelets_js_1.formatCube4Facelets)(facelets),
|
|
310
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`.
|
|
311
502
|
solutionMoves: (0, moves_js_1.invertCube4Algorithm)(scrambleMoves),
|
|
312
503
|
// Winning-reduction breakdown (unsimplified lengths) + wall-clock
|
|
313
504
|
// split, for tuning and A/B comparisons.
|