@cubingcenter/scrambler 0.4.1 → 0.5.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 (65) hide show
  1. package/README.md +21 -0
  2. package/dist/core/api.d.ts.map +1 -1
  3. package/dist/core/api.js +6 -1
  4. package/dist/puzzles/skewb/facelets.d.ts.map +1 -1
  5. package/dist/puzzles/skewb/facelets.js +46 -23
  6. package/dist/puzzles/skewb/index.d.ts +2 -1
  7. package/dist/puzzles/skewb/index.d.ts.map +1 -1
  8. package/dist/puzzles/skewb/index.js +2 -1
  9. package/dist/puzzles/skewb/random-state.d.ts +1 -1
  10. package/dist/puzzles/skewb/random-state.d.ts.map +1 -1
  11. package/dist/puzzles/skewb/random-state.js +25 -18
  12. package/dist/puzzles/skewb/scrambler.d.ts.map +1 -1
  13. package/dist/puzzles/skewb/scrambler.js +10 -13
  14. package/dist/puzzles/skewb/solver/browser-loader.d.ts +20 -0
  15. package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -0
  16. package/dist/puzzles/skewb/solver/browser-loader.js +86 -0
  17. package/dist/puzzles/skewb/solver/generated/centers-table.bin +0 -0
  18. package/dist/puzzles/skewb/solver/generated/helpers.bin +0 -0
  19. package/dist/puzzles/skewb/solver/generated/manifest.json +31 -0
  20. package/dist/puzzles/skewb/solver/generated/orientation-table.bin +0 -0
  21. package/dist/puzzles/skewb/solver/generated/permutation-table.bin +0 -0
  22. package/dist/puzzles/skewb/solver/generated/policy.bin +0 -0
  23. package/dist/puzzles/skewb/solver/generated-assets.d.ts +10 -0
  24. package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -0
  25. package/dist/puzzles/skewb/solver/generated-assets.js +24 -0
  26. package/dist/puzzles/skewb/solver/index.d.ts +5 -0
  27. package/dist/puzzles/skewb/solver/index.d.ts.map +1 -0
  28. package/dist/puzzles/skewb/solver/index.js +4 -0
  29. package/dist/puzzles/skewb/solver/node-loader.d.ts +10 -0
  30. package/dist/puzzles/skewb/solver/node-loader.d.ts.map +1 -0
  31. package/dist/puzzles/skewb/solver/node-loader.js +91 -0
  32. package/dist/puzzles/skewb/solver/search.d.ts +11 -0
  33. package/dist/puzzles/skewb/solver/search.d.ts.map +1 -0
  34. package/dist/puzzles/skewb/solver/search.js +80 -0
  35. package/dist/puzzles/skewb/solver/table-metadata.d.ts +27 -0
  36. package/dist/puzzles/skewb/solver/table-metadata.d.ts.map +1 -0
  37. package/dist/puzzles/skewb/solver/table-metadata.js +26 -0
  38. package/dist/puzzles/skewb/solver/tables.d.ts +68 -0
  39. package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -0
  40. package/dist/puzzles/skewb/solver/tables.js +508 -0
  41. package/dist/random/prng.js +10 -1
  42. package/dist-cjs/core/api.js +6 -1
  43. package/dist-cjs/puzzles/skewb/facelets.js +46 -23
  44. package/dist-cjs/puzzles/skewb/index.js +49 -11
  45. package/dist-cjs/puzzles/skewb/random-state.js +25 -18
  46. package/dist-cjs/puzzles/skewb/scrambler.js +9 -12
  47. package/dist-cjs/puzzles/skewb/solver/browser-loader.js +91 -0
  48. package/dist-cjs/puzzles/skewb/solver/generated/centers-table.bin +0 -0
  49. package/dist-cjs/puzzles/skewb/solver/generated/helpers.bin +0 -0
  50. package/dist-cjs/puzzles/skewb/solver/generated/manifest.json +31 -0
  51. package/dist-cjs/puzzles/skewb/solver/generated/orientation-table.bin +0 -0
  52. package/dist-cjs/puzzles/skewb/solver/generated/permutation-table.bin +0 -0
  53. package/dist-cjs/puzzles/skewb/solver/generated/policy.bin +0 -0
  54. package/dist-cjs/puzzles/skewb/solver/generated-assets.js +60 -0
  55. package/dist-cjs/puzzles/skewb/solver/index.js +23 -0
  56. package/dist-cjs/puzzles/skewb/solver/node-loader.js +97 -0
  57. package/dist-cjs/puzzles/skewb/solver/search.js +87 -0
  58. package/dist-cjs/puzzles/skewb/solver/table-metadata.js +29 -0
  59. package/dist-cjs/puzzles/skewb/solver/tables.js +539 -0
  60. package/dist-cjs/random/prng.js +10 -1
  61. package/package.json +6 -3
  62. package/dist/puzzles/skewb/solver.d.ts +0 -11
  63. package/dist/puzzles/skewb/solver.d.ts.map +0 -1
  64. package/dist/puzzles/skewb/solver.js +0 -149
  65. package/dist-cjs/puzzles/skewb/solver.js +0 -160
@@ -0,0 +1,508 @@
1
+ import { applySkewbMove } from "../moves.js";
2
+ import { loadGeneratedSkewbSolverTables } from "./generated-assets.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Coordinate encoding / decoding (kept from the original solver.ts)
5
+ // ---------------------------------------------------------------------------
6
+ const FACT = [1, 1, 2, 6, 24, 120, 720, 5040, 40320];
7
+ // Popcount lookup (0..255) for the allocation-free Lehmer encoders below.
8
+ const POPCOUNT = new Uint8Array(256);
9
+ for (let i = 1; i < 256; i++)
10
+ POPCOUNT[i] = POPCOUNT[i >> 1] + (i & 1);
11
+ export function getPermutationIndex(perm) {
12
+ // perm length 8, but pos0 is always 0 for WCA reachable states.
13
+ // We encode full 8-perm via Lehmer code (0..40319), but for fixed corner it will be in subset.
14
+ let index = 0;
15
+ let used = 0;
16
+ const n = perm.length;
17
+ for (let i = 0; i < n; i++) {
18
+ const v = perm[i];
19
+ const digit = v - POPCOUNT[used & ((1 << v) - 1)];
20
+ used |= 1 << v;
21
+ index += digit * FACT[n - 1 - i];
22
+ }
23
+ return index;
24
+ }
25
+ export function getPermutationFromIndex(index, n) {
26
+ const perm = Array(n).fill(0);
27
+ const temp = Array.from({ length: n }, (_, i) => i);
28
+ let val = index;
29
+ for (let i = 0; i < n; i++) {
30
+ const f = FACT[n - 1 - i];
31
+ const choice = Math.floor(val / f);
32
+ val %= f;
33
+ perm[i] = temp[choice];
34
+ temp.splice(choice, 1);
35
+ }
36
+ return perm;
37
+ }
38
+ export function getOrientationIndex(orient) {
39
+ // orient length 8, index 0 is fixed 0, encode remaining 7 base3
40
+ let index = 0;
41
+ for (let i = 7; i >= 1; i--) {
42
+ index = index * 3 + orient[i];
43
+ }
44
+ return index;
45
+ }
46
+ export function getOrientationFromIndex(index) {
47
+ const orient = Array(8).fill(0);
48
+ orient[0] = 0;
49
+ let val = index;
50
+ for (let i = 1; i < 8; i++) {
51
+ orient[i] = val % 3;
52
+ val = Math.floor(val / 3);
53
+ }
54
+ return orient;
55
+ }
56
+ export function getCenterIndex(centers) {
57
+ let index = 0;
58
+ let used = 0;
59
+ const n = centers.length;
60
+ for (let i = 0; i < n; i++) {
61
+ const v = centers[i];
62
+ const digit = v - POPCOUNT[used & ((1 << v) - 1)];
63
+ used |= 1 << v;
64
+ index += digit * FACT[n - 1 - i];
65
+ }
66
+ return index;
67
+ }
68
+ export function getCenterFromIndex(index) {
69
+ return getPermutationFromIndex(index, 6);
70
+ }
71
+ export function getStateIndex(state) {
72
+ const p = getPermutationIndex(state.corners.permutation);
73
+ const c = getCenterIndex(state.centers);
74
+ const o = getOrientationIndex(state.corners.orientation);
75
+ // p in 0..40319, c in 0..719, o in 0..2186
76
+ // Combine: ((p * 720 + c) * 2187 + o) -> up to 63B fits in JS number (53-bit)
77
+ return ((p * 720 + c) * 2187 + o);
78
+ }
79
+ export function getStateFromIndex(index) {
80
+ const o = index % 2187;
81
+ let val = Math.floor(index / 2187);
82
+ const c = val % 720;
83
+ const p = Math.floor(val / 720);
84
+ return {
85
+ corners: {
86
+ permutation: getPermutationFromIndex(p, 8),
87
+ orientation: getOrientationFromIndex(o),
88
+ },
89
+ centers: getCenterFromIndex(c),
90
+ };
91
+ }
92
+ // ---------------------------------------------------------------------------
93
+ // Compact encoding over the WCA-reachable group (3,149,280 states)
94
+ // ---------------------------------------------------------------------------
95
+ // With the UFR corner fixed (position 0, orientation 0), the reachable group
96
+ // factors as:
97
+ // rC: even center permutations only -> 360
98
+ // rO: orientations of corners 1..7 -> 2187, partitioned into 9 classes
99
+ // r7: corner permutations of 1..7 -> exactly 4 values per (rC, rO-class)
100
+ // so rank = (rC_even * 2187 + rO) * 4 + k is a bijection over the reachable set.
101
+ export const PERMUTATION_COUNT = 5040; // r7: corners 1..7 Lehmer
102
+ export const ORIENTATION_COUNT = 2187; // rO: corners 1..7 base-3
103
+ export const CENTER_COUNT = 720; // rC: centers Lehmer
104
+ export const EVEN_CENTER_COUNT = 360; // reachable center arrangements
105
+ export const ORIENT_CLASS_COUNT = 9; // orientation classes
106
+ export const PER_SET_SIZE = 4; // corner permutations per (rC, rO-class)
107
+ export const MOVE_COUNT = 8;
108
+ export const STATE_COUNT = EVEN_CENTER_COUNT * ORIENTATION_COUNT * PER_SET_SIZE; // 3_149_280
109
+ export const POLICY_PACKED_ENTRIES = Math.ceil((STATE_COUNT * 3) / 8);
110
+ export const SKEWB_MOVES = ["R", "R'", "U", "U'", "L", "L'", "B", "B'"];
111
+ export function getPerm7Index(perm) {
112
+ // Lehmer rank of the positions 1..7 (values 1..7, offset by -1).
113
+ // Allocation-free: digit = count of unused values below the current one,
114
+ // computed with a used-bitmask and a popcount lookup.
115
+ let index = 0;
116
+ let used = 0;
117
+ for (let i = 1; i < 8; i++) {
118
+ const v = perm[i] - 1;
119
+ const digit = v - POPCOUNT[used & ((1 << v) - 1)];
120
+ used |= 1 << v;
121
+ index += digit * FACT[7 - i];
122
+ }
123
+ return index;
124
+ }
125
+ export function getPerm7FromIndex(index) {
126
+ const perm = Array(8).fill(0);
127
+ const available = [1, 2, 3, 4, 5, 6, 7];
128
+ let val = index;
129
+ for (let i = 1; i < 8; i++) {
130
+ const f = FACT[7 - i];
131
+ const choice = Math.floor(val / f);
132
+ val %= f;
133
+ perm[i] = available[choice];
134
+ available.splice(choice, 1);
135
+ }
136
+ return perm;
137
+ }
138
+ // ---------------------------------------------------------------------------
139
+ // Table loading / runtime state
140
+ // ---------------------------------------------------------------------------
141
+ let tablesLoaded = false;
142
+ export let PERMUTATION_TABLE = null; // r7 * MOVE_COUNT + m
143
+ export let ORIENTATION_TABLE = null; // rO * MOVE_COUNT + m
144
+ export let CENTERS_TABLE = null; // rC * MOVE_COUNT + m
145
+ export let POLICY_TABLE = null; // optimal move per rank, 3-bit packed
146
+ export let EVEN_CENTER_RANK = null; // rC -> compact 0..359 (65535 = odd)
147
+ export let EVEN_CENTER_FULL = null; // compact 0..359 -> rC (derived at load)
148
+ export let ORIENT_CLASS = null; // rO -> class 0..8
149
+ export let R7_SETS = null; // (rC_even * 9 + class) * 4 + i
150
+ export let PERM7_UNRANK = null; // r7 * 7 + i -> piece at corner i+1 (derived at load)
151
+ export let CENTER_UNRANK = null; // rC * 6 + i -> center piece (derived at load)
152
+ export let SOLVED_RANK = 0;
153
+ export async function ensureTablesLoaded() {
154
+ if (tablesLoaded)
155
+ return;
156
+ const tables = await loadGeneratedSkewbSolverTables();
157
+ assignTables(tables);
158
+ }
159
+ export function assignTables(tables) {
160
+ PERMUTATION_TABLE = tables.permutationTable;
161
+ ORIENTATION_TABLE = tables.orientationTable;
162
+ CENTERS_TABLE = tables.centersTable;
163
+ POLICY_TABLE = tables.policyTable;
164
+ EVEN_CENTER_RANK = tables.evenCenterRank;
165
+ ORIENT_CLASS = tables.orientClass;
166
+ R7_SETS = tables.r7Sets;
167
+ EVEN_CENTER_FULL = new Uint16Array(EVEN_CENTER_COUNT);
168
+ for (let rC = 0; rC < CENTER_COUNT; rC++) {
169
+ const compactIdx = tables.evenCenterRank[rC];
170
+ if (compactIdx !== 65535)
171
+ EVEN_CENTER_FULL[compactIdx] = rC;
172
+ }
173
+ PERM7_UNRANK = new Uint8Array(PERMUTATION_COUNT * 7);
174
+ for (let r7 = 0; r7 < PERMUTATION_COUNT; r7++) {
175
+ const perm = getPerm7FromIndex(r7);
176
+ for (let i = 0; i < 7; i++)
177
+ PERM7_UNRANK[r7 * 7 + i] = perm[i + 1];
178
+ }
179
+ CENTER_UNRANK = new Uint8Array(CENTER_COUNT * 6);
180
+ for (let rC = 0; rC < CENTER_COUNT; rC++) {
181
+ const centers = getCenterFromIndex(rC);
182
+ for (let i = 0; i < 6; i++)
183
+ CENTER_UNRANK[rC * 6 + i] = centers[i];
184
+ }
185
+ SOLVED_RANK = rankFromComponents(0, 0, 0);
186
+ tablesLoaded = true;
187
+ }
188
+ export function resetTablesForTests() {
189
+ tablesLoaded = false;
190
+ PERMUTATION_TABLE = null;
191
+ ORIENTATION_TABLE = null;
192
+ CENTERS_TABLE = null;
193
+ POLICY_TABLE = null;
194
+ EVEN_CENTER_RANK = null;
195
+ EVEN_CENTER_FULL = null;
196
+ ORIENT_CLASS = null;
197
+ R7_SETS = null;
198
+ PERM7_UNRANK = null;
199
+ CENTER_UNRANK = null;
200
+ }
201
+ // ---------------------------------------------------------------------------
202
+ // Compact rank helpers
203
+ // ---------------------------------------------------------------------------
204
+ export function rankFromComponents(r7, rO, rC) {
205
+ const evenRank = EVEN_CENTER_RANK[rC];
206
+ if (evenRank === 65535) {
207
+ throw new Error("Unreachable Skewb state: odd center permutation parity.");
208
+ }
209
+ const cls = ORIENT_CLASS[rO];
210
+ const set = R7_SETS;
211
+ const base = (evenRank * ORIENT_CLASS_COUNT + cls) * PER_SET_SIZE;
212
+ for (let i = 0; i < PER_SET_SIZE; i++) {
213
+ if (set[base + i] === r7) {
214
+ return (evenRank * ORIENTATION_COUNT + rO) * PER_SET_SIZE + i;
215
+ }
216
+ }
217
+ throw new Error("Unreachable Skewb state: corner permutation not in reachable set.");
218
+ }
219
+ export function getStateRank(state) {
220
+ if (state.corners.permutation[0] !== 0) {
221
+ throw new Error("The Skewb solver requires the UFR corner (index 0) to be fixed " +
222
+ "as the frame of reference. Please normalize the state before solving.");
223
+ }
224
+ const r7 = getPerm7Index(state.corners.permutation);
225
+ const rO = getOrientationIndex(state.corners.orientation);
226
+ const rC = getCenterIndex(state.centers);
227
+ return rankFromComponents(r7, rO, rC);
228
+ }
229
+ // Inverse of getStateRank: decodes a reachable rank (0..3_149_279) back into
230
+ // a full state. Used for exact uniform random-state sampling.
231
+ export function getStateFromRank(rank) {
232
+ const even = Math.floor(rank / (ORIENTATION_COUNT * PER_SET_SIZE));
233
+ const rest = rank % (ORIENTATION_COUNT * PER_SET_SIZE);
234
+ const rO = Math.floor(rest / PER_SET_SIZE);
235
+ const k = rest % PER_SET_SIZE;
236
+ const cls = ORIENT_CLASS[rO];
237
+ const r7 = R7_SETS[(even * ORIENT_CLASS_COUNT + cls) * PER_SET_SIZE + k];
238
+ const perm = new Array(8);
239
+ perm[0] = 0;
240
+ const permBase = r7 * 7;
241
+ for (let i = 0; i < 7; i++)
242
+ perm[i + 1] = PERM7_UNRANK[permBase + i];
243
+ const orient = new Array(8);
244
+ orient[0] = 0;
245
+ let val = rO;
246
+ for (let i = 1; i < 8; i++) {
247
+ orient[i] = val % 3;
248
+ val = Math.floor(val / 3);
249
+ }
250
+ const centers = new Array(6);
251
+ const centerBase = EVEN_CENTER_FULL[even] * 6;
252
+ for (let i = 0; i < 6; i++)
253
+ centers[i] = CENTER_UNRANK[centerBase + i];
254
+ return { corners: { permutation: perm, orientation: orient }, centers };
255
+ }
256
+ // 3-bit little-endian unpacking of the packed policy table.
257
+ export function getPolicyMoveFromPacked(packed, stateRank) {
258
+ const bits = stateRank * 3;
259
+ const byteIndex = bits >> 3;
260
+ const offset = bits & 7;
261
+ const word = (packed[byteIndex] | ((packed[byteIndex + 1] ?? 0) << 8)) >> offset;
262
+ return word & 0x07;
263
+ }
264
+ export function getPolicyMove(stateRank) {
265
+ if (!POLICY_TABLE) {
266
+ throw new Error("Skewb solver tables not loaded. Call ensureTablesLoaded() first.");
267
+ }
268
+ return getPolicyMoveFromPacked(POLICY_TABLE, stateRank);
269
+ }
270
+ export function getPermutationTransition(permIdx, move) {
271
+ if (!PERMUTATION_TABLE) {
272
+ throw new Error("Skewb solver tables not loaded. Call ensureTablesLoaded() first.");
273
+ }
274
+ return PERMUTATION_TABLE[permIdx * MOVE_COUNT + move];
275
+ }
276
+ export function getOrientationTransition(orientIdx, move) {
277
+ if (!ORIENTATION_TABLE) {
278
+ throw new Error("Skewb solver tables not loaded. Call ensureTablesLoaded() first.");
279
+ }
280
+ return ORIENTATION_TABLE[orientIdx * MOVE_COUNT + move];
281
+ }
282
+ export function getCentersTransition(centerIdx, move) {
283
+ if (!CENTERS_TABLE) {
284
+ throw new Error("Skewb solver tables not loaded. Call ensureTablesLoaded() first.");
285
+ }
286
+ return CENTERS_TABLE[centerIdx * MOVE_COUNT + move];
287
+ }
288
+ // ---------------------------------------------------------------------------
289
+ // Table generation (used by the generate script)
290
+ // ---------------------------------------------------------------------------
291
+ export function generatePermutationTable() {
292
+ const table = new Uint16Array(PERMUTATION_COUNT * MOVE_COUNT);
293
+ for (let r7 = 0; r7 < PERMUTATION_COUNT; r7++) {
294
+ const perm = getPerm7FromIndex(r7);
295
+ for (let m = 0; m < MOVE_COUNT; m++) {
296
+ const state = {
297
+ corners: { permutation: [...perm], orientation: [0, 0, 0, 0, 0, 0, 0, 0] },
298
+ centers: [0, 1, 2, 3, 4, 5],
299
+ };
300
+ const next = applySkewbMove(state, SKEWB_MOVES[m]);
301
+ table[r7 * MOVE_COUNT + m] = getPerm7Index(next.corners.permutation);
302
+ }
303
+ }
304
+ return table;
305
+ }
306
+ export function generateOrientationTable() {
307
+ const table = new Uint16Array(ORIENTATION_COUNT * MOVE_COUNT);
308
+ for (let rO = 0; rO < ORIENTATION_COUNT; rO++) {
309
+ const orient = getOrientationFromIndex(rO);
310
+ for (let m = 0; m < MOVE_COUNT; m++) {
311
+ const state = {
312
+ corners: { permutation: [0, 1, 2, 3, 4, 5, 6, 7], orientation: [...orient] },
313
+ centers: [0, 1, 2, 3, 4, 5],
314
+ };
315
+ const next = applySkewbMove(state, SKEWB_MOVES[m]);
316
+ table[rO * MOVE_COUNT + m] = getOrientationIndex(next.corners.orientation);
317
+ }
318
+ }
319
+ return table;
320
+ }
321
+ export function generateCentersTable() {
322
+ const table = new Uint16Array(CENTER_COUNT * MOVE_COUNT);
323
+ for (let rC = 0; rC < CENTER_COUNT; rC++) {
324
+ const centers = getCenterFromIndex(rC);
325
+ for (let m = 0; m < MOVE_COUNT; m++) {
326
+ const state = {
327
+ corners: { permutation: [0, 1, 2, 3, 4, 5, 6, 7], orientation: [0, 0, 0, 0, 0, 0, 0, 0] },
328
+ centers: [...centers],
329
+ };
330
+ const next = applySkewbMove(state, SKEWB_MOVES[m]);
331
+ table[rC * MOVE_COUNT + m] = getCenterIndex(next.centers);
332
+ }
333
+ }
334
+ return table;
335
+ }
336
+ export function permutationParity(perm) {
337
+ let inversions = 0;
338
+ for (let i = 0; i < perm.length; i++) {
339
+ for (let j = i + 1; j < perm.length; j++) {
340
+ if (perm[i] > perm[j])
341
+ inversions++;
342
+ }
343
+ }
344
+ return inversions % 2;
345
+ }
346
+ // Discovers the reachable-state structure with a BFS over the full product
347
+ // coordinate (r7, rO, rC) and derives the compact-rank helper tables:
348
+ // - even center permutations only (360 of 720)
349
+ // - rO partitioned into 9 classes by the 4-corner-permutation sets
350
+ export function generateHelperTables(permTable, orientTable, centersTable) {
351
+ const evenCenterRank = new Uint16Array(CENTER_COUNT).fill(65535);
352
+ const evenCenterFull = new Uint16Array(EVEN_CENTER_COUNT);
353
+ let compact = 0;
354
+ for (let rC = 0; rC < CENTER_COUNT; rC++) {
355
+ if (permutationParity(getCenterFromIndex(rC)) === 0) {
356
+ evenCenterRank[rC] = compact;
357
+ evenCenterFull[compact] = rC;
358
+ compact++;
359
+ }
360
+ }
361
+ if (compact !== EVEN_CENTER_COUNT) {
362
+ throw new Error(`Expected ${EVEN_CENTER_COUNT} even center permutations, found ${compact}.`);
363
+ }
364
+ const pack = (r7, rO, rC) => (r7 * ORIENTATION_COUNT + rO) * CENTER_COUNT + rC;
365
+ const pairSets = new Map();
366
+ const seen = new Set([0]);
367
+ const queue = [0];
368
+ let head = 0;
369
+ while (head < queue.length) {
370
+ const cur = queue[head++];
371
+ const rC = cur % CENTER_COUNT;
372
+ const rest = Math.floor(cur / CENTER_COUNT);
373
+ const rO = rest % ORIENTATION_COUNT;
374
+ const r7 = Math.floor(rest / ORIENTATION_COUNT);
375
+ const pairKey = rC * ORIENTATION_COUNT + rO;
376
+ let set = pairSets.get(pairKey);
377
+ if (!set) {
378
+ set = new Set();
379
+ pairSets.set(pairKey, set);
380
+ }
381
+ set.add(r7);
382
+ for (let m = 0; m < MOVE_COUNT; m++) {
383
+ const n7 = permTable[r7 * MOVE_COUNT + m];
384
+ const nO = orientTable[rO * MOVE_COUNT + m];
385
+ const nC = centersTable[rC * MOVE_COUNT + m];
386
+ const next = pack(n7, nO, nC);
387
+ if (!seen.has(next)) {
388
+ seen.add(next);
389
+ queue.push(next);
390
+ }
391
+ }
392
+ }
393
+ if (seen.size !== STATE_COUNT) {
394
+ throw new Error(`Reachability BFS covered ${seen.size} states, expected ${STATE_COUNT}.`);
395
+ }
396
+ const setKey = (values) => [...values].sort((a, b) => a - b).join(",");
397
+ // Reference partition from rC = 0 (verified identical for all even rC).
398
+ const orientClass = new Uint8Array(ORIENTATION_COUNT);
399
+ const classOfSet = new Map();
400
+ for (let rO = 0; rO < ORIENTATION_COUNT; rO++) {
401
+ const set = pairSets.get(rO);
402
+ const key = setKey(set);
403
+ if (!classOfSet.has(key)) {
404
+ classOfSet.set(key, classOfSet.size);
405
+ }
406
+ orientClass[rO] = classOfSet.get(key);
407
+ }
408
+ if (classOfSet.size !== ORIENT_CLASS_COUNT) {
409
+ throw new Error(`Expected ${ORIENT_CLASS_COUNT} orientation classes, found ${classOfSet.size}.`);
410
+ }
411
+ // Verify the partition is identical for every even rC and that each
412
+ // (rC, class) has exactly PER_SET_SIZE corner permutations.
413
+ const r7Sets = new Uint16Array(EVEN_CENTER_COUNT * ORIENT_CLASS_COUNT * PER_SET_SIZE);
414
+ for (let rC = 0; rC < CENTER_COUNT; rC++) {
415
+ const rCCompact = evenCenterRank[rC];
416
+ if (rCCompact === 65535)
417
+ continue;
418
+ for (let rO = 0; rO < ORIENTATION_COUNT; rO++) {
419
+ const set = pairSets.get(rC * ORIENTATION_COUNT + rO);
420
+ if (set.size !== PER_SET_SIZE) {
421
+ throw new Error(`Expected ${PER_SET_SIZE} permutations for (rC=${rC}, rO=${rO}), found ${set.size}.`);
422
+ }
423
+ const cls = orientClass[rO];
424
+ if (classOfSet.get(setKey(set)) !== cls) {
425
+ throw new Error(`Orientation class partition differs at rC=${rC}, rO=${rO}.`);
426
+ }
427
+ const stored = [...set].sort((a, b) => a - b);
428
+ const base = (rCCompact * ORIENT_CLASS_COUNT + cls) * PER_SET_SIZE;
429
+ for (let i = 0; i < PER_SET_SIZE; i++)
430
+ r7Sets[base + i] = stored[i];
431
+ }
432
+ }
433
+ return { evenCenterRank, orientClass, r7Sets, evenCenterFull };
434
+ }
435
+ // BFS from solved stores, for each state, the inverse of the move that first
436
+ // discovered it: an optimal move toward solved. Packed 3 bits per state
437
+ // (8 moves; the solved entry is never read, the solve loop exits first).
438
+ export function generatePolicyTable(permTable, orientTable, centersTable, helpers) {
439
+ // Install the compact-rank tables for the duration of the build.
440
+ const saved = { evenCenterRank: EVEN_CENTER_RANK, orientClass: ORIENT_CLASS, r7Sets: R7_SETS };
441
+ EVEN_CENTER_RANK = helpers.evenCenterRank;
442
+ ORIENT_CLASS = helpers.orientClass;
443
+ R7_SETS = helpers.r7Sets;
444
+ try {
445
+ const solvedRank = rankFromComponents(0, 0, 0);
446
+ const distances = new Uint8Array(STATE_COUNT).fill(255);
447
+ const policy = new Uint8Array(STATE_COUNT);
448
+ distances[solvedRank] = 0;
449
+ const queue = new Uint32Array(STATE_COUNT);
450
+ let read = 0;
451
+ let write = 1;
452
+ queue[0] = solvedRank;
453
+ let maxDepth = 0;
454
+ while (read < write) {
455
+ const curr = queue[read++];
456
+ const dist = distances[curr];
457
+ const even = Math.floor(curr / (ORIENTATION_COUNT * PER_SET_SIZE));
458
+ const rest = curr % (ORIENTATION_COUNT * PER_SET_SIZE);
459
+ const rO = Math.floor(rest / PER_SET_SIZE);
460
+ const k = rest % PER_SET_SIZE;
461
+ const cls = ORIENT_CLASS[rO];
462
+ const r7 = R7_SETS[(even * ORIENT_CLASS_COUNT + cls) * PER_SET_SIZE + k];
463
+ const rC = helpers.evenCenterFull[even];
464
+ const nextDist = dist + 1;
465
+ for (let m = 0; m < MOVE_COUNT; m++) {
466
+ const n7 = permTable[r7 * MOVE_COUNT + m];
467
+ const nO = orientTable[rO * MOVE_COUNT + m];
468
+ const nC = centersTable[rC * MOVE_COUNT + m];
469
+ const next = rankFromComponents(n7, nO, nC);
470
+ if (distances[next] === 255) {
471
+ distances[next] = nextDist;
472
+ policy[next] = m ^ 1; // inverse move of the discovery edge
473
+ if (nextDist > maxDepth)
474
+ maxDepth = nextDist;
475
+ queue[write++] = next;
476
+ }
477
+ }
478
+ }
479
+ if (write !== STATE_COUNT) {
480
+ throw new Error(`Policy BFS covered ${write} states, expected ${STATE_COUNT}.`);
481
+ }
482
+ const packed = new Uint8Array(POLICY_PACKED_ENTRIES);
483
+ for (let i = 0; i < STATE_COUNT; i++) {
484
+ const bits = i * 3;
485
+ const byteIndex = bits >> 3;
486
+ const offset = bits & 7;
487
+ const value = policy[i] & 0x07;
488
+ packed[byteIndex] |= (value << offset) & 0xff;
489
+ if (offset > 5) {
490
+ packed[byteIndex + 1] |= value >> (8 - offset);
491
+ }
492
+ }
493
+ return { packed, maxDepth };
494
+ }
495
+ finally {
496
+ EVEN_CENTER_RANK = saved.evenCenterRank;
497
+ ORIENT_CLASS = saved.orientClass;
498
+ R7_SETS = saved.r7Sets;
499
+ }
500
+ }
501
+ export function checksum(values) {
502
+ let hash = 0x811c9dc5;
503
+ for (let i = 0; i < values.length; i++) {
504
+ hash ^= values[i];
505
+ hash = Math.imul(hash, 0x01000193);
506
+ }
507
+ return hash >>> 0;
508
+ }
@@ -6,8 +6,17 @@ export function createRandomSource(seed) {
6
6
  return new SeededRandomSource(hashSeed(seed));
7
7
  }
8
8
  class CryptoRandomSource {
9
+ buffer = null;
10
+ offset = 0;
9
11
  next() {
10
- const value = randomBytes(6).readUIntBE(0, 6);
12
+ // Batch the crypto calls: one randomBytes per move is ~2 orders of
13
+ // magnitude slower in Node (syscall per call). Refill lazily.
14
+ if (this.buffer === null || this.offset + 6 > 256) {
15
+ this.buffer = randomBytes(256);
16
+ this.offset = 0;
17
+ }
18
+ const value = this.buffer.readUIntBE(this.offset, 6);
19
+ this.offset += 6;
11
20
  return value / 0x1000000000000;
12
21
  }
13
22
  nextInt(maxExclusive) {
@@ -71,11 +71,16 @@ async function initializeScrambler(baseUrl, eventId) {
71
71
  const { loadBrowserTables } = await Promise.resolve().then(() => __importStar(require("../puzzles/pyraminx/solver/browser-loader.js")));
72
72
  await loadBrowserTables(baseUrl);
73
73
  }
74
+ else if (eventId === "skwb") {
75
+ const { loadBrowserTables } = await Promise.resolve().then(() => __importStar(require("../puzzles/skewb/solver/browser-loader.js")));
76
+ await loadBrowserTables(baseUrl);
77
+ }
74
78
  else if (!eventId) {
75
79
  await Promise.all([
76
80
  Promise.resolve().then(() => __importStar(require("../puzzles/cube2/solver/browser-loader.js"))).then((m) => m.loadBrowserTables(baseUrl)),
77
81
  Promise.resolve().then(() => __importStar(require("../puzzles/cube3/solver/browser-loader.js"))).then((m) => m.loadBrowserTables(baseUrl)),
78
- Promise.resolve().then(() => __importStar(require("../puzzles/pyraminx/solver/browser-loader.js"))).then((m) => m.loadBrowserTables(baseUrl))
82
+ Promise.resolve().then(() => __importStar(require("../puzzles/pyraminx/solver/browser-loader.js"))).then((m) => m.loadBrowserTables(baseUrl)),
83
+ Promise.resolve().then(() => __importStar(require("../puzzles/skewb/solver/browser-loader.js"))).then((m) => m.loadBrowserTables(baseUrl))
79
84
  ]);
80
85
  }
81
86
  }
@@ -7,6 +7,7 @@ exports.getSkewbFaceOrder = getSkewbFaceOrder;
7
7
  // Facelets model matching cubingcenter-viz/src/puzzles/skewb.ts
8
8
  // Each face has 5 stickers: 0=center, 1=TL,2=TR,3=BL,4=BR (net layout)
9
9
  const FACE_ORDER = ["U", "R", "F", "D", "L", "B"];
10
+ const FACE_INDEX = { U: 0, R: 1, F: 2, D: 3, L: 4, B: 5 };
10
11
  // Canonical sticker faces per corner position: [UD sticker, RL sticker, FB sticker]
11
12
  const CORNER_STICKERS = [
12
13
  ["U", "R", "F"],
@@ -29,43 +30,65 @@ const CORNER_SLOTS = [
29
30
  [["D", 4], ["R", 4], ["B", 3]],
30
31
  [["D", 3], ["L", 3], ["B", 4]],
31
32
  ];
33
+ // For each (position, piece, orientation) the three facelet writes, flattened:
34
+ // face index, sticker slot index and sticker color index, so the hot loop is
35
+ // pure table lookups (no tuple destructuring, no modulo).
36
+ const CORNER_WRITE_COUNT = 8 * 8 * 3 * 3;
37
+ const WRITE_FACE = new Int8Array(CORNER_WRITE_COUNT);
38
+ const WRITE_SLOT = new Int8Array(CORNER_WRITE_COUNT);
39
+ const WRITE_COLOR = new Int8Array(CORNER_WRITE_COUNT);
40
+ for (let pos = 0; pos < 8; pos++) {
41
+ const slots = CORNER_SLOTS[pos];
42
+ for (let piece = 0; piece < 8; piece++) {
43
+ const stickers = CORNER_STICKERS[piece];
44
+ for (let ori = 0; ori < 3; ori++) {
45
+ const base = ((pos * 8 + piece) * 3 + ori) * 3;
46
+ for (let i = 0; i < 3; i++) {
47
+ const [face, idx] = slots[i];
48
+ const color = stickers[(i + 3 - ori) % 3];
49
+ WRITE_FACE[base + i] = FACE_INDEX[face];
50
+ WRITE_SLOT[base + i] = idx;
51
+ WRITE_COLOR[base + i] = FACE_INDEX[color];
52
+ }
53
+ }
54
+ }
55
+ }
32
56
  function createSolvedSkewbFacelets() {
33
57
  return {
34
- U: Array.from({ length: 5 }, () => "U"),
35
- R: Array.from({ length: 5 }, () => "R"),
36
- F: Array.from({ length: 5 }, () => "F"),
37
- D: Array.from({ length: 5 }, () => "D"),
38
- L: Array.from({ length: 5 }, () => "L"),
39
- B: Array.from({ length: 5 }, () => "B"),
58
+ U: ["U", "U", "U", "U", "U"],
59
+ R: ["R", "R", "R", "R", "R"],
60
+ F: ["F", "F", "F", "F", "F"],
61
+ D: ["D", "D", "D", "D", "D"],
62
+ L: ["L", "L", "L", "L", "L"],
63
+ B: ["B", "B", "B", "B", "B"],
40
64
  };
41
65
  }
42
66
  function createEmptySkewbFacelets() {
43
67
  return {
44
- U: Array(5).fill("?"),
45
- R: Array(5).fill("?"),
46
- F: Array(5).fill("?"),
47
- D: Array(5).fill("?"),
48
- L: Array(5).fill("?"),
49
- B: Array(5).fill("?"),
68
+ U: ["?", "?", "?", "?", "?"],
69
+ R: ["?", "?", "?", "?", "?"],
70
+ F: ["?", "?", "?", "?", "?"],
71
+ D: ["?", "?", "?", "?", "?"],
72
+ L: ["?", "?", "?", "?", "?"],
73
+ B: ["?", "?", "?", "?", "?"],
50
74
  };
51
75
  }
52
76
  function toSkewbFacelets(state) {
53
77
  const facelets = createEmptySkewbFacelets();
78
+ const faces = [facelets.U, facelets.R, facelets.F, facelets.D, facelets.L, facelets.B];
79
+ const perm = state.corners.permutation;
80
+ const orient = state.corners.orientation;
81
+ const centers = state.centers;
54
82
  // centers
55
83
  for (let face = 0; face < 6; face++) {
56
- const targetFace = FACE_ORDER[face];
57
- facelets[targetFace][0] = FACE_ORDER[state.centers[face]];
84
+ faces[face][0] = FACE_ORDER[centers[face]];
58
85
  }
59
- // corners
86
+ // corners (unrolled 3 writes per position)
60
87
  for (let pos = 0; pos < 8; pos++) {
61
- const piece = state.corners.permutation[pos];
62
- const ori = state.corners.orientation[pos];
63
- const slots = CORNER_SLOTS[pos];
64
- const stickers = CORNER_STICKERS[piece];
65
- for (let i = 0; i < 3; i++) {
66
- const [face, idx] = slots[i];
67
- facelets[face][idx] = stickers[(i + 3 - ori) % 3];
68
- }
88
+ const base = ((pos * 8 + perm[pos]) * 3 + orient[pos]) * 3;
89
+ faces[WRITE_FACE[base]][WRITE_SLOT[base]] = FACE_ORDER[WRITE_COLOR[base]];
90
+ faces[WRITE_FACE[base + 1]][WRITE_SLOT[base + 1]] = FACE_ORDER[WRITE_COLOR[base + 1]];
91
+ faces[WRITE_FACE[base + 2]][WRITE_SLOT[base + 2]] = FACE_ORDER[WRITE_COLOR[base + 2]];
69
92
  }
70
93
  return facelets;
71
94
  }