@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.
Files changed (126) hide show
  1. package/README.md +15 -4
  2. package/dist/core/api.d.ts.map +1 -1
  3. package/dist/core/api.js +10 -2
  4. package/dist/puzzles/cube2/solver/browser-loader.d.ts +4 -7
  5. package/dist/puzzles/cube2/solver/browser-loader.d.ts.map +1 -1
  6. package/dist/puzzles/cube2/solver/browser-loader.js +42 -14
  7. package/dist/puzzles/cube2/solver/generated-assets.d.ts.map +1 -1
  8. package/dist/puzzles/cube2/solver/generated-assets.js +8 -3
  9. package/dist/puzzles/cube3/solver/browser-loader.d.ts.map +1 -1
  10. package/dist/puzzles/cube3/solver/browser-loader.js +42 -20
  11. package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
  12. package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
  13. package/dist/puzzles/cube3/solver/phase1.d.ts +19 -0
  14. package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
  15. package/dist/puzzles/cube3/solver/phase1.js +29 -6
  16. package/dist/puzzles/cube3/solver/phase2.d.ts +28 -0
  17. package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
  18. package/dist/puzzles/cube3/solver/phase2.js +56 -15
  19. package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
  20. package/dist/puzzles/cube3/solver/tables.js +14 -12
  21. package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
  22. package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
  23. package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
  24. package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
  25. package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
  26. package/dist/puzzles/cube4/browser-worker.js +31 -0
  27. package/dist/puzzles/cube4/index.d.ts +7 -0
  28. package/dist/puzzles/cube4/index.d.ts.map +1 -1
  29. package/dist/puzzles/cube4/index.js +4 -0
  30. package/dist/puzzles/cube4/moves.d.ts.map +1 -1
  31. package/dist/puzzles/cube4/moves.js +122 -30
  32. package/dist/puzzles/cube4/reduction/browser-loader.d.ts +26 -1
  33. package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
  34. package/dist/puzzles/cube4/reduction/browser-loader.js +212 -48
  35. package/dist/puzzles/cube4/reduction/center2.d.ts +24 -0
  36. package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
  37. package/dist/puzzles/cube4/reduction/center2.js +99 -1
  38. package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
  39. package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
  40. package/dist/puzzles/cube4/reduction/finish3.js +393 -0
  41. package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
  42. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +5 -3
  43. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -1
  44. package/dist/puzzles/cube4/reduction/hc-pdb.js +30 -12
  45. package/dist/puzzles/cube4/reduction/node-loader.d.ts +10 -1
  46. package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
  47. package/dist/puzzles/cube4/reduction/node-loader.js +170 -36
  48. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
  49. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
  50. package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
  51. package/dist/puzzles/cube4/reduction/phase3-search.d.ts +33 -30
  52. package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
  53. package/dist/puzzles/cube4/reduction/phase3-search.js +211 -104
  54. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
  55. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
  56. package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
  57. package/dist/puzzles/cube4/reduction/solve.d.ts +72 -13
  58. package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
  59. package/dist/puzzles/cube4/reduction/solve.js +374 -92
  60. package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
  61. package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
  62. package/dist/puzzles/cube4/scramble-cache.js +150 -0
  63. package/dist/puzzles/cube4/scrambler.d.ts +112 -18
  64. package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
  65. package/dist/puzzles/cube4/scrambler.js +271 -83
  66. package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
  67. package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
  68. package/dist/puzzles/cube4/worker-pool.js +405 -0
  69. package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
  70. package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
  71. package/dist/puzzles/cube4/worker-protocol.js +58 -0
  72. package/dist/puzzles/cube4/worker.d.ts +26 -0
  73. package/dist/puzzles/cube4/worker.d.ts.map +1 -0
  74. package/dist/puzzles/cube4/worker.js +49 -0
  75. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
  76. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
  77. package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
  78. package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
  79. package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
  80. package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
  81. package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
  82. package/dist/puzzles/shared/compressed-fetch.js +226 -31
  83. package/dist/puzzles/shared/load-once.d.ts +16 -0
  84. package/dist/puzzles/shared/load-once.d.ts.map +1 -0
  85. package/dist/puzzles/shared/load-once.js +11 -0
  86. package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
  87. package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
  88. package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
  89. package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
  90. package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
  91. package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
  92. package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
  93. package/dist-cjs/core/api.js +10 -2
  94. package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
  95. package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
  96. package/dist-cjs/puzzles/cube3/solver/browser-loader.js +42 -20
  97. package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
  98. package/dist-cjs/puzzles/cube3/solver/phase1.js +29 -6
  99. package/dist-cjs/puzzles/cube3/solver/phase2.js +56 -15
  100. package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
  101. package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
  102. package/dist-cjs/puzzles/cube4/index.js +21 -1
  103. package/dist-cjs/puzzles/cube4/moves.js +122 -30
  104. package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +212 -48
  105. package/dist-cjs/puzzles/cube4/reduction/center2.js +102 -2
  106. package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
  107. package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
  108. package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +31 -12
  109. package/dist-cjs/puzzles/cube4/reduction/node-loader.js +171 -36
  110. package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
  111. package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +211 -104
  112. package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
  113. package/dist-cjs/puzzles/cube4/reduction/solve.js +374 -88
  114. package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
  115. package/dist-cjs/puzzles/cube4/scrambler.js +271 -80
  116. package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
  117. package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
  118. package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
  119. package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
  120. package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
  121. package/dist-cjs/puzzles/shared/load-once.js +14 -0
  122. package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
  123. package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
  124. package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
  125. package/docs/development.md +19 -7
  126. package/package.json +4 -3
@@ -1,25 +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 scramble
4
- * always reproduces the sampled target state and facelets exactly.
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
- * Reduction-only, no resampling: each call runs one round of up to `bestOf`
7
- * independent attempts and returns the shortest success (ties keep the
8
- * lowest job index). If every attempt fails, the last error is thrown.
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
38
  import { formatCube4Algorithm, applyCube4Algorithm, invertCube4Algorithm } from "./moves.js";
11
39
  import { generateRandomCube4State } from "./random-state.js";
12
- import { createSolvedCube4State, isSolvedCube4State } from "./state.js";
40
+ import { createSolvedCube4State, cube4StatesEqual } from "./state.js";
13
41
  import { formatCube4Facelets, toCube4Facelets } from "./facelets.js";
14
- import { solveCube4State, applyCubeRotation, rotateAlgorithm } from "./reduction/solve.js";
15
- import { solveCube3State } from "../cube3/solver/index.js";
16
- import { loadCube3Phase1Tables } from "../cube3/solver/tables.js";
17
- import { loadCube3PairTables } from "../cube3/solver/pair-tables.js";
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";
18
47
  // ────────────────────────────────────────────────────────────
19
- let cachedTables;
20
- async function loadCube4Tables(baseUrl) {
21
- if (cachedTables)
22
- return cachedTables;
48
+ /**
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).
56
+ */
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;
85
+ }
86
+ async function loadCube4TablesOnce(baseUrl) {
23
87
  const isBrowser = typeof window !== "undefined" ||
24
88
  typeof document !== "undefined" ||
25
89
  (typeof self !== "undefined" && typeof self.importScripts === "function") ||
@@ -32,20 +96,15 @@ async function loadCube4Tables(baseUrl) {
32
96
  "./reduction/browser-loader.js");
33
97
  const cached = loader.getCachedCube4Tables();
34
98
  if (cached) {
35
- cachedTables = cached;
36
99
  return cached;
37
100
  }
38
- const tables = await loader.loadCube4ReductionTablesBrowser(baseUrl);
39
- cachedTables = tables;
40
- return tables;
101
+ return await loader.loadCube4ReductionTablesBrowser(baseUrl);
41
102
  }
42
103
  const loader = await import(
43
104
  /* webpackIgnore: true */
44
105
  /* @vite-ignore */
45
106
  "./reduction/node-loader.js");
46
- const tables = await loader.loadCube4ReductionTables();
47
- cachedTables = tables;
48
- return tables;
107
+ return await loader.loadCube4ReductionTables();
49
108
  }
50
109
  export const cube4ScrambleGenerator = {
51
110
  info: {
@@ -60,65 +119,118 @@ export const cube4ScrambleGenerator = {
60
119
  return generateCube4Scramble(options);
61
120
  }
62
121
  };
63
- function invertRotationSequence(rotSeq) {
64
- return rotSeq.slice().reverse().map((r) => (r.endsWith("'") ? r.slice(0, -1) : r.endsWith("2") ? r : `${r}'`));
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
+ }
65
133
  }
66
- /** Shortest-first pick (ties keep `a`, i.e. the lowest job index). */
67
- function shorterScramble(a, b) {
68
- if (!a || b.moves.length < a.moves.length)
69
- return b;
70
- return a;
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
+ }
71
141
  }
72
- function seedForBestJob(options, job) {
73
- if (job === 0)
74
- return options;
75
- const next = { ...options };
76
- if (options.seed !== undefined) {
77
- next.seed = `${options.seed}:best${job}`;
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;
78
178
  }
79
- else {
80
- delete next.seed;
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;
81
183
  }
82
- return next;
184
+ return SOLVER_RETRYABLE_MESSAGE_PATTERNS.some((pattern) => error.message.includes(pattern));
83
185
  }
84
- function isRetryableCube4Error(error) {
85
- const msg = error instanceof Error ? error.message : String(error);
86
- const isPhase2Error = typeof error?.name === "string" &&
87
- error.name.includes("Cube3Phase2");
88
- return (error instanceof Error &&
89
- (msg.includes("reduction failed") ||
90
- msg.includes("failed to convert") ||
91
- msg.includes("no valid phase 2") ||
92
- msg.includes("timeout") ||
93
- isPhase2Error ||
94
- msg.includes("Phase 2 solution not found")));
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
+ }
95
203
  }
96
204
  export async function generateCube4Scramble(options = {}) {
97
- const bestOf = Math.max(1, Math.floor(options.bestOf ?? 1) || 1);
98
205
  let tables;
99
- let pairTables;
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;
100
210
  try {
101
- const [loadedTables, cube3P1Tables, loadedPairTables] = await Promise.all([
211
+ const continuousFinish = options.finish?.mode !== "legacy";
212
+ const [loadedTables, cube3P1Tables, cube3P2Tables] = await Promise.all([
102
213
  loadCube4Tables(),
103
214
  loadCube3Phase1Tables(),
104
- loadCube3PairTables()
215
+ continuousFinish ? loadCube3Phase2Tables() : Promise.resolve(undefined)
105
216
  ]);
106
217
  tables = loadedTables;
107
218
  tables.cube3Phase1 = cube3P1Tables;
108
- tables.pairTables = loadedPairTables;
109
- pairTables = loadedPairTables;
219
+ if (cube3P2Tables !== undefined)
220
+ tables.cube3Phase2 = cube3P2Tables;
110
221
  }
111
222
  catch (error) {
112
223
  throw new Error("[cube4-scrambler] Failed to load reduction tables; run pnpm cube4:center1:tables etc. " +
113
224
  `(${error instanceof Error ? error.message : String(error)})`);
114
225
  }
115
226
  const totalRestarts = Math.max(1, Math.floor(options.restarts ?? options.bestOf ?? 1) || 1);
227
+ const targetState = generateRandomCube4State(options);
116
228
  let lastError;
117
229
  try {
118
- return await generateCube4Attempt(tables, pairTables, {
230
+ return await solveCube4Attempt(tables, pairTables, {
119
231
  ...options,
120
232
  restarts: totalRestarts
121
- });
233
+ }, targetState);
122
234
  }
123
235
  catch (error) {
124
236
  if (!isRetryableCube4Error(error))
@@ -127,11 +239,11 @@ export async function generateCube4Scramble(options = {}) {
127
239
  }
128
240
  if (!options.deep) {
129
241
  try {
130
- return await generateCube4Attempt(tables, pairTables, {
242
+ return await solveCube4Attempt(tables, pairTables, {
131
243
  ...options,
132
244
  restarts: totalRestarts,
133
245
  deep: true
134
- });
246
+ }, targetState);
135
247
  }
136
248
  catch (error) {
137
249
  if (isRetryableCube4Error(error))
@@ -146,12 +258,17 @@ export async function generateCube4Scramble(options = {}) {
146
258
  }
147
259
  /**
148
260
  * Single-attempt 444 solve: samples ONE state (from `options.seed`) and
149
- * solves it with the reduction solver. Throws on failure — no resampling.
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.
150
265
  * Fan-out primitive for hosts: run with seeds `seed`, `seed:bestN`, ...
151
266
  * across workers and keep the shortest valid result.
152
267
  */
153
268
  export async function generateCube4Attempt(tables, pairTables, options = {}) {
154
- const attemptState = generateRandomCube4State(options);
269
+ return solveCube4Attempt(tables, pairTables, options, generateRandomCube4State(options));
270
+ }
271
+ async function solveCube4Attempt(tables, pairTables, options, attemptState) {
155
272
  const axes = options.threeAxes ? [[], ["z"], ["y'"]] : [[]];
156
273
  let bestScrambleMoves = null;
157
274
  let bestSolutionMoves = null;
@@ -168,36 +285,84 @@ export async function generateCube4Attempt(tables, pairTables, options = {}) {
168
285
  // The reduction search is bounded by deterministic work caps (see
169
286
  // solve.ts), so these timeouts are only wall-clock backstops for
170
287
  // pathological cases.
171
- const solvePromise = solveCube4State(sRot, tables, async (cube3State) => {
172
- // Single solver-side best-of-orientations finish call with
173
- // extra phase-1 improvement: same lengths as the former
174
- // hand-rolled UD/X/Z triple at a fraction of the cost.
175
- const solveFinish = async (timeBudget, phase2Budget) => {
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) => {
176
293
  const solution = await solveCube3State(cube3State, {
177
294
  orientations: finishOpts.orientations ?? 3,
178
295
  phase1ExtraDepth: finishOpts.phase1ExtraDepth ?? 1,
179
296
  phase1SolutionLimit: finishOpts.phase1SolutionLimit ?? 24,
180
- targetLength: finishOpts.targetLength ?? 20,
181
- timeoutMs: timeBudget,
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
+ : {}),
182
307
  // The 54MB pair tables are skipped for the 444 finish: pair
183
308
  // pruning changes nothing measurable here (identical lengths
184
309
  // on a 44-state corpus, zero extra finish retries), and
185
310
  // loading them dominates cold-start I/O. Hosts that already
186
311
  // hold pairTables can still pass them via generateCube4Attempt.
187
- phase2: { pairTables, skipPairTables: pairTables === undefined, timeoutMs: phase2Budget }
312
+ phase2: {
313
+ pairTables,
314
+ skipPairTables: pairTables === undefined,
315
+ maxNodes: Math.round((finishOpts.phase2MaxNodes ?? FINISH_PHASE2_MAX_NODES) * capScale)
316
+ }
188
317
  });
189
318
  return { moves: solution.moves };
190
319
  };
191
- // The first call seeds everything, so a flaky finish gets one
192
- // budgeted retry before the whole finish is declared failed.
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).
193
359
  try {
194
- return await solveFinish(finishOpts.timeoutMs ?? 150, finishOpts.phase2TimeoutMs ?? 50);
360
+ return await solveLegacyFinish(1);
195
361
  }
196
362
  catch {
197
- return await solveFinish(finishOpts.retryTimeoutMs ?? 300, finishOpts.retryPhase2TimeoutMs ?? 100);
363
+ return solveLegacyFinish(FINISH_RETRY_SCALE);
198
364
  }
199
365
  }, {
200
- timeoutMs: 75000,
201
366
  deep: options.deep === true,
202
367
  deepPolish: options.deepPolish ?? options.threeAxes,
203
368
  ...(options.finishTries !== undefined ? { finishTries: options.finishTries } : {}),
@@ -207,14 +372,22 @@ export async function generateCube4Attempt(tables, pairTables, options = {}) {
207
372
  ...(options.phase1Cap !== undefined ? { phase1Cap: options.phase1Cap } : {}),
208
373
  ...(options.center2Wide !== undefined ? { center2Wide: options.center2Wide } : {}),
209
374
  ...(options.restarts !== undefined ? { restarts: options.restarts } : {}),
375
+ finishScreenTop: finishOpts.mode === "legacy" ? 0 : finishOpts.screenTop ?? 0,
376
+ ...(options.maxFixMoves !== undefined ? { maxFixMoves: options.maxFixMoves } : {}),
210
377
  ...(options.prepCap !== undefined ? { prepCap: options.prepCap } : {}),
211
378
  ...(options.p1CandCap !== undefined ? { p1CandCap: options.p1CandCap } : {}),
212
379
  ...(options.stagnationCut !== undefined ? { stagnationCut: options.stagnationCut } : {}),
213
380
  maxReductionLen: bestReductionLen
214
381
  });
215
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).
216
389
  const timeoutPromise = new Promise((_, reject) => {
217
- timeoutHandle = setTimeout(() => reject(new Error("4x4 solve: timeout")), 90000);
390
+ timeoutHandle = setTimeout(() => reject(new Cube4WallTimeoutError("4x4 solve: timeout")), 90000);
218
391
  });
219
392
  let solveOutcome;
220
393
  try {
@@ -227,9 +400,12 @@ export async function generateCube4Attempt(tables, pairTables, options = {}) {
227
400
  clearTimeout(timeoutHandle);
228
401
  }
229
402
  const { scramble: scrambleMoves, moves: solutionMoves, phases, timings, nodes } = solveOutcome;
230
- const invRot = rot.length === 0 ? [] : invertRotationSequence(rot);
231
- const fullScramble = rot.length === 0 ? scrambleMoves : rotateAlgorithm(scrambleMoves, invRot);
232
- const fullSolution = rot.length === 0 ? solutionMoves : rotateAlgorithm(solutionMoves, invRot);
403
+ const fullSolution = rot.length === 0
404
+ ? solutionMoves
405
+ : placeRotationFix([], rot, solutionMoves);
406
+ const fullScramble = rot.length === 0
407
+ ? scrambleMoves
408
+ : invertCube4Algorithm(fullSolution);
233
409
  if (!bestScrambleMoves || fullScramble.length < bestScrambleMoves.length) {
234
410
  bestScrambleMoves = fullScramble;
235
411
  bestSolutionMoves = fullSolution;
@@ -249,13 +425,17 @@ export async function generateCube4Attempt(tables, pairTables, options = {}) {
249
425
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
250
426
  }
251
427
  const scrambleMoves = bestScrambleMoves;
252
- const solutionMoves = bestSolutionMoves;
253
428
  const scrambledState = applyCube4Algorithm(createSolvedCube4State(), scrambleMoves);
254
429
  if (!options.skipValidation) {
255
- const solvedCheck = applyCube4Algorithm(scrambledState, solutionMoves);
256
- if (!isSolvedCube4State(solvedCheck)) {
257
- throw new Error("Generated 4x4 scramble does not solve back to solved state.");
258
- }
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);
259
439
  }
260
440
  const facelets = toCube4Facelets(scrambledState);
261
441
  const result = {
@@ -266,10 +446,18 @@ export async function generateCube4Attempt(tables, pairTables, options = {}) {
266
446
  generator: "cube4-reduction",
267
447
  metadata: {
268
448
  generator: "cube4-reduction",
269
- state: scrambledState,
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,
270
455
  facelets,
271
456
  faceletString: formatCube4Facelets(facelets),
272
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`.
273
461
  solutionMoves: invertCube4Algorithm(scrambleMoves),
274
462
  // Winning-reduction breakdown (unsimplified lengths) + wall-clock
275
463
  // split, for tuning and A/B comparisons.
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Worker pool for 4x4 random-state scrambles (Node.js `worker_threads`).
3
+ *
4
+ * Why: generation is CPU-bound in a single event loop — a `Promise.all`
5
+ * batch of N scrambles does not scale. This pool spreads one batch across
6
+ * N threads so `generateScrambles`-style batches grow roughly linearly
7
+ * with core count, while keeping the (seed, complete-options) → moves
8
+ * contract byte-identical to the inline path: tasks forward the FULL
9
+ * options object verbatim and `worker.ts` calls the very same
10
+ * `generateCube4Scramble`.
11
+ *
12
+ * Lifecycle:
13
+ * - Lazy spawn: threads are created on demand (one per queued job, up to
14
+ * `size`), never in the constructor. A cold first generation pays for
15
+ * table loading (≈37 MB) per spawned thread, once; the thread then
16
+ * reuses its tables for every later job (per-thread promise cache inside
17
+ * `scrambler.ts`).
18
+ * - Idle threads are `unref()`d, so a pool that is never terminated does
19
+ * not keep the process alive; in-flight jobs `ref()` their thread so an
20
+ * awaited batch always completes.
21
+ * - One task in flight per thread; extra tasks queue FIFO.
22
+ * - `terminate()` rejects everything queued/in-flight with
23
+ * `Cube4WorkerPoolClosedError` and joins all threads — after it
24
+ * resolves there are no open handles.
25
+ *
26
+ * Failure: a crashed thread only fails ITS in-flight task and is dropped
27
+ * from the pool; the next dispatch spawns a fresh thread (which reloads
28
+ * tables). Typed solver errors from inside a worker arrive as an `Error`
29
+ * with the original `name`, `message` and `code` preserved (see
30
+ * `worker-protocol.ts`).
31
+ *
32
+ * Dual-build note: this file intentionally contains no bare `import.meta`
33
+ * (it must also compile in the CommonJS build that `index.ts` is part of).
34
+ * The worker entry is located from `__filename` (CJS) or this module's top
35
+ * stack frame (ESM), and `dist-cjs` falls back to the sibling ESM tree,
36
+ * which always ships `worker.js`.
37
+ */
38
+ import type { ScrambleResult } from "../../core/types.js";
39
+ import type { Cube4ScrambleOptions } from "./scrambler.js";
40
+ import { type Cube4WorkerGenerationOptions } from "./worker-protocol.js";
41
+ import type { ScrambleOptions } from "../../core/types.js";
42
+ /** Full option surface accepted by the pool and the batch helper. */
43
+ export type Cube4PoolOptions = Cube4WorkerGenerationOptions;
44
+ export interface Cube4WorkerPoolOptions {
45
+ /**
46
+ * Maximum number of worker threads. Defaults to
47
+ * `os.availableParallelism() - 1` (min 1), resolved lazily so importing
48
+ * this module costs nothing.
49
+ */
50
+ size?: number;
51
+ /**
52
+ * Explicit path to the worker entry (`worker.ts` / `worker.js`).
53
+ * Auto-resolved normally; pass this only if the automatic location
54
+ * fails (e.g. a bundler renamed this module).
55
+ */
56
+ workerPath?: string;
57
+ }
58
+ /** Typed rejection used for post-terminate operations and shutdown itself. */
59
+ export declare class Cube4WorkerPoolClosedError extends Error {
60
+ readonly code: "cube4-pool-closed";
61
+ constructor(action?: string);
62
+ }
63
+ /**
64
+ * Batch seed derivation — byte-for-byte the semantics of
65
+ * `deriveBatchScrambleOptions` in `src/core/api.ts` (`generateScrambles`):
66
+ * with a `seed` and `count > 1`, item i uses `${seed}:${i}`; otherwise the
67
+ * options pass through untouched. Exported so tests can assert the pool
68
+ * batch equals the sequential/core batch for the same inputs.
69
+ */
70
+ export declare function deriveBatchCube4Options(options: Cube4PoolOptions, index: number, count: number): Cube4PoolOptions;
71
+ export declare class Cube4WorkerPool {
72
+ private readonly workers;
73
+ private readonly queue;
74
+ private readonly explicitSize;
75
+ private readonly workerPathOption;
76
+ private sizePromise;
77
+ /** Last resolved size, kept sync-readable for the pump's headroom check. */
78
+ private sizeSync;
79
+ private pumping;
80
+ private terminatePromise;
81
+ private nextId;
82
+ private terminated;
83
+ constructor(options?: Cube4WorkerPoolOptions);
84
+ /** Maximum thread count (resolves the default lazily on first use). */
85
+ get size(): Promise<number>;
86
+ /** Threads spawned so far (lazily grown; 0 before the first job). */
87
+ get workerCount(): number;
88
+ get queuedCount(): number;
89
+ get busyCount(): number;
90
+ get closed(): boolean;
91
+ /**
92
+ * Generates one scramble on an available thread. Rejects (typed) if the
93
+ * pool is closed, the worker crashes, the options cannot be
94
+ * structured-cloned, or generation fails inside the worker.
95
+ */
96
+ generate(options?: Cube4PoolOptions): Promise<ScrambleResult>;
97
+ /**
98
+ * Batch generation with EXACTLY the seed derivation of core
99
+ * `generateScrambles` (see `deriveBatchCube4Options`), so results are
100
+ * identical — move for move — to the sequential path for the same
101
+ * inputs. Order of the returned array matches request order regardless
102
+ * of completion order.
103
+ */
104
+ generateBatch(count: number, options?: Cube4PoolOptions): Promise<ScrambleResult[]>;
105
+ /**
106
+ * Prompt shutdown: rejects queued and in-flight tasks with
107
+ * `Cube4WorkerPoolClosedError`, terminates every thread and resolves
108
+ * when none is left. Idempotent.
109
+ */
110
+ terminate(): Promise<void>;
111
+ private resolveSize;
112
+ /**
113
+ * Serialised dispatcher: grows the pool (lazy spawn), hands queued jobs
114
+ * to idle threads, and stops when every thread is busy at capacity.
115
+ * Re-entry while a spawn is awaited is a no-op — the running loop's
116
+ * `while` re-checks the queue, and every completion event (`response`,
117
+ * `crash`) calls `pump()` again.
118
+ */
119
+ private pump;
120
+ private hasDispatchHeadroom;
121
+ private runPump;
122
+ private spawnWorker;
123
+ private dispatch;
124
+ private handleResponse;
125
+ private handleWorkerGone;
126
+ private resolveWorkerEntry;
127
+ }
128
+ /** Shared lazily-created pool (used by `generateCube4ScrambleBatch`). */
129
+ export declare function getDefaultCube4WorkerPool(): Cube4WorkerPool;
130
+ /** Terminates (and forgets) the shared pool — call on process shutdown. */
131
+ export declare function closeDefaultCube4WorkerPool(): Promise<void>;
132
+ /**
133
+ * Batch convenience mirroring core `generateScrambles` for `444`:
134
+ * - `count <= 1` runs INLINE in the caller (no thread spawn, no cold
135
+ * start — a single scramble must never get slower);
136
+ * - `count > 1` fans out over the shared pool (same seed derivation).
137
+ */
138
+ export declare function generateCube4ScrambleBatch(count: number, options?: ScrambleOptions & Cube4ScrambleOptions): Promise<ScrambleResult[]>;
139
+ //# sourceMappingURL=worker-pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-pool.d.ts","sourceRoot":"","sources":["../../../src/puzzles/cube4/worker-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAEL,KAAK,4BAA4B,EAGlC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,qEAAqE;AACrE,MAAM,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAE5D,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,8EAA8E;AAC9E,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,EAAG,mBAAmB,CAAU;gBAEjC,MAAM,SAAc;CAIjC;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,gBAAgB,CAKlB;AAsED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,WAAW,CAA8B;IACjD,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,UAAU,CAAS;gBAEf,OAAO,GAAE,sBAA2B;IAWhD,uEAAuE;IACvE,IAAI,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAE1B;IAED,qEAAqE;IACrE,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;;;OAIG;IACH,QAAQ,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;IAWjE;;;;;;OAMG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAWvF;;;;OAIG;IACH,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB1B,OAAO,CAAC,WAAW;IAmBnB;;;;;;OAMG;IACH,OAAO,CAAC,IAAI;IAiBZ,OAAO,CAAC,mBAAmB;YAKb,OAAO;YAuBP,WAAW;IA6BzB,OAAO,CAAC,QAAQ;IAgBhB,OAAO,CAAC,cAAc;IAetB,OAAO,CAAC,gBAAgB;IAexB,OAAO,CAAC,kBAAkB;CAkC3B;AAMD,yEAAyE;AACzE,wBAAgB,yBAAyB,IAAI,eAAe,CAG3D;AAED,2EAA2E;AAC3E,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC,CAIjE;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,eAAe,GAAG,oBAAyB,GACnD,OAAO,CAAC,cAAc,EAAE,CAAC,CAQ3B"}