@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
|
@@ -158,6 +158,32 @@ const FACE_MOVE_EFFECTS = {
|
|
|
158
158
|
Uw: WIDE_Uw, Dw: WIDE_Dw, Rw: WIDE_Rw, Lw: WIDE_Lw, Fw: WIDE_Fw, Bw: WIDE_Bw
|
|
159
159
|
};
|
|
160
160
|
// ---------------------------------------------------------------------------
|
|
161
|
+
// Precomposed total effect per move string ("R'" = R x3, "U2" = U x2),
|
|
162
|
+
// built once at module load. applyCube4Algorithm then costs one lookup +
|
|
163
|
+
// one pass per move: no per-move string parsing, no repeated
|
|
164
|
+
// quarter-turn passes (average ~2 passes collapse into exactly 1).
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
function composePower(face, turns) {
|
|
167
|
+
const quarter = FACE_MOVE_EFFECTS[face];
|
|
168
|
+
if (turns === 2)
|
|
169
|
+
return composeEffects(quarter, quarter);
|
|
170
|
+
if (turns === 3)
|
|
171
|
+
return composeEffects(composeEffects(quarter, quarter), quarter);
|
|
172
|
+
return quarter;
|
|
173
|
+
}
|
|
174
|
+
const COMPOSED_EFFECTS = Object.fromEntries(CUBE4_ALL_MOVES.map((move) => {
|
|
175
|
+
const { face, turns } = parseMove(move);
|
|
176
|
+
return [move, composePower(face, turns)];
|
|
177
|
+
}));
|
|
178
|
+
/** Total effect for one move string (precomposed fast path + parse fallback). */
|
|
179
|
+
function effectForMove(move) {
|
|
180
|
+
const hit = COMPOSED_EFFECTS[move];
|
|
181
|
+
if (hit !== undefined)
|
|
182
|
+
return hit;
|
|
183
|
+
const { face, turns } = parseMove(move);
|
|
184
|
+
return composePower(face, turns);
|
|
185
|
+
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
161
187
|
// Apply a single move to a state
|
|
162
188
|
// ---------------------------------------------------------------------------
|
|
163
189
|
function applyEffect(state, effect) {
|
|
@@ -192,20 +218,52 @@ export function parseMove(move) {
|
|
|
192
218
|
return { face: face, suffix, turns };
|
|
193
219
|
}
|
|
194
220
|
export function applyCube4Move(state, move) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
current = applyEffect(current, effect);
|
|
200
|
-
}
|
|
201
|
-
return current;
|
|
221
|
+
// Single-pass fast path through the precomposed total effect: one lookup
|
|
222
|
+
// + one pass, no string re-parsing, no repeated quarter-turn passes, no
|
|
223
|
+
// intermediate states.
|
|
224
|
+
return applyEffect(state, effectForMove(move));
|
|
202
225
|
}
|
|
203
226
|
export function applyCube4Algorithm(state, moves) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
227
|
+
if (moves.length === 0)
|
|
228
|
+
return state;
|
|
229
|
+
const bufA = {
|
|
230
|
+
cp: new Uint8Array(state.cp),
|
|
231
|
+
co: new Uint8Array(state.co),
|
|
232
|
+
wp: new Uint8Array(state.wp),
|
|
233
|
+
centers: new Uint8Array(state.centers)
|
|
234
|
+
};
|
|
235
|
+
const bufB = {
|
|
236
|
+
cp: new Uint8Array(8),
|
|
237
|
+
co: new Uint8Array(8),
|
|
238
|
+
wp: new Uint8Array(24),
|
|
239
|
+
centers: new Uint8Array(24)
|
|
240
|
+
};
|
|
241
|
+
let cur = bufA;
|
|
242
|
+
let nxt = bufB;
|
|
243
|
+
for (let mIdx = 0; mIdx < moves.length; mIdx++) {
|
|
244
|
+
const move = moves[mIdx];
|
|
245
|
+
const eff = effectForMove(move);
|
|
246
|
+
for (let i = 0; i < 8; i++) {
|
|
247
|
+
const from = eff.cperm[i];
|
|
248
|
+
nxt.cp[i] = cur.cp[from];
|
|
249
|
+
nxt.co[i] = (cur.co[from] + eff.corient[i]) % 3;
|
|
250
|
+
}
|
|
251
|
+
for (let i = 0; i < 24; i++) {
|
|
252
|
+
nxt.wp[i] = cur.wp[eff.wperm[i]];
|
|
253
|
+
}
|
|
254
|
+
for (let i = 0; i < 24; i++) {
|
|
255
|
+
nxt.centers[i] = cur.centers[eff.ctperm[i]];
|
|
256
|
+
}
|
|
257
|
+
const tmp = cur;
|
|
258
|
+
cur = nxt;
|
|
259
|
+
nxt = tmp;
|
|
207
260
|
}
|
|
208
|
-
return
|
|
261
|
+
return {
|
|
262
|
+
cp: cur.cp,
|
|
263
|
+
co: cur.co,
|
|
264
|
+
wp: cur.wp,
|
|
265
|
+
centers: cur.centers
|
|
266
|
+
};
|
|
209
267
|
}
|
|
210
268
|
// ---------------------------------------------------------------------------
|
|
211
269
|
// Move inversion
|
|
@@ -237,6 +295,149 @@ export function parseCube4Move(value) {
|
|
|
237
295
|
}
|
|
238
296
|
throw new Error(`Invalid 4x4 move: ${value}`);
|
|
239
297
|
}
|
|
298
|
+
function parse4Move(m) {
|
|
299
|
+
const match = m.match(/^([UDFBLR]w?)(2|')?$/);
|
|
300
|
+
if (!match)
|
|
301
|
+
throw new Error(`Invalid move format: ${m}`);
|
|
302
|
+
const base = match[1];
|
|
303
|
+
const suffix = match[2] ?? "";
|
|
304
|
+
const amount = suffix === "2" ? 2 : suffix === "'" ? 3 : 1;
|
|
305
|
+
const axis = (base[0] === "U" || base[0] === "D") ? 0 :
|
|
306
|
+
(base[0] === "R" || base[0] === "L") ? 1 : 2;
|
|
307
|
+
return { base, axis, amount };
|
|
308
|
+
}
|
|
309
|
+
const AXIS_FACES = [
|
|
310
|
+
["U", "D"],
|
|
311
|
+
["R", "L"],
|
|
312
|
+
["F", "B"]
|
|
313
|
+
];
|
|
314
|
+
const AXIS_TURN_OPTIONS = [
|
|
315
|
+
["2", 2],
|
|
316
|
+
["", 1],
|
|
317
|
+
["'", 3]
|
|
318
|
+
];
|
|
319
|
+
function axisVectorKey(vector) {
|
|
320
|
+
return vector[0] | (vector[1] << 2) | (vector[2] << 4) | (vector[3] << 6);
|
|
321
|
+
}
|
|
322
|
+
function applyMoveToAxisVector(vector, base, turns, out) {
|
|
323
|
+
out[0] = vector[0];
|
|
324
|
+
out[1] = vector[1];
|
|
325
|
+
out[2] = vector[2];
|
|
326
|
+
out[3] = vector[3];
|
|
327
|
+
const axis = base[0];
|
|
328
|
+
const [positive, negative] = AXIS_FACES.find(([p, n]) => p === axis || n === axis);
|
|
329
|
+
const isPositive = axis === positive;
|
|
330
|
+
const signedTurns = isPositive ? turns : 4 - turns;
|
|
331
|
+
const wide = base.endsWith("w");
|
|
332
|
+
if (wide) {
|
|
333
|
+
const firstLayer = isPositive ? 0 : 2;
|
|
334
|
+
out[firstLayer] = (out[firstLayer] + signedTurns) & 3;
|
|
335
|
+
out[firstLayer + 1] = (out[firstLayer + 1] + signedTurns) & 3;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const layer = isPositive ? 0 : 3;
|
|
339
|
+
out[layer] = (out[layer] + signedTurns) & 3;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function buildAxisReductionTable(positiveFace, negativeFace) {
|
|
343
|
+
const bases = [positiveFace, negativeFace, `${positiveFace}w`, `${negativeFace}w`];
|
|
344
|
+
const paths = new Array(256);
|
|
345
|
+
const queue = new Uint8Array(256);
|
|
346
|
+
let head = 0;
|
|
347
|
+
let tail = 0;
|
|
348
|
+
paths[0] = [];
|
|
349
|
+
queue[tail++] = 0;
|
|
350
|
+
while (head < tail) {
|
|
351
|
+
const key = queue[head++];
|
|
352
|
+
const vector = [key & 3, (key >> 2) & 3, (key >> 4) & 3, (key >> 6) & 3];
|
|
353
|
+
const prefix = paths[key];
|
|
354
|
+
for (const base of bases) {
|
|
355
|
+
for (const [suffix, turns] of AXIS_TURN_OPTIONS) {
|
|
356
|
+
const nextVector = [0, 0, 0, 0];
|
|
357
|
+
applyMoveToAxisVector(vector, base, turns, nextVector);
|
|
358
|
+
const nextKey = axisVectorKey(nextVector);
|
|
359
|
+
if (paths[nextKey] !== undefined)
|
|
360
|
+
continue;
|
|
361
|
+
paths[nextKey] = [...prefix, `${base}${suffix}`];
|
|
362
|
+
queue[tail++] = nextKey;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (tail !== 256 || paths.some((path) => path === undefined)) {
|
|
367
|
+
throw new Error(`4x4 move simplifier: incomplete axis table for ${positiveFace}/${negativeFace}.`);
|
|
368
|
+
}
|
|
369
|
+
return paths;
|
|
370
|
+
}
|
|
371
|
+
const AXIS_REDUCTIONS = AXIS_FACES.map(([positive, negative]) => buildAxisReductionTable(positive, negative));
|
|
372
|
+
function encodeAxisRun(moves, axis) {
|
|
373
|
+
const vector = [0, 0, 0, 0];
|
|
374
|
+
const next = [0, 0, 0, 0];
|
|
375
|
+
for (const move of moves) {
|
|
376
|
+
if (move.axis === axis) {
|
|
377
|
+
applyMoveToAxisVector(vector, move.base, move.amount, next);
|
|
378
|
+
vector[0] = next[0];
|
|
379
|
+
vector[1] = next[1];
|
|
380
|
+
vector[2] = next[2];
|
|
381
|
+
vector[3] = next[3];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return axisVectorKey(vector);
|
|
385
|
+
}
|
|
386
|
+
function addAxisKeys(a, b) {
|
|
387
|
+
let result = 0;
|
|
388
|
+
for (let layer = 0; layer < 4; layer++) {
|
|
389
|
+
const shift = layer * 2;
|
|
390
|
+
const amount = (((a >> shift) & 3) + ((b >> shift) & 3)) & 3;
|
|
391
|
+
result |= amount << shift;
|
|
392
|
+
}
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Simplifies a 4x4 algorithm by canceling inverse moves and merging redundant
|
|
397
|
+
* turns on the same face or across commuting parallel faces.
|
|
398
|
+
*/
|
|
399
|
+
export function simplifyCube4Algorithm(moves) {
|
|
400
|
+
// Moves on parallel layers commute, and each maximal same-axis run is a
|
|
401
|
+
// four-coordinate vector of quarter turns (one coordinate per layer).
|
|
402
|
+
// A small exact shortest-path table maps that vector back to the fewest
|
|
403
|
+
// legal face/wide moves, including mixed identities such as Rw R' = the
|
|
404
|
+
// corresponding inner-slice turn. Different axes are never reordered.
|
|
405
|
+
const segments = [];
|
|
406
|
+
let run = [];
|
|
407
|
+
let runAxis = -1;
|
|
408
|
+
const flushRun = () => {
|
|
409
|
+
if (run.length === 0)
|
|
410
|
+
return;
|
|
411
|
+
const key = encodeAxisRun(run, runAxis);
|
|
412
|
+
if (key !== 0) {
|
|
413
|
+
const previous = segments[segments.length - 1];
|
|
414
|
+
if (previous?.axis === runAxis) {
|
|
415
|
+
const mergedKey = addAxisKeys(previous.key, key);
|
|
416
|
+
if (mergedKey === 0)
|
|
417
|
+
segments.pop();
|
|
418
|
+
else {
|
|
419
|
+
previous.key = mergedKey;
|
|
420
|
+
previous.moves = AXIS_REDUCTIONS[runAxis][mergedKey];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
segments.push({ axis: runAxis, key, moves: AXIS_REDUCTIONS[runAxis][key] });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
run = [];
|
|
428
|
+
runAxis = -1;
|
|
429
|
+
};
|
|
430
|
+
for (const move of moves) {
|
|
431
|
+
const parsed = parse4Move(move);
|
|
432
|
+
if (runAxis !== -1 && parsed.axis !== runAxis)
|
|
433
|
+
flushRun();
|
|
434
|
+
if (runAxis === -1)
|
|
435
|
+
runAxis = parsed.axis;
|
|
436
|
+
run.push(parsed);
|
|
437
|
+
}
|
|
438
|
+
flushRun();
|
|
439
|
+
return segments.flatMap((segment) => segment.moves);
|
|
440
|
+
}
|
|
240
441
|
// ---------------------------------------------------------------------------
|
|
241
442
|
// Export the raw effects for use by the reduction solver
|
|
242
443
|
// ---------------------------------------------------------------------------
|
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser loader for the cube4 reduction tables (the committed .bin files).
|
|
3
|
-
* The manifests are per-phase (`center1-manifest.json`, ...)
|
|
3
|
+
* The manifests are per-phase (`center1-manifest.json`, ...) plus the shared
|
|
4
|
+
* `hc-manifest.json`, which also seals the optional `c3_pair_dist.bin`.
|
|
5
|
+
*
|
|
6
|
+
* Integrity — identical to node-loader.ts: every downloaded .bin is verified
|
|
7
|
+
* against the manifest sha256 + expected byte length (on the DECOMPRESSED
|
|
8
|
+
* payload, so .br/.gz variants and the raw file are checked the same way).
|
|
9
|
+
*
|
|
10
|
+
* Optional tables (`hc` + `c3PairDist`), NEVER silent:
|
|
11
|
+
* - `hc-manifest.json` present → its entries must download and verify, else
|
|
12
|
+
* the whole load REJECTS (node throws too). There is no silent degradation
|
|
13
|
+
* to an unpruned search: it would change the scramble produced for the
|
|
14
|
+
* same seed, breaking node/browser determinism.
|
|
15
|
+
* - `hc-manifest.json` absent (404) → the optional tables were not deployed;
|
|
16
|
+
* `c3_pair_dist.bin` is then probed to tell "not deployed" (both absent →
|
|
17
|
+
* skip, like node) apart from "stale/half-deployed assets" (bin present,
|
|
18
|
+
* manifest missing → REJECT with an actionable message — node refuses the
|
|
19
|
+
* same asset set).
|
|
20
|
+
* There is deliberately no "degraded" flag: the only valid outcomes are
|
|
21
|
+
* "fully loaded tables" or "loud failure", so both platforms always compute
|
|
22
|
+
* the same scrambles.
|
|
23
|
+
*
|
|
24
|
+
* Caching: concurrent loads share ONE in-flight promise (one fetch pass per
|
|
25
|
+
* table). The promise is kept after success; when it rejects it is purged so
|
|
26
|
+
* the next call can retry instead of being stuck on a poisoned cache.
|
|
4
27
|
*/
|
|
5
28
|
import type { Center1Tables } from "./center1.js";
|
|
6
29
|
import type { Center2Tables } from "./center2.js";
|
|
7
30
|
import type { Center3Tables } from "./center3.js";
|
|
8
31
|
import type { Edge3Tables } from "./edge3.js";
|
|
32
|
+
import { type HcDenseTables } from "./hc-pdb.js";
|
|
9
33
|
export interface Cube4ReductionTables {
|
|
10
34
|
center1: Center1Tables;
|
|
11
35
|
center2: Center2Tables;
|
|
12
36
|
center3: Center3Tables;
|
|
13
37
|
edge3: Edge3Tables;
|
|
38
|
+
/** Half-class exact PDB distances (optional; absent = bound disabled). */
|
|
39
|
+
hc?: HcDenseTables;
|
|
40
|
+
/** Joint Center3 x Single Pair PDB distances (optional; node parity). */
|
|
41
|
+
c3PairDist?: Uint8Array;
|
|
14
42
|
}
|
|
15
43
|
export declare function getCachedCube4Tables(): Cube4ReductionTables | undefined;
|
|
16
44
|
export declare function clearCachedCube4Tables(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-loader.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube4/reduction/browser-loader.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"browser-loader.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube4/reduction/browser-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,KAAK,EAAE,WAAW,CAAC;IACnB,0EAA0E;IAC1E,EAAE,CAAC,EAAE,aAAa,CAAC;IACnB,yEAAyE;IACzE,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAqDD,wBAAgB,oBAAoB,IAAI,oBAAoB,GAAG,SAAS,CAEvE;AAED,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;AAgBD,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAiC/F"}
|
|
@@ -1,17 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser loader for the cube4 reduction tables (the committed .bin files).
|
|
3
|
-
* The manifests are per-phase (`center1-manifest.json`, ...)
|
|
3
|
+
* The manifests are per-phase (`center1-manifest.json`, ...) plus the shared
|
|
4
|
+
* `hc-manifest.json`, which also seals the optional `c3_pair_dist.bin`.
|
|
5
|
+
*
|
|
6
|
+
* Integrity — identical to node-loader.ts: every downloaded .bin is verified
|
|
7
|
+
* against the manifest sha256 + expected byte length (on the DECOMPRESSED
|
|
8
|
+
* payload, so .br/.gz variants and the raw file are checked the same way).
|
|
9
|
+
*
|
|
10
|
+
* Optional tables (`hc` + `c3PairDist`), NEVER silent:
|
|
11
|
+
* - `hc-manifest.json` present → its entries must download and verify, else
|
|
12
|
+
* the whole load REJECTS (node throws too). There is no silent degradation
|
|
13
|
+
* to an unpruned search: it would change the scramble produced for the
|
|
14
|
+
* same seed, breaking node/browser determinism.
|
|
15
|
+
* - `hc-manifest.json` absent (404) → the optional tables were not deployed;
|
|
16
|
+
* `c3_pair_dist.bin` is then probed to tell "not deployed" (both absent →
|
|
17
|
+
* skip, like node) apart from "stale/half-deployed assets" (bin present,
|
|
18
|
+
* manifest missing → REJECT with an actionable message — node refuses the
|
|
19
|
+
* same asset set).
|
|
20
|
+
* There is deliberately no "degraded" flag: the only valid outcomes are
|
|
21
|
+
* "fully loaded tables" or "loud failure", so both platforms always compute
|
|
22
|
+
* the same scrambles.
|
|
23
|
+
*
|
|
24
|
+
* Caching: concurrent loads share ONE in-flight promise (one fetch pass per
|
|
25
|
+
* table). The promise is kept after success; when it rejects it is purged so
|
|
26
|
+
* the next call can retry instead of being stuck on a poisoned cache.
|
|
4
27
|
*/
|
|
28
|
+
import { CENTER3_STATES } from "./center3.js";
|
|
5
29
|
import { edge3DistanceLookup } from "./edge3.js";
|
|
6
30
|
import { fetchTableBuffer } from "../../shared/compressed-fetch.js";
|
|
31
|
+
import { buildHcDense } from "./hc-pdb.js";
|
|
7
32
|
const GENERATED_DIRECTORY = "puzzles/cube4/reduction/generated";
|
|
8
33
|
const CACHE_NAME = "cubingcenter-scrambler-tables-v1";
|
|
34
|
+
const TABLE_MEMORY_BUDGET_BYTES = 150 * 1024 * 1024;
|
|
35
|
+
/**
|
|
36
|
+
* Byte length of `c3_pair_dist.bin`: CENTER3_STATES x 144 nibble entries
|
|
37
|
+
* (index = ct3 * 144 + f * 12 + h, see phase3-search.ts), nibble-packed.
|
|
38
|
+
* Kept in sync with node-loader.ts.
|
|
39
|
+
*/
|
|
40
|
+
const C3_PAIR_DIST_BYTES = (CENTER3_STATES * 144) / 2;
|
|
9
41
|
let cachedTables;
|
|
42
|
+
let cachedTablesPromise;
|
|
43
|
+
function assertCube4ReductionTableBudget(tables) {
|
|
44
|
+
const bytes = tables.center1.distance.byteLength +
|
|
45
|
+
tables.center1.moveMaps.byteLength +
|
|
46
|
+
tables.center2.ctMove.byteLength +
|
|
47
|
+
tables.center2.rlMove.byteLength +
|
|
48
|
+
tables.center2.pruning.byteLength +
|
|
49
|
+
(tables.center2.pruningWide?.byteLength ?? 0) +
|
|
50
|
+
tables.center3.moveTable.byteLength +
|
|
51
|
+
tables.center3.pruning.byteLength +
|
|
52
|
+
tables.edge3.distances.byteLength +
|
|
53
|
+
(tables.hc?.moves.byteLength ?? 0) +
|
|
54
|
+
(tables.hc?.distances.byteLength ?? 0) +
|
|
55
|
+
(tables.hc?.coordinates.byteLength ?? 0) +
|
|
56
|
+
(tables.c3PairDist?.byteLength ?? 0);
|
|
57
|
+
if (bytes > TABLE_MEMORY_BUDGET_BYTES) {
|
|
58
|
+
throw new Error(`Cube4 reduction tables use ${(bytes / (1024 * 1024)).toFixed(1)} MiB, above the 150 MiB table budget.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
10
61
|
export function getCachedCube4Tables() {
|
|
11
62
|
return cachedTables;
|
|
12
63
|
}
|
|
13
64
|
export function clearCachedCube4Tables() {
|
|
14
65
|
cachedTables = undefined;
|
|
66
|
+
cachedTablesPromise = undefined;
|
|
15
67
|
}
|
|
16
68
|
function resolveBaseUrl(provided) {
|
|
17
69
|
if (provided) {
|
|
@@ -26,33 +78,95 @@ function resolveBaseUrl(provided) {
|
|
|
26
78
|
}
|
|
27
79
|
return ".";
|
|
28
80
|
}
|
|
29
|
-
export
|
|
30
|
-
if (
|
|
31
|
-
|
|
81
|
+
export function loadCube4ReductionTablesBrowser(baseUrl) {
|
|
82
|
+
if (cachedTablesPromise) {
|
|
83
|
+
// One shared in-flight promise: concurrent calls trigger exactly one
|
|
84
|
+
// fetch pass (dedup), and the resolved value stays cached afterwards.
|
|
85
|
+
return cachedTablesPromise;
|
|
32
86
|
}
|
|
33
87
|
const resolved = resolveBaseUrl(baseUrl);
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
88
|
+
const promise = (async () => {
|
|
89
|
+
const [center1, center2, center3, edge3, optional] = await Promise.all([
|
|
90
|
+
loadCenter1(resolved),
|
|
91
|
+
loadCenter2(resolved),
|
|
92
|
+
loadCenter3(resolved),
|
|
93
|
+
loadEdge3(resolved),
|
|
94
|
+
loadOptionalTables(resolved)
|
|
95
|
+
]);
|
|
96
|
+
const tables = { center1, center2, center3, edge3 };
|
|
97
|
+
if (optional.hc)
|
|
98
|
+
tables.hc = optional.hc;
|
|
99
|
+
if (optional.c3PairDist)
|
|
100
|
+
tables.c3PairDist = optional.c3PairDist;
|
|
101
|
+
assertCube4ReductionTableBudget(tables);
|
|
102
|
+
return tables;
|
|
103
|
+
})();
|
|
104
|
+
cachedTablesPromise = promise;
|
|
105
|
+
promise.then((value) => {
|
|
106
|
+
cachedTables = value;
|
|
107
|
+
}, () => {
|
|
108
|
+
// A failed load must not poison the cache: purge the rejected promise
|
|
109
|
+
// so a later call retries from scratch.
|
|
110
|
+
if (cachedTablesPromise === promise)
|
|
111
|
+
cachedTablesPromise = undefined;
|
|
112
|
+
});
|
|
113
|
+
return promise;
|
|
114
|
+
}
|
|
115
|
+
function requireEntry(entry, manifestUrl, label) {
|
|
116
|
+
if (!entry || typeof entry.file !== "string" || typeof entry.checksum !== "string") {
|
|
117
|
+
throw new Error(`Invalid table manifest ${manifestUrl}: entry "${label}" must declare { file, checksum } — ` +
|
|
118
|
+
"the deployed assets are stale; sync them with pnpm tables:web:sync.");
|
|
119
|
+
}
|
|
120
|
+
return entry;
|
|
42
121
|
}
|
|
43
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Expected byte length of a table file from its manifest entry.
|
|
124
|
+
* `packing` matches how each table is serialized — keep in sync with
|
|
125
|
+
* node-loader.ts.
|
|
126
|
+
*/
|
|
127
|
+
function expectedBytes(entry, packing) {
|
|
128
|
+
if (typeof entry.bytes === "number")
|
|
129
|
+
return entry.bytes;
|
|
130
|
+
if (typeof entry.range !== "number")
|
|
131
|
+
return undefined;
|
|
132
|
+
if (packing === "byte")
|
|
133
|
+
return entry.range;
|
|
134
|
+
if (packing === "nibble")
|
|
135
|
+
return Math.ceil(entry.range / 2);
|
|
136
|
+
return entry.range * 4;
|
|
137
|
+
}
|
|
138
|
+
async function fetchVerified(baseUrl, entry, packing, expectedLength) {
|
|
139
|
+
const length = expectedLength ?? expectedBytes(entry, packing);
|
|
140
|
+
return fetchTableBuffer(baseUrl, GENERATED_DIRECTORY, entry.file, {
|
|
141
|
+
cacheName: CACHE_NAME,
|
|
142
|
+
compressed: entry.compressed !== false,
|
|
143
|
+
expectedSha256: entry.checksum,
|
|
144
|
+
...(length !== undefined ? { expectedBytes: length } : {})
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async function fetchManifest(baseUrl, file) {
|
|
44
148
|
const url = `${baseUrl}/${GENERATED_DIRECTORY}/${file}`;
|
|
45
149
|
const response = await fetch(url);
|
|
46
150
|
if (!response.ok) {
|
|
47
|
-
throw new Error(`Failed to load ${
|
|
151
|
+
throw new Error(`Failed to load table manifest ${url}: ${response.status} ${response.statusText}`);
|
|
152
|
+
}
|
|
153
|
+
const text = await response.text();
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(text);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
// Always name the URL: a bare JSON.parse error is unactionable.
|
|
159
|
+
throw new Error(`Invalid table manifest JSON: ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
48
160
|
}
|
|
49
|
-
return response.json();
|
|
50
161
|
}
|
|
51
162
|
async function loadCenter1(baseUrl) {
|
|
52
|
-
const
|
|
163
|
+
const manifestUrl = `${baseUrl}/${GENERATED_DIRECTORY}/center1-manifest.json`;
|
|
164
|
+
const manifest = await fetchManifest(baseUrl, "center1-manifest.json");
|
|
165
|
+
const distanceTable = requireEntry(manifest.distanceTable, manifestUrl, "distanceTable");
|
|
166
|
+
const moveMaps = requireEntry(manifest.moveMaps, manifestUrl, "moveMaps");
|
|
53
167
|
const [distanceBuffer, moveMapsBuffer] = await Promise.all([
|
|
54
|
-
|
|
55
|
-
|
|
168
|
+
fetchVerified(baseUrl, distanceTable, "nibble"),
|
|
169
|
+
fetchVerified(baseUrl, moveMaps, "int32")
|
|
56
170
|
]);
|
|
57
171
|
return {
|
|
58
172
|
distance: new Uint8Array(distanceBuffer),
|
|
@@ -60,27 +174,48 @@ async function loadCenter1(baseUrl) {
|
|
|
60
174
|
};
|
|
61
175
|
}
|
|
62
176
|
async function loadCenter2(baseUrl) {
|
|
63
|
-
const
|
|
177
|
+
const manifestUrl = `${baseUrl}/${GENERATED_DIRECTORY}/center2-manifest.json`;
|
|
178
|
+
const manifest = await fetchManifest(baseUrl, "center2-manifest.json");
|
|
179
|
+
const ctMove = requireEntry(manifest.ctMove, manifestUrl, "ctMove");
|
|
180
|
+
const rlMove = requireEntry(manifest.rlMove, manifestUrl, "rlMove");
|
|
181
|
+
const pruningTable = requireEntry(manifest.pruningTable, manifestUrl, "pruningTable");
|
|
182
|
+
const pruningWideEntry = manifest.pruningWideTable
|
|
183
|
+
? requireEntry(manifest.pruningWideTable, manifestUrl, "pruningWideTable")
|
|
184
|
+
: undefined;
|
|
64
185
|
const [ctMoveBuffer, rlMoveBuffer, pruningBuffer] = await Promise.all([
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
186
|
+
fetchVerified(baseUrl, ctMove, "int32"),
|
|
187
|
+
fetchVerified(baseUrl, rlMove, "int32"),
|
|
188
|
+
fetchVerified(baseUrl, pruningTable, "nibble")
|
|
68
189
|
]);
|
|
69
|
-
const
|
|
70
|
-
const
|
|
190
|
+
const ctMoveValues = new Int32Array(ctMoveBuffer);
|
|
191
|
+
const rlMoveValues = new Int32Array(rlMoveBuffer);
|
|
71
192
|
const pruningPacked = new Uint8Array(pruningBuffer);
|
|
72
193
|
const pruning = new Uint8Array(450450);
|
|
73
194
|
for (let i = 0; i < 450450; i++) {
|
|
74
195
|
const byte = pruningPacked[i >> 1];
|
|
75
196
|
pruning[i] = (i & 1) === 0 ? byte & 0x0f : byte >> 4;
|
|
76
197
|
}
|
|
77
|
-
|
|
198
|
+
const tables = { ctMove: ctMoveValues, rlMove: rlMoveValues, pruning };
|
|
199
|
+
if (pruningWideEntry) {
|
|
200
|
+
const wideBuffer = await fetchVerified(baseUrl, pruningWideEntry, "nibble");
|
|
201
|
+
const widePacked = new Uint8Array(wideBuffer);
|
|
202
|
+
const pruningWide = new Uint8Array(450450);
|
|
203
|
+
for (let i = 0; i < 450450; i++) {
|
|
204
|
+
const byte = widePacked[i >> 1];
|
|
205
|
+
pruningWide[i] = (i & 1) === 0 ? byte & 0x0f : byte >> 4;
|
|
206
|
+
}
|
|
207
|
+
tables.pruningWide = pruningWide;
|
|
208
|
+
}
|
|
209
|
+
return tables;
|
|
78
210
|
}
|
|
79
211
|
async function loadCenter3(baseUrl) {
|
|
80
|
-
const
|
|
212
|
+
const manifestUrl = `${baseUrl}/${GENERATED_DIRECTORY}/center3-manifest.json`;
|
|
213
|
+
const manifest = await fetchManifest(baseUrl, "center3-manifest.json");
|
|
214
|
+
const moveTable = requireEntry(manifest.moveTable, manifestUrl, "moveTable");
|
|
215
|
+
const pruningTable = requireEntry(manifest.pruningTable, manifestUrl, "pruningTable");
|
|
81
216
|
const [moveTableBuffer, pruningBuffer] = await Promise.all([
|
|
82
|
-
|
|
83
|
-
|
|
217
|
+
fetchVerified(baseUrl, moveTable, "int32"),
|
|
218
|
+
fetchVerified(baseUrl, pruningTable, "byte")
|
|
84
219
|
]);
|
|
85
220
|
return {
|
|
86
221
|
moveTable: new Int32Array(moveTableBuffer),
|
|
@@ -88,8 +223,68 @@ async function loadCenter3(baseUrl) {
|
|
|
88
223
|
};
|
|
89
224
|
}
|
|
90
225
|
async function loadEdge3(baseUrl) {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const distances =
|
|
94
|
-
|
|
226
|
+
const manifestUrl = `${baseUrl}/${GENERATED_DIRECTORY}/edge3-manifest.json`;
|
|
227
|
+
const manifest = await fetchManifest(baseUrl, "edge3-manifest.json");
|
|
228
|
+
const distances = requireEntry(manifest.distances, manifestUrl, "distances");
|
|
229
|
+
// `range` here already counts packed nibble BYTES (classCount / 2), unlike
|
|
230
|
+
// the other manifests — derive the length from classCount instead.
|
|
231
|
+
const length = typeof manifest.classCount === "number" ? Math.ceil(manifest.classCount / 2) : expectedBytes(distances, "byte");
|
|
232
|
+
const distancesBuffer = await fetchVerified(baseUrl, distances, "nibble", length);
|
|
233
|
+
// Keep nibble-packed (see node-loader.ts): all readers handle both formats.
|
|
234
|
+
const table = new Uint8Array(distancesBuffer);
|
|
235
|
+
return { distances: table, distanceOfEdge: edge3DistanceLookup(table) };
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The two optional PDB tables sealed in `hc-manifest.json`. Semantics mirror
|
|
239
|
+
* node-loader.ts exactly (see the header comment): present → verified or
|
|
240
|
+
* reject; absent → probe the c3 bin so stale deployments fail loudly.
|
|
241
|
+
*/
|
|
242
|
+
async function loadOptionalTables(baseUrl) {
|
|
243
|
+
const manifestUrl = `${baseUrl}/${GENERATED_DIRECTORY}/hc-manifest.json`;
|
|
244
|
+
const response = await fetch(manifestUrl);
|
|
245
|
+
let manifest;
|
|
246
|
+
if (response.ok) {
|
|
247
|
+
const text = await response.text();
|
|
248
|
+
try {
|
|
249
|
+
manifest = JSON.parse(text);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
throw new Error(`Invalid table manifest JSON: ${manifestUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else if (response.status !== 404) {
|
|
256
|
+
throw new Error(`Failed to load table manifest ${manifestUrl}: ${response.status} ${response.statusText}`);
|
|
257
|
+
}
|
|
258
|
+
const result = {};
|
|
259
|
+
if (manifest?.c3PairDist !== undefined) {
|
|
260
|
+
const entry = requireEntry(manifest.c3PairDist, manifestUrl, "c3PairDist");
|
|
261
|
+
if (typeof entry.bytes === "number" && entry.bytes !== C3_PAIR_DIST_BYTES) {
|
|
262
|
+
throw new Error(`Invalid table manifest ${manifestUrl}: c3PairDist.bytes is ${entry.bytes}, expected ` +
|
|
263
|
+
`${C3_PAIR_DIST_BYTES} — sync the assets with pnpm tables:web:sync.`);
|
|
264
|
+
}
|
|
265
|
+
const buffer = await fetchVerified(baseUrl, entry, "nibble", C3_PAIR_DIST_BYTES);
|
|
266
|
+
result.c3PairDist = new Uint8Array(buffer);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Mirror node's fs check: bin present but unsealed = broken deployment.
|
|
270
|
+
const probeUrl = `${baseUrl}/${GENERATED_DIRECTORY}/c3_pair_dist.bin`;
|
|
271
|
+
const probe = await fetch(probeUrl);
|
|
272
|
+
if (probe.ok) {
|
|
273
|
+
throw new Error(`c3_pair_dist.bin is deployed (${probeUrl}) but hc-manifest.json has no "c3PairDist" checksum ` +
|
|
274
|
+
`entry (${manifestUrl}) — refusing to load tables without integrity checks (node fails on the ` +
|
|
275
|
+
"same assets). Sync the assets: pnpm tables:web:sync.");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (manifest) {
|
|
279
|
+
const distances = requireEntry(manifest.distances, manifestUrl, "distances");
|
|
280
|
+
const packed = new Uint8Array(await fetchVerified(baseUrl, distances, "nibble"));
|
|
281
|
+
const dense = buildHcDense(packed);
|
|
282
|
+
if (manifest.moveTable !== undefined) {
|
|
283
|
+
const moveTable = requireEntry(manifest.moveTable, manifestUrl, "moveTable");
|
|
284
|
+
const moveBuffer = await fetchVerified(baseUrl, moveTable, "int32");
|
|
285
|
+
dense.moves = new Int32Array(moveBuffer);
|
|
286
|
+
}
|
|
287
|
+
result.hc = dense;
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
95
290
|
}
|