@cubingcenter/scrambler 0.4.1 → 0.5.1

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 (83) 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/pyraminx/facelets.d.ts.map +1 -1
  5. package/dist/puzzles/pyraminx/facelets.js +43 -17
  6. package/dist/puzzles/pyraminx/random-state.d.ts.map +1 -1
  7. package/dist/puzzles/pyraminx/random-state.js +63 -29
  8. package/dist/puzzles/pyraminx/scrambler.d.ts.map +1 -1
  9. package/dist/puzzles/pyraminx/scrambler.js +35 -26
  10. package/dist/puzzles/pyraminx/solver/search.d.ts +3 -0
  11. package/dist/puzzles/pyraminx/solver/search.d.ts.map +1 -1
  12. package/dist/puzzles/pyraminx/solver/search.js +60 -5
  13. package/dist/puzzles/pyraminx/solver/tables.d.ts +1 -0
  14. package/dist/puzzles/pyraminx/solver/tables.d.ts.map +1 -1
  15. package/dist/puzzles/pyraminx/solver/tables.js +3 -0
  16. package/dist/puzzles/skewb/facelets.d.ts.map +1 -1
  17. package/dist/puzzles/skewb/facelets.js +46 -23
  18. package/dist/puzzles/skewb/index.d.ts +2 -1
  19. package/dist/puzzles/skewb/index.d.ts.map +1 -1
  20. package/dist/puzzles/skewb/index.js +2 -1
  21. package/dist/puzzles/skewb/random-state.d.ts +1 -1
  22. package/dist/puzzles/skewb/random-state.d.ts.map +1 -1
  23. package/dist/puzzles/skewb/random-state.js +25 -18
  24. package/dist/puzzles/skewb/scrambler.d.ts.map +1 -1
  25. package/dist/puzzles/skewb/scrambler.js +27 -16
  26. package/dist/puzzles/skewb/solver/browser-loader.d.ts +20 -0
  27. package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -0
  28. package/dist/puzzles/skewb/solver/browser-loader.js +86 -0
  29. package/dist/puzzles/skewb/solver/generated/centers-table.bin +0 -0
  30. package/dist/puzzles/skewb/solver/generated/helpers.bin +0 -0
  31. package/dist/puzzles/skewb/solver/generated/manifest.json +31 -0
  32. package/dist/puzzles/skewb/solver/generated/orientation-table.bin +0 -0
  33. package/dist/puzzles/skewb/solver/generated/permutation-table.bin +0 -0
  34. package/dist/puzzles/skewb/solver/generated/policy.bin +0 -0
  35. package/dist/puzzles/skewb/solver/generated-assets.d.ts +10 -0
  36. package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -0
  37. package/dist/puzzles/skewb/solver/generated-assets.js +24 -0
  38. package/dist/puzzles/skewb/solver/index.d.ts +5 -0
  39. package/dist/puzzles/skewb/solver/index.d.ts.map +1 -0
  40. package/dist/puzzles/skewb/solver/index.js +4 -0
  41. package/dist/puzzles/skewb/solver/node-loader.d.ts +10 -0
  42. package/dist/puzzles/skewb/solver/node-loader.d.ts.map +1 -0
  43. package/dist/puzzles/skewb/solver/node-loader.js +91 -0
  44. package/dist/puzzles/skewb/solver/search.d.ts +14 -0
  45. package/dist/puzzles/skewb/solver/search.d.ts.map +1 -0
  46. package/dist/puzzles/skewb/solver/search.js +120 -0
  47. package/dist/puzzles/skewb/solver/table-metadata.d.ts +27 -0
  48. package/dist/puzzles/skewb/solver/table-metadata.d.ts.map +1 -0
  49. package/dist/puzzles/skewb/solver/table-metadata.js +26 -0
  50. package/dist/puzzles/skewb/solver/tables.d.ts +69 -0
  51. package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -0
  52. package/dist/puzzles/skewb/solver/tables.js +511 -0
  53. package/dist/random/prng.d.ts.map +1 -1
  54. package/dist/random/prng.js +25 -2
  55. package/dist-cjs/core/api.js +6 -1
  56. package/dist-cjs/puzzles/pyraminx/facelets.js +43 -17
  57. package/dist-cjs/puzzles/pyraminx/random-state.js +63 -29
  58. package/dist-cjs/puzzles/pyraminx/scrambler.js +34 -25
  59. package/dist-cjs/puzzles/pyraminx/solver/search.js +62 -4
  60. package/dist-cjs/puzzles/pyraminx/solver/tables.js +4 -0
  61. package/dist-cjs/puzzles/skewb/facelets.js +46 -23
  62. package/dist-cjs/puzzles/skewb/index.js +49 -11
  63. package/dist-cjs/puzzles/skewb/random-state.js +25 -18
  64. package/dist-cjs/puzzles/skewb/scrambler.js +26 -15
  65. package/dist-cjs/puzzles/skewb/solver/browser-loader.js +91 -0
  66. package/dist-cjs/puzzles/skewb/solver/generated/centers-table.bin +0 -0
  67. package/dist-cjs/puzzles/skewb/solver/generated/helpers.bin +0 -0
  68. package/dist-cjs/puzzles/skewb/solver/generated/manifest.json +31 -0
  69. package/dist-cjs/puzzles/skewb/solver/generated/orientation-table.bin +0 -0
  70. package/dist-cjs/puzzles/skewb/solver/generated/permutation-table.bin +0 -0
  71. package/dist-cjs/puzzles/skewb/solver/generated/policy.bin +0 -0
  72. package/dist-cjs/puzzles/skewb/solver/generated-assets.js +60 -0
  73. package/dist-cjs/puzzles/skewb/solver/index.js +23 -0
  74. package/dist-cjs/puzzles/skewb/solver/node-loader.js +97 -0
  75. package/dist-cjs/puzzles/skewb/solver/search.js +130 -0
  76. package/dist-cjs/puzzles/skewb/solver/table-metadata.js +29 -0
  77. package/dist-cjs/puzzles/skewb/solver/tables.js +543 -0
  78. package/dist-cjs/random/prng.js +25 -2
  79. package/package.json +6 -3
  80. package/dist/puzzles/skewb/solver.d.ts +0 -11
  81. package/dist/puzzles/skewb/solver.d.ts.map +0 -1
  82. package/dist/puzzles/skewb/solver.js +0 -149
  83. package/dist-cjs/puzzles/skewb/solver.js +0 -160
package/README.md CHANGED
@@ -15,6 +15,7 @@ The package currently ships random-state scramblers for `222` (2x2x2), `333` (3x
15
15
  - 2x2 quality: random-state target + optimal table-guided solver + verified scramble inversion
16
16
  - 3x3 quality: random-state target + two-phase solver + verified scramble inversion
17
17
  - Pyraminx quality: random-state target + optimal policy-table solver (all 933,120 core states verified)
18
+ - Skewb quality: random-state target + optimal policy-table solver (all 3,149,280 states; exact uniform sampling)
18
19
  - 4x4 quality: reduced 4x4 random-state target + existing 3x3 two-phase finish; full center/wing random-state reduction is isolated under `cube4.reduction`
19
20
 
20
21
  This library is intended for practice, training, and internal CubingCenter use. Do not present it as an official WCA competition scrambler; sanctioned competitions must use the official program required by the delegate/organization.
@@ -141,6 +142,26 @@ const solution = await pyraminx.solvePyraminx(state); // optimal, policy-table b
141
142
 
142
143
  The solver only covers the core (centers + edges); tips are appended by the scrambler. The `pyraminx.solver` namespace exposes the compact coordinates and the policy table; its optimality is verified exhaustively over all 933,120 core states (`pnpm verify:pyraminx:exhaustive`). Normal consumers should prefer `generateScramble("pymx")`.
143
144
 
145
+ ## Skewb Technical Module
146
+
147
+ ```ts
148
+ import { skewb } from "@cubingcenter/scrambler";
149
+
150
+ const solved = skewb.createSolvedSkewbState();
151
+ const moved = skewb.applySkewbAlgorithm(solved, ["R", "U", "R'"]);
152
+ const state = await skewb.generateRandomSkewbState({ seed: "demo" });
153
+ const facelets = skewb.toSkewbFacelets(state);
154
+ const faceletString = skewb.formatSkewbFacelets(facelets);
155
+ const validation = skewb.validateSkewbState(state);
156
+ const solution = await skewb.solveSkewb(state); // optimal, policy-table based
157
+ ```
158
+
159
+ `generateRandomSkewbState` returns a legal random state, not a scramble sequence. It samples the compact group rank uniformly, so every one of the 3,149,280 reachable states is exactly as likely as any other.
160
+
161
+ `toSkewbFacelets` returns six faces with five stickers each. `formatSkewbFacelets` produces a stable 30-character string.
162
+
163
+ Both `generateRandomSkewbState` and `solveSkewb` are async: they need the solver tables, which load lazily in Node and are prefetched in the browser via `initializeScrambler(baseUrl, "skwb")` (or `skewb.solveSkewbSync` after preloading). The `skewb.solver` namespace exposes the compact rank coordinates and the 3-bit policy table (all 3,149,280 states, God's number 11). Normal consumers should prefer `generateScramble("skwb")`.
164
+
144
165
  ## 4x4 Technical Module
145
166
 
146
167
  ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/core/api.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBrG;AAED,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,iBAAmC,GAC5C,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,GAAE,iBAAmC,GAAG,SAAS,EAAE,CAE7F;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,OAAO,EAChB,QAAQ,GAAE,iBAAmC,GAC5C,SAAS,CAQX;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,eAAoB,EAC7B,QAAQ,GAAE,iBAAmC,GAC5C,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,eAAoB,EAC7B,QAAQ,GAAE,iBAAmC,GAC5C,OAAO,CAAC,cAAc,EAAE,CAAC,CAU3B"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/core/api.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BrG;AAED,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,iBAAmC,GAC5C,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,GAAE,iBAAmC,GAAG,SAAS,EAAE,CAE7F;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,OAAO,EAChB,QAAQ,GAAE,iBAAmC,GAC5C,SAAS,CAQX;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,eAAoB,EAC7B,QAAQ,GAAE,iBAAmC,GAC5C,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,eAAoB,EAC7B,QAAQ,GAAE,iBAAmC,GAC5C,OAAO,CAAC,cAAc,EAAE,CAAC,CAU3B"}
package/dist/core/api.js CHANGED
@@ -30,11 +30,16 @@ export async function initializeScrambler(baseUrl, eventId) {
30
30
  const { loadBrowserTables } = await import("../puzzles/pyraminx/solver/browser-loader.js");
31
31
  await loadBrowserTables(baseUrl);
32
32
  }
33
+ else if (eventId === "skwb") {
34
+ const { loadBrowserTables } = await import("../puzzles/skewb/solver/browser-loader.js");
35
+ await loadBrowserTables(baseUrl);
36
+ }
33
37
  else if (!eventId) {
34
38
  await Promise.all([
35
39
  import("../puzzles/cube2/solver/browser-loader.js").then((m) => m.loadBrowserTables(baseUrl)),
36
40
  import("../puzzles/cube3/solver/browser-loader.js").then((m) => m.loadBrowserTables(baseUrl)),
37
- import("../puzzles/pyraminx/solver/browser-loader.js").then((m) => m.loadBrowserTables(baseUrl))
41
+ import("../puzzles/pyraminx/solver/browser-loader.js").then((m) => m.loadBrowserTables(baseUrl)),
42
+ import("../puzzles/skewb/solver/browser-loader.js").then((m) => m.loadBrowserTables(baseUrl))
38
43
  ]);
39
44
  }
40
45
  }
@@ -1 +1 @@
1
- {"version":3,"file":"facelets.d.ts","sourceRoot":"","sources":["../../../src/puzzles/pyraminx/facelets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUhD,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;AA6DhF,wBAAgB,4BAA4B,IAAI,gBAAgB,CAO/D;AAyBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,CAiCzE;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAEzE;AAED,wBAAgB,oBAAoB,IAAI,SAAS,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE,CAEzE"}
1
+ {"version":3,"file":"facelets.d.ts","sourceRoot":"","sources":["../../../src/puzzles/pyraminx/facelets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUhD,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;AAsGhF,wBAAgB,4BAA4B,IAAI,gBAAgB,CAO/D;AAoBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,CAezE;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAEzE;AAED,wBAAgB,oBAAoB,IAAI,SAAS,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE,CAEzE"}
@@ -1,4 +1,6 @@
1
1
  const FACE_ORDER = ["F", "L", "R", "D"];
2
+ const COLORS = ["F", "L", "R", "D"];
3
+ const COLOR_INDEX = { F: 0, L: 1, R: 2, D: 3 };
2
4
  // positionId -> sticker slots (each slot = one triangle cell of the grid).
3
5
  // Positions follow the scrambler piece model (moves.ts):
4
6
  // edges 0..5 = UL, UR, UB, LR, LB, RB
@@ -48,6 +50,33 @@ const TIP_PIECE_STICKERS = [
48
50
  ["F", "R", "D"], // 2 R
49
51
  ["L", "D", "R"], // 3 B
50
52
  ];
53
+ const EDGE_CELLS = buildCells(EDGE_POSITION_SLOTS, EDGE_PIECE_STICKERS, 6, 2);
54
+ const CENTER_CELLS = buildCells(CENTER_POSITION_SLOTS, CENTER_PIECE_STICKERS, 4, 3);
55
+ const TIP_CELLS = buildCells(TIP_POSITION_SLOTS, TIP_PIECE_STICKERS, 4, 3);
56
+ function buildCells(slots, stickersTable, pieceCount, orientationCount) {
57
+ const cells = [];
58
+ for (let position = 0; position < slots.length; position += 1) {
59
+ const byPiece = [];
60
+ for (let piece = 0; piece < pieceCount; piece += 1) {
61
+ const byOrientation = [];
62
+ for (let orientation = 0; orientation < orientationCount; orientation += 1) {
63
+ const stickers = stickersTable[piece];
64
+ const slotList = slots[position];
65
+ const cell = [];
66
+ for (let index = 0; index < slotList.length; index += 1) {
67
+ const [face, stickerIndex] = slotList[index];
68
+ cell.push(FACE_ORDER.indexOf(face));
69
+ cell.push(stickerIndex);
70
+ cell.push(COLOR_INDEX[stickers[(index - orientation + stickers.length) % stickers.length]]);
71
+ }
72
+ byOrientation.push(cell);
73
+ }
74
+ byPiece.push(byOrientation);
75
+ }
76
+ cells.push(byPiece);
77
+ }
78
+ return cells;
79
+ }
51
80
  export function createSolvedPyraminxFacelets() {
52
81
  return {
53
82
  F: Array.from({ length: 9 }, () => "F"),
@@ -56,36 +85,33 @@ export function createSolvedPyraminxFacelets() {
56
85
  D: Array.from({ length: 9 }, () => "D"),
57
86
  };
58
87
  }
88
+ // Sparse arrays: every one of the 36 sticker cells is overwritten by the
89
+ // three placement loops below (12 edges + 12 centers + 12 tips), so no fill
90
+ // is needed. Coverage is enforced by test/pyraminx-facelets.test.ts.
59
91
  function createEmptyPyraminxFacelets() {
60
92
  return {
61
- F: Array(9).fill("?"),
62
- L: Array(9).fill("?"),
63
- R: Array(9).fill("?"),
64
- D: Array(9).fill("?"),
93
+ F: Array(9),
94
+ L: Array(9),
95
+ R: Array(9),
96
+ D: Array(9),
65
97
  };
66
98
  }
67
- function placeOrientedStickers(facelets, slots, stickers, orientation) {
68
- for (let index = 0; index < slots.length; index += 1) {
69
- const [face, stickerIndex] = slots[index];
70
- const sticker = stickers[(index - orientation + stickers.length) % stickers.length];
71
- const arr = facelets[face];
72
- arr[stickerIndex] = sticker;
99
+ function placeCells(faces, cells) {
100
+ for (let cellIndex = 0; cellIndex < cells.length; cellIndex += 3) {
101
+ faces[cells[cellIndex]][cells[cellIndex + 1]] = COLORS[cells[cellIndex + 2]];
73
102
  }
74
103
  }
75
104
  export function toPyraminxFacelets(state) {
76
105
  const facelets = createEmptyPyraminxFacelets();
106
+ const faces = [facelets.F, facelets.L, facelets.R, facelets.D];
77
107
  for (let position = 0; position < 6; position += 1) {
78
- const piece = state.edgesPerm[position];
79
- const orientation = state.edgesOrient[position];
80
- placeOrientedStickers(facelets, EDGE_POSITION_SLOTS[position], EDGE_PIECE_STICKERS[piece], orientation);
108
+ placeCells(faces, EDGE_CELLS[position][state.edgesPerm[position]][state.edgesOrient[position]]);
81
109
  }
82
110
  for (let position = 0; position < 4; position += 1) {
83
- const orientation = state.centers[position];
84
- placeOrientedStickers(facelets, CENTER_POSITION_SLOTS[position], CENTER_PIECE_STICKERS[position], orientation);
111
+ placeCells(faces, CENTER_CELLS[position][position][state.centers[position]]);
85
112
  }
86
113
  for (let position = 0; position < 4; position += 1) {
87
- const orientation = state.tips[position];
88
- placeOrientedStickers(facelets, TIP_POSITION_SLOTS[position], TIP_PIECE_STICKERS[position], orientation);
114
+ placeCells(faces, TIP_CELLS[position][position][state.tips[position]]);
89
115
  }
90
116
  return facelets;
91
117
  }
@@ -1 +1 @@
1
- {"version":3,"file":"random-state.d.ts","sourceRoot":"","sources":["../../../src/puzzles/pyraminx/random-state.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,0BAA0B,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG5E,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,0BAA+B,GAAG,aAAa,CAYnG"}
1
+ {"version":3,"file":"random-state.d.ts","sourceRoot":"","sources":["../../../src/puzzles/pyraminx/random-state.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,0BAA0B,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AA2B5E,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,0BAA+B,GAAG,aAAa,CAenG"}
@@ -1,39 +1,69 @@
1
1
  import { createRandomSource } from "../../random/index.js";
2
- import { isAllowedRandomPyraminxState, permutationParity } from "./validation.js";
2
+ import { isSolvedPyraminxState } from "./state.js";
3
+ // Compact -> full Lehmer index for the 360 even permutations of 6 edges.
4
+ // Generated once: a permutation's parity is the parity of the digit sum of
5
+ // its Lehmer code (each digit counts smaller elements to its right).
6
+ const COMPACT_TO_FULL = new Uint16Array(360);
7
+ (function buildCompactMapping() {
8
+ let compact = 0;
9
+ for (let full = 0; full < 720 && compact < 360; full += 1) {
10
+ let value = full;
11
+ const c5 = (value / 120) | 0;
12
+ value -= c5 * 120;
13
+ const c4 = (value / 24) | 0;
14
+ value -= c4 * 24;
15
+ const c3 = (value / 6) | 0;
16
+ value -= c3 * 6;
17
+ const c2 = (value / 2) | 0;
18
+ value -= c2 * 2;
19
+ const c1 = value;
20
+ if ((c5 + c4 + c3 + c2 + c1) % 2 === 0) {
21
+ COMPACT_TO_FULL[compact] = full;
22
+ compact += 1;
23
+ }
24
+ }
25
+ })();
3
26
  export function generateRandomPyraminxState(options = {}) {
4
27
  const random = createRandomSource(options.seed);
5
28
  const rejectTrivial = options.rejectTrivial ?? true;
6
29
  for (let attempt = 0; attempt < 100; attempt += 1) {
30
+ // The state is legal by construction (even permutation parity, even
31
+ // orientation sum, values in range), so only the trivial core check is
32
+ // needed here — no full re-validation.
7
33
  const state = createLegalRandomState(random);
8
- if (!rejectTrivial || isAllowedRandomPyraminxState(state)) {
34
+ if (!rejectTrivial || !isSolvedPyraminxState(state)) {
9
35
  return state;
10
36
  }
11
37
  }
12
38
  throw new Error("Unable to generate a non-trivial random Pyraminx state.");
13
39
  }
14
40
  function createLegalRandomState(random) {
15
- // 4 random centers
16
- const centers = Array.from({ length: 4 }, () => random.nextInt(3));
17
- // 4 random tips
18
- const tips = Array.from({ length: 4 }, () => random.nextInt(3));
19
- // 6 edges permutation
20
- const edgesPerm = shuffledRange(6, random);
21
- // Ensure even parity
22
- if (permutationParity(edgesPerm) !== 0) {
23
- // Swap the first two elements to flip parity
24
- const temp = edgesPerm[0];
25
- edgesPerm[0] = edgesPerm[1];
26
- edgesPerm[1] = temp;
41
+ // 4 random centers from one uniform value in [0, 81) (4 base-3 digits)
42
+ let centerValue = random.nextInt(81);
43
+ const centers = [0, 0, 0, 0];
44
+ for (let index = 0; index < 4; index += 1) {
45
+ centers[index] = centerValue % 3;
46
+ centerValue = (centerValue / 3) | 0;
47
+ }
48
+ // 4 random tips from one uniform value in [0, 81)
49
+ let tipValue = random.nextInt(81);
50
+ const tips = [0, 0, 0, 0];
51
+ for (let index = 0; index < 4; index += 1) {
52
+ tips[index] = tipValue % 3;
53
+ tipValue = (tipValue / 3) | 0;
27
54
  }
28
- // 6 edges orientation (sum must be even)
29
- const edgesOrient = Array(6).fill(0);
55
+ // 6 edges permutation, even parity, uniform over the 360 even permutations
56
+ const edgesPerm = permutationFromCompact(random.nextInt(360));
57
+ // 6 edges orientation (sum must be even): 5 free bits + parity digit
58
+ let orientationValue = random.nextInt(32);
59
+ const edgesOrient = [0, 0, 0, 0, 0, 0];
30
60
  let sumOrient = 0;
31
- for (let i = 0; i < 5; i++) {
32
- const val = random.nextInt(2);
33
- edgesOrient[i] = val;
34
- sumOrient += val;
61
+ for (let index = 0; index < 5; index += 1) {
62
+ edgesOrient[index] = orientationValue & 1;
63
+ sumOrient += orientationValue & 1;
64
+ orientationValue >>= 1;
35
65
  }
36
- edgesOrient[5] = sumOrient % 2 === 0 ? 0 : 1;
66
+ edgesOrient[5] = sumOrient & 1;
37
67
  return {
38
68
  centers,
39
69
  edgesPerm,
@@ -41,13 +71,17 @@ function createLegalRandomState(random) {
41
71
  tips
42
72
  };
43
73
  }
44
- function shuffledRange(length, random) {
45
- const values = Array.from({ length }, (_, index) => index);
46
- for (let index = values.length - 1; index > 0; index -= 1) {
47
- const swapIndex = random.nextInt(index + 1);
48
- const temporary = values[index];
49
- values[index] = values[swapIndex];
50
- values[swapIndex] = temporary;
74
+ const FACTORIALS = [1, 1, 2, 6, 24, 120, 720];
75
+ function permutationFromCompact(compact) {
76
+ let full = COMPACT_TO_FULL[compact];
77
+ const permutation = [0, 0, 0, 0, 0, 0];
78
+ const remaining = [0, 1, 2, 3, 4, 5];
79
+ for (let position = 0; position < 6; position += 1) {
80
+ const divisor = FACTORIALS[5 - position];
81
+ const choice = (full / divisor) | 0;
82
+ full -= choice * divisor;
83
+ permutation[position] = remaining[choice];
84
+ remaining.splice(choice, 1);
51
85
  }
52
- return values;
86
+ return permutation;
53
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"scrambler.d.ts","sourceRoot":"","sources":["../../../src/puzzles/pyraminx/scrambler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAW9F,eAAO,MAAM,yBAAyB,EAAE,iBAYvC,CAAC;AAEF,wBAAsB,wBAAwB,CAC5C,OAAO,GAAE,eAAe,GAAG;IAAE,cAAc,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3D,OAAO,CAAC,cAAc,CAAC,CAkDzB"}
1
+ {"version":3,"file":"scrambler.d.ts","sourceRoot":"","sources":["../../../src/puzzles/pyraminx/scrambler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAe9F,eAAO,MAAM,yBAAyB,EAAE,iBAYvC,CAAC;AAEF,wBAAsB,wBAAwB,CAC5C,OAAO,GAAE,eAAe,GAAG;IAAE,cAAc,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3D,OAAO,CAAC,cAAc,CAAC,CA8DzB"}
@@ -1,10 +1,8 @@
1
1
  import { generateRandomPyraminxState } from "./random-state.js";
2
- import { invertPyraminxAlgorithm } from "./moves.js";
3
- import { solvePyraminx, verifyPyraminxScramble } from "./solver/index.js";
2
+ import { arePyraminxTablesLoaded, solvePyraminxIndices, solvePyraminxIndicesSync, verifyPyraminxScrambleIndices, PYRAMINX_MOVES } from "./solver/index.js";
4
3
  import { formatPyraminxFacelets, toPyraminxFacelets } from "./facelets.js";
5
4
  import { PYRAMINX_TIPS } from "./types.js";
6
- // Face -> tip index (U,L,R,B order, same as PYRAMINX_TIPS).
7
- const FACE_TIP_INDEX = { U: 0, L: 1, R: 2, B: 3 };
5
+ const TIP_INVERSE = ["u'", "l'", "r'", "b'"];
8
6
  export const pyraminxScrambleGenerator = {
9
7
  info: {
10
8
  eventId: "pymx",
@@ -20,47 +18,58 @@ export const pyraminxScrambleGenerator = {
20
18
  };
21
19
  export async function generatePyraminxScramble(options = {}) {
22
20
  const state = generateRandomPyraminxState(options);
23
- // Solve the core edges and centers
24
- const solution = await solvePyraminx(state);
25
- const scrambleMoves = invertPyraminxAlgorithm(solution);
26
- // Append tips: layer moves already rotate tips, so we need the delta
27
- // between the target tips and the tips produced by the core scramble.
28
- // Computed arithmetically from the move sequence (no state replay).
21
+ // Solve the core edges and centers as move indices (0..7). For index m the
22
+ // inverse is m ^ 1, the face is m >> 1 (0=U, 1=L, 2=R, 3=B) and the
23
+ // direction is m & 1 (1 = clockwise, 0 = counter-clockwise).
24
+ const solution = arePyraminxTablesLoaded()
25
+ ? solvePyraminxIndicesSync(state)
26
+ : await solvePyraminxIndices(state);
27
+ const depth = solution.length;
28
+ // Single pass over the solution (reversed): build the scramble move strings
29
+ // while accumulating the tips rotated by the layer moves arithmetically
30
+ // (no state replay, no string parsing).
31
+ const moves = new Array(depth + 4);
29
32
  const coreTips = [0, 0, 0, 0];
30
- for (const move of scrambleMoves) {
31
- const isInverse = move.endsWith("'");
32
- const face = isInverse ? move.slice(0, -1) : move;
33
- const tipIndex = FACE_TIP_INDEX[face];
34
- if (tipIndex !== undefined) {
35
- coreTips[tipIndex] = (coreTips[tipIndex] + (isInverse ? 2 : 1)) % 3;
36
- }
33
+ let count = 0;
34
+ for (let index = depth - 1; index >= 0; index -= 1) {
35
+ const m = solution[index] ^ 1;
36
+ moves[count++] = PYRAMINX_MOVES[m];
37
+ const face = m >> 1;
38
+ coreTips[face] = (coreTips[face] + (m & 1 ? 2 : 1)) % 3;
37
39
  }
38
- const finalMoves = [...scrambleMoves];
39
- for (let i = 0; i < 4; i++) {
40
+ const tipMoves = [];
41
+ for (let i = 0; i < 4; i += 1) {
40
42
  const delta = (state.tips[i] - coreTips[i] + 3) % 3;
41
43
  if (delta === 1) {
42
- finalMoves.push(PYRAMINX_TIPS[i]);
44
+ moves[count++] = PYRAMINX_TIPS[i];
45
+ tipMoves.push(i * 2);
43
46
  }
44
47
  else if (delta === 2) {
45
- finalMoves.push(`${PYRAMINX_TIPS[i]}'`);
48
+ moves[count++] = TIP_INVERSE[i];
49
+ tipMoves.push(i * 2 + 1);
46
50
  }
47
51
  }
48
- if (!options.skipValidation && !verifyPyraminxScramble(state, finalMoves)) {
52
+ moves.length = count;
53
+ if (!options.skipValidation && !verifyPyraminxScrambleIndices(state, solution, tipMoves)) {
49
54
  throw new Error("Generated Pyraminx scramble does not reproduce the target random state.");
50
55
  }
51
56
  const facelets = toPyraminxFacelets(state);
57
+ const solutionMoves = new Array(depth);
58
+ for (let index = 0; index < depth; index += 1) {
59
+ solutionMoves[index] = PYRAMINX_MOVES[solution[index]];
60
+ }
52
61
  return {
53
62
  eventId: "pymx",
54
- scramble: finalMoves.join(" "),
55
- moves: finalMoves,
63
+ scramble: moves.join(" "),
64
+ moves,
56
65
  quality: "random-state",
57
66
  generator: "pyraminx-optimal",
58
67
  metadata: {
59
68
  state,
60
69
  facelets,
61
70
  faceletString: formatPyraminxFacelets(facelets),
62
- solutionMoves: solution,
63
- solutionDepth: solution.length
71
+ solutionMoves,
72
+ solutionDepth: depth
64
73
  }
65
74
  };
66
75
  }
@@ -5,6 +5,9 @@ export interface PyraminxSolution {
5
5
  }
6
6
  export declare function solvePyraminx(state: PyraminxState): Promise<PyraminxMove[]>;
7
7
  export declare function solvePyraminxWithInfo(state: PyraminxState): Promise<PyraminxSolution>;
8
+ export declare function solvePyraminxIndices(state: PyraminxState): Promise<number[]>;
9
+ export declare function solvePyraminxIndicesSync(state: PyraminxState): number[];
8
10
  export declare function solvePyraminxSync(state: PyraminxState): PyraminxMove[];
9
11
  export declare function verifyPyraminxScramble(state: PyraminxState, finalMoves: readonly PyraminxMove[]): boolean;
12
+ export declare function verifyPyraminxScrambleIndices(state: PyraminxState, solution: readonly number[], tipMoves: readonly number[]): boolean;
10
13
  //# sourceMappingURL=search.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/pyraminx/solver/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAgB/D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAGjF;AAED,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAG3F;AA8BD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,EAAE,CAGtE;AAaD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,YAAY,EAAE,GAAG,OAAO,CAsCzG"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/pyraminx/solver/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAe/D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAGjF;AAED,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAG3F;AAMD,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAGlF;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,EAAE,CAGvE;AAuCD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,EAAE,CAGtE;AAaD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,YAAY,EAAE,GAAG,OAAO,CAsCzG;AAKD,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,OAAO,CAmCT"}
@@ -1,4 +1,4 @@
1
- import { POLICY_TABLE, CENTERS_TABLE, PERMUTATION_TABLE, ORIENTATION_TABLE, PYRAMINX_MOVES, MOVE_COUNT, PERMUTATION_COUNT, getCentersIndex, getOrientationIndex, getPermutationCompactIndex, getPolicyMoveFromPacked, ensureTablesLoaded } from "./tables.js";
1
+ import { POLICY_TABLE, CENTERS_TABLE, PERMUTATION_TABLE, ORIENTATION_TABLE, PYRAMINX_MOVES, MOVE_COUNT, PERMUTATION_COUNT, getCentersIndex, getOrientationIndex, getPermutationCompactIndex, ensureTablesLoaded } from "./tables.js";
2
2
  export async function solvePyraminx(state) {
3
3
  await ensureTablesLoaded();
4
4
  return solveWithTables(state).moves;
@@ -7,7 +7,25 @@ export async function solvePyraminxWithInfo(state) {
7
7
  await ensureTablesLoaded();
8
8
  return solveWithTables(state);
9
9
  }
10
+ // Index-based solve for the hot scramble path: returns the solution as move
11
+ // indices (0..7) in walk order. The inverse of index m is m ^ 1 (moves are
12
+ // paired U/U', L/L', R/R', B/B'), its face is m >> 1 and its direction is
13
+ // m & 1 (1 = clockwise, 0 = counter-clockwise).
14
+ export async function solvePyraminxIndices(state) {
15
+ await ensureTablesLoaded();
16
+ return solveWithTablesIndices(state);
17
+ }
18
+ export function solvePyraminxIndicesSync(state) {
19
+ if (!POLICY_TABLE)
20
+ throw new Error("Pyraminx tables not loaded");
21
+ return solveWithTablesIndices(state);
22
+ }
10
23
  function solveWithTables(state) {
24
+ const indices = solveWithTablesIndices(state);
25
+ const moves = indices.map((index) => PYRAMINX_MOVES[index]);
26
+ return { moves, depth: moves.length };
27
+ }
28
+ function solveWithTablesIndices(state) {
11
29
  const centersTable = CENTERS_TABLE;
12
30
  const permTable = PERMUTATION_TABLE;
13
31
  const orientTable = ORIENTATION_TABLE;
@@ -17,17 +35,20 @@ function solveWithTables(state) {
17
35
  let o = getOrientationIndex(state.edgesOrient);
18
36
  let stateIndex = c + 81 * (p + PERMUTATION_COUNT * o);
19
37
  if (stateIndex === 0)
20
- return { moves: [], depth: 0 };
38
+ return [];
21
39
  const moves = [];
22
40
  while (stateIndex !== 0) {
23
- const m = getPolicyMoveFromPacked(policy, stateIndex);
24
- moves.push(PYRAMINX_MOVES[m]);
41
+ const bits = stateIndex * 3;
42
+ const byteIndex = bits >> 3;
43
+ const offset = bits & 7;
44
+ const m = ((policy[byteIndex] | ((policy[byteIndex + 1] ?? 0) << 8)) >> offset) & 0x07;
45
+ moves.push(m);
25
46
  c = centersTable[c * MOVE_COUNT + m];
26
47
  p = permTable[p * MOVE_COUNT + m];
27
48
  o = orientTable[o * MOVE_COUNT + m];
28
49
  stateIndex = c + 81 * (p + PERMUTATION_COUNT * o);
29
50
  }
30
- return { moves, depth: moves.length };
51
+ return moves;
31
52
  }
32
53
  // Synchronous variant for scripts that have already ensured tables are loaded
33
54
  export function solvePyraminxSync(state) {
@@ -80,3 +101,37 @@ export function verifyPyraminxScramble(state, finalMoves) {
80
101
  tips[2] === state.tips[2] &&
81
102
  tips[3] === state.tips[3]);
82
103
  }
104
+ // Numeric verification for the hot scramble path: replays the scramble
105
+ // (solution reversed with m ^ 1 inverses, tip moves as tipIndex * 2 + dir
106
+ // with dir 0 = clockwise, 1 = counter-clockwise) without any string parsing.
107
+ export function verifyPyraminxScrambleIndices(state, solution, tipMoves) {
108
+ const centersTable = CENTERS_TABLE;
109
+ const permTable = PERMUTATION_TABLE;
110
+ const orientTable = ORIENTATION_TABLE;
111
+ if (!centersTable || !permTable || !orientTable) {
112
+ throw new Error("Pyraminx solver tables not loaded. Call ensureTablesLoaded() first.");
113
+ }
114
+ let c = 0;
115
+ let p = 0;
116
+ let o = 0;
117
+ const tips = [0, 0, 0, 0];
118
+ for (let index = solution.length - 1; index >= 0; index -= 1) {
119
+ const m = solution[index] ^ 1;
120
+ c = centersTable[c * MOVE_COUNT + m];
121
+ p = permTable[p * MOVE_COUNT + m];
122
+ o = orientTable[o * MOVE_COUNT + m];
123
+ const face = m >> 1;
124
+ tips[face] = (tips[face] + (m & 1 ? 2 : 1)) % 3;
125
+ }
126
+ for (const tipMove of tipMoves) {
127
+ const tipIndex = tipMove >> 1;
128
+ tips[tipIndex] = (tips[tipIndex] + (tipMove & 1 ? 2 : 1)) % 3;
129
+ }
130
+ return (c === getCentersIndex(state.centers) &&
131
+ p === getPermutationCompactIndex(state.edgesPerm) &&
132
+ o === getOrientationIndex(state.edgesOrient) &&
133
+ tips[0] === state.tips[0] &&
134
+ tips[1] === state.tips[1] &&
135
+ tips[2] === state.tips[2] &&
136
+ tips[3] === state.tips[3]);
137
+ }
@@ -35,6 +35,7 @@ export declare let PERMUTATION_TABLE: Uint16Array | null;
35
35
  export declare let ORIENTATION_TABLE: Uint8Array | null;
36
36
  export declare let POLICY_TABLE: Uint8Array | null;
37
37
  export declare function ensureTablesLoaded(): Promise<void>;
38
+ export declare function arePyraminxTablesLoaded(): boolean;
38
39
  export declare function getPolicyMove(stateIndex: number): number;
39
40
  export declare function getCentersTransition(centerIdx: number, move: number): number;
40
41
  export declare function getPermutationTransition(permIdx: number, move: number): number;
@@ -1 +1 @@
1
- {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/pyraminx/solver/tables.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/pyraminx/solver/tables.ts"],"names":[],"mappings":"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"}
@@ -271,6 +271,9 @@ export async function ensureTablesLoaded() {
271
271
  POLICY_TABLE = tables.policyTable;
272
272
  tablesLoaded = true;
273
273
  }
274
+ export function arePyraminxTablesLoaded() {
275
+ return tablesLoaded;
276
+ }
274
277
  export function getPolicyMove(stateIndex) {
275
278
  if (!POLICY_TABLE)
276
279
  throw new Error("Pyraminx solver tables not loaded. Call ensureTablesLoaded() first.");
@@ -1 +1 @@
1
- {"version":3,"file":"facelets.d.ts","sourceRoot":"","sources":["../../../src/puzzles/skewb/facelets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgC5D,wBAAgB,yBAAyB,IAAI,aAAa,CASzD;AAaD,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa,CAsBhE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAEnE;AAED,wBAAgB,iBAAiB,IAAI,SAAS,MAAM,EAAE,CAErD"}
1
+ {"version":3,"file":"facelets.d.ts","sourceRoot":"","sources":["../../../src/puzzles/skewb/facelets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA0D5D,wBAAgB,yBAAyB,IAAI,aAAa,CASzD;AAaD,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa,CAqBhE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAEnE;AAED,wBAAgB,iBAAiB,IAAI,SAAS,MAAM,EAAE,CAErD"}
@@ -1,6 +1,7 @@
1
1
  // Facelets model matching cubingcenter-viz/src/puzzles/skewb.ts
2
2
  // Each face has 5 stickers: 0=center, 1=TL,2=TR,3=BL,4=BR (net layout)
3
3
  const FACE_ORDER = ["U", "R", "F", "D", "L", "B"];
4
+ const FACE_INDEX = { U: 0, R: 1, F: 2, D: 3, L: 4, B: 5 };
4
5
  // Canonical sticker faces per corner position: [UD sticker, RL sticker, FB sticker]
5
6
  const CORNER_STICKERS = [
6
7
  ["U", "R", "F"],
@@ -23,43 +24,65 @@ const CORNER_SLOTS = [
23
24
  [["D", 4], ["R", 4], ["B", 3]],
24
25
  [["D", 3], ["L", 3], ["B", 4]],
25
26
  ];
27
+ // For each (position, piece, orientation) the three facelet writes, flattened:
28
+ // face index, sticker slot index and sticker color index, so the hot loop is
29
+ // pure table lookups (no tuple destructuring, no modulo).
30
+ const CORNER_WRITE_COUNT = 8 * 8 * 3 * 3;
31
+ const WRITE_FACE = new Int8Array(CORNER_WRITE_COUNT);
32
+ const WRITE_SLOT = new Int8Array(CORNER_WRITE_COUNT);
33
+ const WRITE_COLOR = new Int8Array(CORNER_WRITE_COUNT);
34
+ for (let pos = 0; pos < 8; pos++) {
35
+ const slots = CORNER_SLOTS[pos];
36
+ for (let piece = 0; piece < 8; piece++) {
37
+ const stickers = CORNER_STICKERS[piece];
38
+ for (let ori = 0; ori < 3; ori++) {
39
+ const base = ((pos * 8 + piece) * 3 + ori) * 3;
40
+ for (let i = 0; i < 3; i++) {
41
+ const [face, idx] = slots[i];
42
+ const color = stickers[(i + 3 - ori) % 3];
43
+ WRITE_FACE[base + i] = FACE_INDEX[face];
44
+ WRITE_SLOT[base + i] = idx;
45
+ WRITE_COLOR[base + i] = FACE_INDEX[color];
46
+ }
47
+ }
48
+ }
49
+ }
26
50
  export function createSolvedSkewbFacelets() {
27
51
  return {
28
- U: Array.from({ length: 5 }, () => "U"),
29
- R: Array.from({ length: 5 }, () => "R"),
30
- F: Array.from({ length: 5 }, () => "F"),
31
- D: Array.from({ length: 5 }, () => "D"),
32
- L: Array.from({ length: 5 }, () => "L"),
33
- B: Array.from({ length: 5 }, () => "B"),
52
+ U: ["U", "U", "U", "U", "U"],
53
+ R: ["R", "R", "R", "R", "R"],
54
+ F: ["F", "F", "F", "F", "F"],
55
+ D: ["D", "D", "D", "D", "D"],
56
+ L: ["L", "L", "L", "L", "L"],
57
+ B: ["B", "B", "B", "B", "B"],
34
58
  };
35
59
  }
36
60
  function createEmptySkewbFacelets() {
37
61
  return {
38
- U: Array(5).fill("?"),
39
- R: Array(5).fill("?"),
40
- F: Array(5).fill("?"),
41
- D: Array(5).fill("?"),
42
- L: Array(5).fill("?"),
43
- B: Array(5).fill("?"),
62
+ U: ["?", "?", "?", "?", "?"],
63
+ R: ["?", "?", "?", "?", "?"],
64
+ F: ["?", "?", "?", "?", "?"],
65
+ D: ["?", "?", "?", "?", "?"],
66
+ L: ["?", "?", "?", "?", "?"],
67
+ B: ["?", "?", "?", "?", "?"],
44
68
  };
45
69
  }
46
70
  export function toSkewbFacelets(state) {
47
71
  const facelets = createEmptySkewbFacelets();
72
+ const faces = [facelets.U, facelets.R, facelets.F, facelets.D, facelets.L, facelets.B];
73
+ const perm = state.corners.permutation;
74
+ const orient = state.corners.orientation;
75
+ const centers = state.centers;
48
76
  // centers
49
77
  for (let face = 0; face < 6; face++) {
50
- const targetFace = FACE_ORDER[face];
51
- facelets[targetFace][0] = FACE_ORDER[state.centers[face]];
78
+ faces[face][0] = FACE_ORDER[centers[face]];
52
79
  }
53
- // corners
80
+ // corners (unrolled 3 writes per position)
54
81
  for (let pos = 0; pos < 8; pos++) {
55
- const piece = state.corners.permutation[pos];
56
- const ori = state.corners.orientation[pos];
57
- const slots = CORNER_SLOTS[pos];
58
- const stickers = CORNER_STICKERS[piece];
59
- for (let i = 0; i < 3; i++) {
60
- const [face, idx] = slots[i];
61
- facelets[face][idx] = stickers[(i + 3 - ori) % 3];
62
- }
82
+ const base = ((pos * 8 + perm[pos]) * 3 + orient[pos]) * 3;
83
+ faces[WRITE_FACE[base]][WRITE_SLOT[base]] = FACE_ORDER[WRITE_COLOR[base]];
84
+ faces[WRITE_FACE[base + 1]][WRITE_SLOT[base + 1]] = FACE_ORDER[WRITE_COLOR[base + 1]];
85
+ faces[WRITE_FACE[base + 2]][WRITE_SLOT[base + 2]] = FACE_ORDER[WRITE_COLOR[base + 2]];
63
86
  }
64
87
  return facelets;
65
88
  }