@wodzik/cubecore 0.1.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 (279) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +118 -0
  3. package/analyze/client.d.ts +18 -0
  4. package/analyze/client.js +42 -0
  5. package/analyze/index.d.ts +114 -0
  6. package/analyze/index.js +151 -0
  7. package/analyze/rotations.d.ts +3 -0
  8. package/analyze/rotations.js +15 -0
  9. package/analyze/roux.d.ts +67 -0
  10. package/analyze/roux.js +95 -0
  11. package/analyze/worker.d.ts +6 -0
  12. package/analyze/worker.js +31 -0
  13. package/analyze/zz.d.ts +47 -0
  14. package/analyze/zz.js +102 -0
  15. package/bld/index.d.ts +14 -0
  16. package/bld/index.js +14 -0
  17. package/bld/memo.d.ts +66 -0
  18. package/bld/memo.js +139 -0
  19. package/bld/scheme.d.ts +30 -0
  20. package/bld/scheme.js +35 -0
  21. package/bld/skin.d.ts +26 -0
  22. package/bld/skin.js +46 -0
  23. package/bld/tracker.d.ts +68 -0
  24. package/bld/tracker.js +145 -0
  25. package/bluetooth/clock.d.ts +22 -0
  26. package/bluetooth/clock.js +37 -0
  27. package/bluetooth/cubeSkins.d.ts +31 -0
  28. package/bluetooth/cubeSkins.js +32 -0
  29. package/bluetooth/gyro.d.ts +31 -0
  30. package/bluetooth/gyro.js +46 -0
  31. package/bluetooth/index.d.ts +5 -0
  32. package/bluetooth/index.js +5 -0
  33. package/bluetooth/session.d.ts +140 -0
  34. package/bluetooth/session.js +201 -0
  35. package/bluetooth/simulated.d.ts +52 -0
  36. package/bluetooth/simulated.js +80 -0
  37. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.d.ts +38 -0
  38. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.js +51 -0
  39. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.d.ts +35 -0
  40. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.js +70 -0
  41. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.d.ts +273 -0
  42. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.js +846 -0
  43. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.d.ts +38 -0
  44. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.js +340 -0
  45. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.d.ts +11 -0
  46. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.js +118 -0
  47. package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.d.ts +2 -0
  48. package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.js +15 -0
  49. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.d.ts +18 -0
  50. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.js +152 -0
  51. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.d.ts +74 -0
  52. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.js +161 -0
  53. package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.d.ts +7 -0
  54. package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.js +11 -0
  55. package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.d.ts +2 -0
  56. package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.js +36 -0
  57. package/bluetooth/vendor/smartcube-web-bluetooth/index.d.ts +4 -0
  58. package/bluetooth/vendor/smartcube-web-bluetooth/index.js +4 -0
  59. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.d.ts +12 -0
  60. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.js +122 -0
  61. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.d.ts +11 -0
  62. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.js +55 -0
  63. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.d.ts +21 -0
  64. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.js +47 -0
  65. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.d.ts +6 -0
  66. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.js +24 -0
  67. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.d.ts +6 -0
  68. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.js +57 -0
  69. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.d.ts +11 -0
  70. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.js +67 -0
  71. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.d.ts +8 -0
  72. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.js +98 -0
  73. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.d.ts +8 -0
  74. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.js +128 -0
  75. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.d.ts +5 -0
  76. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.js +60 -0
  77. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.d.ts +2 -0
  78. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.js +8 -0
  79. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.d.ts +7 -0
  80. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.js +142 -0
  81. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.d.ts +6 -0
  82. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.js +21 -0
  83. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.d.ts +32 -0
  84. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.js +1 -0
  85. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.d.ts +6 -0
  86. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.js +68 -0
  87. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.d.ts +3 -0
  88. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.js +178 -0
  89. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.d.ts +16 -0
  90. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.js +141 -0
  91. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.d.ts +11 -0
  92. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.js +10 -0
  93. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.d.ts +26 -0
  94. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.js +8 -0
  95. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.d.ts +3 -0
  96. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.js +265 -0
  97. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.d.ts +3 -0
  98. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.js +345 -0
  99. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.d.ts +15 -0
  100. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.js +382 -0
  101. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.d.ts +3 -0
  102. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.js +352 -0
  103. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.d.ts +57 -0
  104. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.js +243 -0
  105. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.d.ts +3 -0
  106. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.js +454 -0
  107. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.d.ts +3 -0
  108. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.js +424 -0
  109. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.d.ts +78 -0
  110. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.js +1 -0
  111. package/bluetooth/vendor/smartcube-web-bluetooth/utils.d.ts +45 -0
  112. package/bluetooth/vendor/smartcube-web-bluetooth/utils.js +150 -0
  113. package/cfop/caseData.d.ts +16 -0
  114. package/cfop/caseData.js +88 -0
  115. package/cfop/cases.d.ts +42 -0
  116. package/cfop/cases.js +116 -0
  117. package/cfop/f2l.d.ts +28 -0
  118. package/cfop/f2l.js +88 -0
  119. package/cfop/f2lData.d.ts +15 -0
  120. package/cfop/f2lData.js +49 -0
  121. package/cfop/index.d.ts +41 -0
  122. package/cfop/index.js +110 -0
  123. package/cfop/slots.d.ts +2 -0
  124. package/cfop/slots.js +1 -0
  125. package/core/arrows.d.ts +24 -0
  126. package/core/arrows.js +22 -0
  127. package/core/bytes.d.ts +27 -0
  128. package/core/bytes.js +145 -0
  129. package/core/centers.d.ts +32 -0
  130. package/core/centers.js +83 -0
  131. package/core/checks.d.ts +53 -0
  132. package/core/checks.js +85 -0
  133. package/core/cubies.d.ts +45 -0
  134. package/core/cubies.js +169 -0
  135. package/core/frames.d.ts +61 -0
  136. package/core/frames.js +126 -0
  137. package/core/geometry.d.ts +58 -0
  138. package/core/geometry.js +132 -0
  139. package/core/index.d.ts +19 -0
  140. package/core/index.js +19 -0
  141. package/core/masks.d.ts +32 -0
  142. package/core/masks.js +39 -0
  143. package/core/method.d.ts +35 -0
  144. package/core/method.js +30 -0
  145. package/core/moves.d.ts +48 -0
  146. package/core/moves.js +88 -0
  147. package/core/notation.d.ts +48 -0
  148. package/core/notation.js +226 -0
  149. package/core/physical.d.ts +53 -0
  150. package/core/physical.js +99 -0
  151. package/core/practice.d.ts +62 -0
  152. package/core/practice.js +87 -0
  153. package/core/sequence.d.ts +90 -0
  154. package/core/sequence.js +157 -0
  155. package/core/stages.d.ts +90 -0
  156. package/core/stages.js +21 -0
  157. package/core/state.d.ts +55 -0
  158. package/core/state.js +140 -0
  159. package/core/stateCodec.d.ts +12 -0
  160. package/core/stateCodec.js +51 -0
  161. package/core/stream.d.ts +45 -0
  162. package/core/stream.js +75 -0
  163. package/core/tracker.d.ts +62 -0
  164. package/core/tracker.js +121 -0
  165. package/element/alg.d.ts +45 -0
  166. package/element/alg.js +150 -0
  167. package/element/base.d.ts +2 -0
  168. package/element/base.js +3 -0
  169. package/element/bld.d.ts +82 -0
  170. package/element/bld.js +222 -0
  171. package/element/index.d.ts +8 -0
  172. package/element/index.js +21 -0
  173. package/element/model.d.ts +36 -0
  174. package/element/model.js +64 -0
  175. package/element/player.d.ts +196 -0
  176. package/element/player.js +680 -0
  177. package/element/practice.d.ts +75 -0
  178. package/element/practice.js +150 -0
  179. package/element/scramble.d.ts +50 -0
  180. package/element/scramble.js +107 -0
  181. package/element/sequence.d.ts +126 -0
  182. package/element/sequence.js +254 -0
  183. package/element/styles.d.ts +32 -0
  184. package/element/styles.js +196 -0
  185. package/image/cache.d.ts +21 -0
  186. package/image/cache.js +66 -0
  187. package/image/index.d.ts +2 -0
  188. package/image/index.js +2 -0
  189. package/image/png-node.d.ts +6 -0
  190. package/image/png-node.js +10 -0
  191. package/image/svg.d.ts +36 -0
  192. package/image/svg.js +215 -0
  193. package/lbl/index.d.ts +10 -0
  194. package/lbl/index.js +31 -0
  195. package/methods/index.d.ts +20 -0
  196. package/methods/index.js +33 -0
  197. package/package.json +133 -0
  198. package/petrus/index.d.ts +11 -0
  199. package/petrus/index.js +27 -0
  200. package/react/index.d.ts +176 -0
  201. package/react/index.js +228 -0
  202. package/render/arrows.d.ts +68 -0
  203. package/render/arrows.js +190 -0
  204. package/render/build/attachments.d.ts +35 -0
  205. package/render/build/attachments.js +94 -0
  206. package/render/build/models.d.ts +9 -0
  207. package/render/build/models.js +27 -0
  208. package/render/build/tiles.d.ts +48 -0
  209. package/render/build/tiles.js +194 -0
  210. package/render/gltf.d.ts +10 -0
  211. package/render/gltf.js +106 -0
  212. package/render/index.d.ts +12 -0
  213. package/render/index.js +12 -0
  214. package/render/layers.d.ts +21 -0
  215. package/render/layers.js +24 -0
  216. package/render/pictures.d.ts +46 -0
  217. package/render/pictures.js +80 -0
  218. package/render/pieceModels.d.ts +38 -0
  219. package/render/pieceModels.js +43 -0
  220. package/render/renderer.d.ts +165 -0
  221. package/render/renderer.js +647 -0
  222. package/render/tile.d.ts +114 -0
  223. package/render/tile.js +390 -0
  224. package/render/viewports.d.ts +25 -0
  225. package/render/viewports.js +26 -0
  226. package/roux/cmll.d.ts +35 -0
  227. package/roux/cmll.js +121 -0
  228. package/roux/cmllData.d.ts +12 -0
  229. package/roux/cmllData.js +49 -0
  230. package/roux/index.d.ts +52 -0
  231. package/roux/index.js +175 -0
  232. package/skin/attachments.d.ts +82 -0
  233. package/skin/attachments.js +110 -0
  234. package/skin/index.d.ts +3 -0
  235. package/skin/index.js +3 -0
  236. package/skin/shapes.d.ts +112 -0
  237. package/skin/shapes.js +147 -0
  238. package/skin/skin.d.ts +911 -0
  239. package/skin/skin.js +412 -0
  240. package/solve/client.d.ts +58 -0
  241. package/solve/client.js +56 -0
  242. package/solve/handle.d.ts +6 -0
  243. package/solve/handle.js +38 -0
  244. package/solve/index.d.ts +8 -0
  245. package/solve/index.js +7 -0
  246. package/solve/lse.d.ts +42 -0
  247. package/solve/lse.js +151 -0
  248. package/solve/pieces.d.ts +18 -0
  249. package/solve/pieces.js +24 -0
  250. package/solve/protocol.d.ts +70 -0
  251. package/solve/protocol.js +16 -0
  252. package/solve/scramble.d.ts +76 -0
  253. package/solve/scramble.js +187 -0
  254. package/solve/stage.d.ts +108 -0
  255. package/solve/stage.js +532 -0
  256. package/solve/twophase.d.ts +36 -0
  257. package/solve/twophase.js +229 -0
  258. package/solve/worker.d.ts +6 -0
  259. package/solve/worker.js +15 -0
  260. package/timeline/codec.d.ts +23 -0
  261. package/timeline/codec.js +84 -0
  262. package/timeline/index.d.ts +7 -0
  263. package/timeline/index.js +7 -0
  264. package/timeline/recording.d.ts +19 -0
  265. package/timeline/recording.js +20 -0
  266. package/timeline/replay.d.ts +68 -0
  267. package/timeline/replay.js +145 -0
  268. package/timeline/segments.d.ts +31 -0
  269. package/timeline/segments.js +39 -0
  270. package/timeline/share.d.ts +30 -0
  271. package/timeline/share.js +62 -0
  272. package/timeline/stats.d.ts +34 -0
  273. package/timeline/stats.js +56 -0
  274. package/timeline/timings.d.ts +30 -0
  275. package/timeline/timings.js +54 -0
  276. package/zz/index.d.ts +29 -0
  277. package/zz/index.js +68 -0
  278. package/zz/seeds.d.ts +9 -0
  279. package/zz/seeds.js +29 -0
package/solve/stage.js ADDED
@@ -0,0 +1,532 @@
1
+ /**
2
+ * Optimal solvers for solving stages: cross, xcross, xxcross, EOCross, an F2L
3
+ * slot, the Roux first block — any set of pieces (plus, optionally, the
4
+ * orientation of every edge). Answers: how many moves (`distance`), every
5
+ * optimal solution (`solve`), the optimal next moves from here (`nextMoves`),
6
+ * and random placements at an exact distance (`sample`, for trainer
7
+ * scrambles: "a cross that takes exactly 6").
8
+ *
9
+ * Canonical frame: cross on D, slots FR / FL / BL / BR, Roux first block on
10
+ * L-bottom. Pass `frame` to work on another face (`frameFor("U")` = cross
11
+ * on U) — colour neutral like the rest of cubecore.
12
+ *
13
+ * How: IDA* over the tracked pieces. Each piece is one number (position ×
14
+ * orientations + orientation, 0..23); the heuristic is the max of exact
15
+ * distance tables for groups of up to five pieces (built by breadth-first
16
+ * search on first use, shared between stages — the cross + one slot piece
17
+ * tables are ~8 MB each and take a few hundred ms).
18
+ */
19
+ import { FRAMES, IDENTITY_FRAME, applyMoves, parseAlg, reframe, solvedState, toCubies, transformMoves } from "../core/index.js";
20
+ const SOLVED_STATE = solvedState();
21
+ /** Set by scramble.ts (randomState lives there; avoids an import cycle). */
22
+ let randomStateHook = null;
23
+ export const setRandomStateHook = (f) => void (randomStateHook = f);
24
+ import { MOVES, MOVE_CC } from "./pieces.js";
25
+ // ─── piece moves ───
26
+ const N_MOVES = 18;
27
+ /** Next value of an edge (pos × 2 + ori) / corner (pos × 3 + ori) under each move. */
28
+ const EDGE_NEXT = new Uint8Array(24 * N_MOVES);
29
+ const CORNER_NEXT = new Uint8Array(24 * N_MOVES);
30
+ const FLIP_NEXT = new Uint16Array(2048 * N_MOVES);
31
+ MOVE_CC.forEach((b, m) => {
32
+ for (let i = 0; i < 12; i++) {
33
+ const from = b.ep[i];
34
+ for (let o = 0; o < 2; o++)
35
+ EDGE_NEXT[(from * 2 + o) * N_MOVES + m] = i * 2 + ((o + b.eo[i]) % 2);
36
+ }
37
+ for (let i = 0; i < 8; i++) {
38
+ const from = b.cp[i];
39
+ for (let o = 0; o < 3; o++)
40
+ CORNER_NEXT[(from * 3 + o) * N_MOVES + m] = i * 3 + ((o + b.co[i]) % 3);
41
+ }
42
+ // Flip coordinate (orientation of the edges at positions 0..10; 11 follows): as in two-phase.
43
+ for (let f = 0; f < 2048; f++) {
44
+ const eo = new Array(12).fill(0);
45
+ for (let i = 10, t = f; i >= 0; i--, t >>= 1)
46
+ eo[i] = t & 1;
47
+ eo[11] = eo.reduce((a, x) => a + x, 0) % 2;
48
+ let g = 0;
49
+ for (let i = 0; i < 11; i++)
50
+ g = g * 2 + ((eo[b.ep[i]] + b.eo[i]) % 2);
51
+ FLIP_NEXT[f * N_MOVES + m] = g;
52
+ }
53
+ });
54
+ const home = (p) => (p.kind === "edge" ? p.id * 2 : p.id * 3);
55
+ const next = (p, value, m) => (p.kind === "edge" ? EDGE_NEXT : CORNER_NEXT)[value * N_MOVES + m];
56
+ // ─── distance tables for groups of pieces ───
57
+ const tableCache = new Map();
58
+ /** Bump when table contents change, so stored copies are rebuilt. */
59
+ const TABLE_VERSION = 1;
60
+ const groupKey = (pieces, goals, moves) => `v${TABLE_VERSION}:${pieces.map((p) => `${p.kind[0]}${p.id}`).join(",")}` +
61
+ (goals ? `|g:${goals.map((g) => g.join(".")).join(";")}` : "") +
62
+ (moves ? `|m:${moves.join(".")}` : "");
63
+ /**
64
+ * Exact distances for a group of pieces: breadth-first from the goal —
65
+ * the pieces at home, or (`goals`) every given placement of the group at once.
66
+ */
67
+ function groupTable(pieces, goals, moves) {
68
+ const key = groupKey(pieces, goals, moves);
69
+ const usable = moves ?? Array.from({ length: N_MOVES }, (_, m) => m);
70
+ const cached = tableCache.get(key);
71
+ if (cached)
72
+ return cached;
73
+ const k = pieces.length;
74
+ const size = 24 ** k;
75
+ const dist = new Int8Array(size).fill(-1);
76
+ const queue = new Int32Array(size);
77
+ const tables = pieces.map((p) => (p.kind === "edge" ? EDGE_NEXT : CORNER_NEXT));
78
+ const scales = pieces.map((_, i) => 24 ** i);
79
+ const starts = (goals ?? [pieces.map(home)]).map((vals) => vals.reduce((acc, v, i) => acc + v * scales[i], 0));
80
+ let head = 0, tail = 0;
81
+ for (const start of starts) {
82
+ if (dist[start] === 0)
83
+ continue;
84
+ dist[start] = 0;
85
+ queue[tail++] = start;
86
+ }
87
+ const vals = new Int32Array(k);
88
+ while (head < tail) {
89
+ const s = queue[head++];
90
+ let t = s;
91
+ for (let i = 0; i < k; i++) {
92
+ const q = (t / 24) | 0;
93
+ vals[i] = t - q * 24;
94
+ t = q;
95
+ }
96
+ const d = dist[s] + 1;
97
+ for (const m of usable) {
98
+ // Only the pieces a move touches change the index: add their differences.
99
+ let idx = s;
100
+ for (let i = 0; i < k; i++) {
101
+ const v = vals[i];
102
+ const nv = tables[i][v * N_MOVES + m];
103
+ if (nv !== v)
104
+ idx += (nv - v) * scales[i];
105
+ }
106
+ if (dist[idx] < 0) {
107
+ dist[idx] = d;
108
+ queue[tail++] = idx;
109
+ }
110
+ }
111
+ }
112
+ tableCache.set(key, dist);
113
+ return dist;
114
+ }
115
+ let flipTable = null;
116
+ function flipDistances() {
117
+ if (flipTable)
118
+ return flipTable;
119
+ const dist = new Int8Array(2048).fill(-1);
120
+ const queue = [0];
121
+ dist[0] = 0;
122
+ for (let h = 0; h < queue.length; h++) {
123
+ const f = queue[h];
124
+ for (let m = 0; m < N_MOVES; m++) {
125
+ const g = FLIP_NEXT[f * N_MOVES + m];
126
+ if (dist[g] < 0) {
127
+ dist[g] = dist[f] + 1;
128
+ queue.push(g);
129
+ }
130
+ }
131
+ }
132
+ return (flipTable = dist);
133
+ }
134
+ // ─── stages ───
135
+ const E = (id) => ({ kind: "edge", id });
136
+ const C = (id) => ({ kind: "corner", id });
137
+ const CROSS = [E(4), E(5), E(6), E(7)]; // DR DF DL DB
138
+ const SLOT = {
139
+ FR: { edge: E(8), corner: C(4) },
140
+ FL: { edge: E(9), corner: C(5) },
141
+ BL: { edge: E(10), corner: C(6) },
142
+ BR: { edge: E(11), corner: C(7) },
143
+ };
144
+ export const STAGES = {
145
+ cross: () => ({ name: "cross", pieces: CROSS, groups: [[0, 1, 2, 3]] }),
146
+ eocross: () => ({ name: "eocross", pieces: CROSS, eo: true, groups: [[0, 1, 2, 3]] }),
147
+ xcross: (slot = "FR") => ({
148
+ name: `xcross-${slot}`,
149
+ pieces: [...CROSS, SLOT[slot].edge, SLOT[slot].corner],
150
+ groups: [[0, 1, 2, 3, 4], [0, 1, 2, 3, 5]],
151
+ }),
152
+ xxcross: (a = "FR", b = "FL") => ({
153
+ name: `xxcross-${a}-${b}`,
154
+ pieces: [...CROSS, SLOT[a].edge, SLOT[a].corner, SLOT[b].edge, SLOT[b].corner],
155
+ groups: [[0, 1, 2, 3, 4], [0, 1, 2, 3, 5], [0, 1, 2, 3, 6], [0, 1, 2, 3, 7]],
156
+ }),
157
+ /** One F2L slot on its own (edge + corner in place). */
158
+ slot: (slot = "FR") => ({ name: `slot-${slot}`, pieces: [SLOT[slot].edge, SLOT[slot].corner], groups: [[0, 1]] }),
159
+ /** Roux first block: DL, FL, BL edges and the DLF, DBL corners. */
160
+ "roux-fb": () => ({ name: "roux-fb", pieces: [E(6), E(9), E(10), C(5), C(6)], groups: [[0, 1, 2, 3, 4]] }),
161
+ /**
162
+ * Both Roux blocks (first + second: DR, FR, BR edges, DFR, DRB corners) —
163
+ * for second-block practice from a solved first block. From a full
164
+ * scramble the optimum is long (~15+) and the search slow.
165
+ */
166
+ "roux-blocks": () => ({
167
+ name: "roux-blocks",
168
+ pieces: [E(6), E(9), E(10), C(5), C(6), E(4), E(8), E(11), C(4), C(7)],
169
+ groups: [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]],
170
+ }),
171
+ };
172
+ /** Move indices a stage may use (`def.moves`; all 18 by default). */
173
+ function allowedMoves(def) {
174
+ return MOVES.map((m, i) => (!def.moves || def.moves.includes(m.family) ? i : -1)).filter((i) => i >= 0);
175
+ }
176
+ /** Values of all the stage's pieces in each of its goal states (`def.goals`), or undefined for "at home". */
177
+ function goalPlacements(def) {
178
+ return def.goals?.map((alg) => {
179
+ let vals = def.pieces.map(home);
180
+ for (const mv of parseAlg(alg)) {
181
+ const m = MOVES.findIndex((x) => x.family === mv.family && x.amount === mv.amount);
182
+ if (m < 0)
183
+ throw new Error(`Stage goals take face turns only: ${alg}`);
184
+ vals = vals.map((v, i) => next(def.pieces[i], v, m));
185
+ }
186
+ return vals;
187
+ });
188
+ }
189
+ export class StageSolver {
190
+ def;
191
+ tables;
192
+ goal;
193
+ /** Goal placements (values of all pieces), when the stage has several (`def.goals`). */
194
+ goalSet;
195
+ /** Move indices the search may use (`def.moves`; the tables stay the all-moves ones — still a valid lower bound). */
196
+ allowed;
197
+ constructor(def) {
198
+ this.def = def;
199
+ this.goal = def.pieces.map(home);
200
+ const goals = goalPlacements(def);
201
+ this.goalSet = goals ? new Set(goals.map((g) => g.join(","))) : null;
202
+ this.allowed = allowedMoves(def);
203
+ // With restricted moves (ZZ: R U L) the tables are built with those moves too: exact for the group, a much better bound.
204
+ this.tables = def.groups.map((g) => groupTable(g.map((i) => def.pieces[i]), goals?.map((vals) => g.map((i) => vals[i])), def.moves ? this.allowed : undefined));
205
+ if (def.eo)
206
+ flipDistances();
207
+ }
208
+ isGoal(vals) {
209
+ return this.goalSet ? this.goalSet.has(vals.join(",")) : vals.every((v, i) => v === this.goal[i]);
210
+ }
211
+ /** Values of the tracked pieces (and flip) in `state`, seen through `frame`. */
212
+ read(state, frame) {
213
+ const c = toCubies(reframe(state, frame));
214
+ if (!c)
215
+ return null;
216
+ const vals = this.def.pieces.map((p) => {
217
+ if (p.kind === "edge") {
218
+ const pos = c.ep.indexOf(p.id);
219
+ return pos * 2 + c.eo[pos];
220
+ }
221
+ const pos = c.cp.indexOf(p.id);
222
+ return pos * 3 + c.co[pos];
223
+ });
224
+ const flip = c.eo.slice(0, 11).reduce((a, x) => a * 2 + x, 0);
225
+ // Pieces are read relative to the centres (after slices / rotations they're not at home): solutions
226
+ // come back in that frame and have to be said in the cube's own terms.
227
+ return { vals, flip, centres: c.frame };
228
+ }
229
+ h(vals, flip) {
230
+ let best = this.def.eo ? flipTable[flip] : 0;
231
+ this.def.groups.forEach((g, gi) => {
232
+ let idx = 0, scale = 1;
233
+ for (const i of g) {
234
+ idx += vals[i] * scale;
235
+ scale *= 24;
236
+ }
237
+ const d = this.tables[gi][idx];
238
+ if (d < 0)
239
+ best = 99; // unreachable with the stage's moves (e.g. a bad edge with R U L only)
240
+ else if (d > best)
241
+ best = d;
242
+ });
243
+ return best;
244
+ }
245
+ /** Solutions of length exactly `depth` from values (all, or stop at the first). */
246
+ search(vals, flip, depth, all, limit) {
247
+ const found = [];
248
+ const path = [];
249
+ const k = vals.length;
250
+ const stack = Array.from({ length: depth + 1 }, () => new Array(k).fill(0));
251
+ stack[0] = [...vals];
252
+ const rec = (d, remaining, f, last) => {
253
+ const cur = stack[d];
254
+ if (remaining === 0) {
255
+ if (this.isGoal(cur) && (!this.def.eo || f === 0)) {
256
+ found.push([...path]);
257
+ return !all || found.length >= limit;
258
+ }
259
+ return false;
260
+ }
261
+ if (this.h(cur, f) > remaining)
262
+ return false;
263
+ const nextVals = stack[d + 1];
264
+ for (const m of this.allowed) {
265
+ const face = Math.floor(m / 3);
266
+ if (last >= 0 && (face === last || face === last - 3))
267
+ continue;
268
+ for (let i = 0; i < k; i++)
269
+ nextVals[i] = next(this.def.pieces[i], cur[i], m);
270
+ path.push(m);
271
+ if (rec(d + 1, remaining - 1, this.def.eo ? FLIP_NEXT[f * N_MOVES + m] : 0, face))
272
+ return true;
273
+ path.pop();
274
+ }
275
+ return false;
276
+ };
277
+ rec(0, depth, flip, -1);
278
+ return found;
279
+ }
280
+ optimal(vals, flip, maxDepth, all, limit) {
281
+ for (let d = this.h(vals, flip); d <= maxDepth; d++) {
282
+ const s = this.search(vals, flip, d, all, limit);
283
+ if (s.length)
284
+ return s;
285
+ }
286
+ return [];
287
+ }
288
+ /**
289
+ * The frames to try: `frame`, or for an x-neutral stage every frame with
290
+ * the same L and R (the block on the L colour with any bottom colour).
291
+ */
292
+ frames(frame) {
293
+ return this.def.neutral === "x" ? FRAMES.filter((f) => f.face.L === frame.face.L && f.face.R === frame.face.R) : [frame];
294
+ }
295
+ distanceIn(state, frame, maxDepth) {
296
+ const r = this.read(state, frame);
297
+ if (!r)
298
+ return -1;
299
+ const s = this.optimal(r.vals, r.flip, maxDepth, false, 1);
300
+ return s.length ? s[0].length : -1;
301
+ }
302
+ /** Fewest moves to finish the stage (−1 if not found within maxDepth). An x-neutral stage takes the best block. */
303
+ distance(state, options = {}) {
304
+ const ds = this.frames(options.frame ?? IDENTITY_FRAME)
305
+ .map((f) => this.distanceIn(state, f, options.maxDepth ?? 20))
306
+ .filter((d) => d >= 0);
307
+ return ds.length ? Math.min(...ds) : -1;
308
+ }
309
+ /** Optimal solutions (face turns, in the cube's own frame); for an x-neutral stage, those of the best blocks. */
310
+ solve(state, options = {}) {
311
+ const frames = this.frames(options.frame ?? IDENTITY_FRAME);
312
+ const best = frames.length > 1 ? this.distance(state, options) : -1;
313
+ const out = [];
314
+ const seen = new Set();
315
+ for (const frame of frames) {
316
+ if (frames.length > 1 && this.distanceIn(state, frame, options.maxDepth ?? 20) !== best)
317
+ continue;
318
+ const r = this.read(state, frame);
319
+ if (!r)
320
+ continue;
321
+ const sols = this.optimal(r.vals, r.flip, options.maxDepth ?? 20, options.all ?? false, options.limit ?? 256);
322
+ for (const sol of sols) {
323
+ let moves = sol.map((m) => MOVES[m]);
324
+ if (r.centres !== IDENTITY_FRAME)
325
+ moves = transformMoves(moves, r.centres);
326
+ if (frame !== IDENTITY_FRAME)
327
+ moves = transformMoves(moves, frame);
328
+ const key = moves.map((m) => `${m.family}${m.amount}`).join(" ");
329
+ if (seen.has(key))
330
+ continue;
331
+ seen.add(key);
332
+ out.push(moves);
333
+ }
334
+ if (!options.all && out.length)
335
+ break;
336
+ }
337
+ return out;
338
+ }
339
+ /**
340
+ * A whole random state whose stage takes exactly `depth` moves — for
341
+ * x-neutral stages, where the best block depends on pieces outside the
342
+ * tracked set. Uniform where levels are common; short levels come from
343
+ * random walks, the rarest deep ones one move beyond a shallower case.
344
+ */
345
+ sampleState(depth, random = Math.random, draw = () => randomStateHook(random), attempts = 3000) {
346
+ for (let a = 0; a < attempts; a++) {
347
+ const s = draw(random);
348
+ if (this.distance(s, { maxDepth: depth }) === depth)
349
+ return s;
350
+ }
351
+ const faceMoves = MOVES;
352
+ for (let a = 0; a < attempts; a++) {
353
+ let s = SOLVED_STATE;
354
+ let last = -1;
355
+ for (let d = 0; d < depth + 2; d++) {
356
+ let m;
357
+ do
358
+ m = Math.floor(random() * N_MOVES);
359
+ while (last >= 0 && Math.floor(m / 3) === last);
360
+ last = Math.floor(m / 3);
361
+ s = applyMoves(s, [faceMoves[m]]);
362
+ if (d + 1 >= depth && this.distance(s, { maxDepth: depth }) === depth)
363
+ return s;
364
+ }
365
+ }
366
+ if (depth > 1) {
367
+ for (let a = 0; a < 50; a++) {
368
+ const shallower = this.sampleState(depth - 1, random, draw, 300);
369
+ if (!shallower)
370
+ break;
371
+ for (const m of [...faceMoves].sort(() => random() - 0.5)) {
372
+ const s = applyMoves(shallower, [m]);
373
+ if (this.distance(s, { maxDepth: depth }) === depth)
374
+ return s;
375
+ }
376
+ }
377
+ }
378
+ return null;
379
+ }
380
+ /** The first moves of every optimal solution — "what's a best next move from here?" */
381
+ nextMoves(state, options = {}) {
382
+ const seen = new Map();
383
+ for (const s of this.solve(state, { ...options, all: true }))
384
+ if (s[0])
385
+ seen.set(`${s[0].family}${s[0].amount}`, s[0]);
386
+ return [...seen.values()];
387
+ }
388
+ /**
389
+ * A random placement of the tracked pieces that takes exactly `depth` moves
390
+ * (canonical frame): piece → value (pos × orientations + ori), plus the
391
+ * flip coordinate of all edges for EO stages. Uniform rejection sampling;
392
+ * very short depths fall back to random walks.
393
+ */
394
+ sample(depth, random = Math.random, attempts = 4000) {
395
+ const evaluate = (vals, flip) => {
396
+ const s = this.optimal(vals, flip, depth, false, 1);
397
+ return s.length ? s[0].length : Infinity;
398
+ };
399
+ const placement = (vals, flip) => ({
400
+ pieces: new Map(this.def.pieces.map((p, i) => [p, vals[i]])),
401
+ ...(this.def.eo ? { flip } : {}),
402
+ });
403
+ // Known cases for levels too rare to draw at run time (see StageDef.seeds) — straight away.
404
+ const seeds = this.def.seeds?.[depth];
405
+ if (seeds?.length) {
406
+ const [valsText, flipText] = seeds[Math.floor(random() * seeds.length)].split(":");
407
+ const vals = valsText.split(".").map(Number);
408
+ const flip = Number(flipText ?? 0);
409
+ if (evaluate(vals, flip) === depth)
410
+ return placement(vals, flip);
411
+ }
412
+ for (let a = 0; a < attempts; a++) {
413
+ // Uniform placement: distinct positions, random orientations.
414
+ const keep = new Set(this.def.keep ?? []);
415
+ const usedE = new Set(this.def.pieces.filter((p, i) => keep.has(i) && p.kind === "edge").map((p) => p.id));
416
+ const usedC = new Set(this.def.pieces.filter((p, i) => keep.has(i) && p.kind === "corner").map((p) => p.id));
417
+ const vals = this.def.pieces.map((p, i) => {
418
+ if (keep.has(i))
419
+ return home(p); // kept solved (e.g. the first block while practising the second square)
420
+ const n = p.kind === "edge" ? 12 : 8, used = p.kind === "edge" ? usedE : usedC;
421
+ let pos;
422
+ do
423
+ pos = Math.floor(random() * n);
424
+ while (used.has(pos));
425
+ used.add(pos);
426
+ return pos * (p.kind === "edge" ? 2 : 3) + Math.floor(random() * (p.kind === "edge" ? 2 : 3));
427
+ });
428
+ const flip = this.def.eo ? Math.floor(random() * 2048) : 0;
429
+ if (this.def.eo) {
430
+ // The tracked edges' orientations are part of the flip: take them from it.
431
+ const eo = flipBits(flip);
432
+ this.def.pieces.forEach((p, i) => {
433
+ if (p.kind === "edge")
434
+ vals[i] = (vals[i] >> 1) * 2 + eo[vals[i] >> 1];
435
+ });
436
+ }
437
+ if (evaluate(vals, flip) === depth)
438
+ return placement(vals, flip);
439
+ }
440
+ // Short depths are rare among uniform placements: walk away from solved instead.
441
+ for (let a = 0; a < attempts; a++) {
442
+ let vals = [...this.goal];
443
+ let flip = 0;
444
+ let last = -1;
445
+ for (let d = 0; d < depth; d++) {
446
+ let m;
447
+ do
448
+ m = Math.floor(random() * N_MOVES);
449
+ while (last >= 0 && (Math.floor(m / 3) === last || Math.floor(m / 3) === last - 3));
450
+ last = Math.floor(m / 3);
451
+ vals = vals.map((v, i) => next(this.def.pieces[i], v, m));
452
+ flip = FLIP_NEXT[flip * N_MOVES + m];
453
+ }
454
+ if (evaluate(vals, this.def.eo ? flip : 0) === depth)
455
+ return placement(vals, this.def.eo ? flip : 0);
456
+ }
457
+ // The deepest levels can be too rare to hit (EOCross at 10): every such placement is one move beyond a
458
+ // placement one shallower, so step outwards from those (fine as a trainer case, not exactly uniform).
459
+ if (depth > 1) {
460
+ for (let a = 0; a < 200; a++) {
461
+ const shallower = this.sample(depth - 1, random, 200);
462
+ if (!shallower)
463
+ break;
464
+ const vals0 = this.def.pieces.map((p) => shallower.pieces.get(p));
465
+ const flip0 = shallower.flip ?? 0;
466
+ const order = Array.from({ length: N_MOVES }, (_, i) => i).sort(() => random() - 0.5);
467
+ for (const m of order) {
468
+ const vals = vals0.map((v, i) => next(this.def.pieces[i], v, m));
469
+ const flip = this.def.eo ? FLIP_NEXT[flip0 * N_MOVES + m] : 0;
470
+ if (evaluate(vals, flip) === depth)
471
+ return placement(vals, flip);
472
+ }
473
+ }
474
+ }
475
+ return null;
476
+ }
477
+ }
478
+ /** Orientation of the edge at each position (0..11) for a flip coordinate. */
479
+ export function flipBits(flip) {
480
+ const eo = new Array(12).fill(0);
481
+ for (let i = 10, t = flip; i >= 0; i--, t >>= 1)
482
+ eo[i] = t & 1;
483
+ eo[11] = eo.reduce((a, x) => a + x, 0) % 2;
484
+ return eo;
485
+ }
486
+ /** Load the tables these stages need from `store`, or build and save them. Afterwards the solvers start instantly. */
487
+ export async function preloadStageTables(stages, store) {
488
+ for (const def of stages) {
489
+ const goals = goalPlacements(def);
490
+ for (const g of def.groups) {
491
+ const pieces = g.map((i) => def.pieces[i]);
492
+ const groupGoals = goals?.map((vals) => g.map((i) => vals[i]));
493
+ const moves = def.moves ? allowedMoves(def) : undefined;
494
+ const key = groupKey(pieces, groupGoals, moves);
495
+ if (tableCache.has(key))
496
+ continue;
497
+ const saved = await store.get(key).catch(() => null);
498
+ if (saved && saved.length === 24 ** pieces.length)
499
+ tableCache.set(key, saved);
500
+ else
501
+ await store.set(key, groupTable(pieces, groupGoals, moves)).catch(() => undefined);
502
+ }
503
+ }
504
+ }
505
+ /** A TableStore in IndexedDB (works in pages and workers). */
506
+ export function indexedDbTableStore(dbName = "cubecore-tables") {
507
+ const db = new Promise((resolve, reject) => {
508
+ const req = indexedDB.open(dbName, 1);
509
+ req.onupgradeneeded = () => req.result.createObjectStore("tables");
510
+ req.onsuccess = () => resolve(req.result);
511
+ req.onerror = () => reject(req.error);
512
+ });
513
+ const run = (mode, fn) => db.then((d) => new Promise((resolve, reject) => {
514
+ const r = fn(d.transaction("tables", mode).objectStore("tables"));
515
+ r.onsuccess = () => resolve(r.result);
516
+ r.onerror = () => reject(r.error);
517
+ }));
518
+ return {
519
+ get: (key) => run("readonly", (s) => s.get(key)).then((b) => (b ? new Int8Array(b) : null)),
520
+ set: (key, table) => run("readwrite", (s) => s.put(table.buffer.slice(0), key)).then(() => undefined),
521
+ };
522
+ }
523
+ const solvers = new Map();
524
+ /** A shared solver for a stage definition (tables are shared anyway). */
525
+ export function stageSolver(def) {
526
+ // Keyed by the whole definition: two stages may share a name (a neutral and a fixed first block).
527
+ const key = JSON.stringify(def);
528
+ let s = solvers.get(key);
529
+ if (!s)
530
+ solvers.set(key, (s = new StageSolver(def)));
531
+ return s;
532
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Kociemba's two-phase algorithm, in TypeScript — solves any state in about
3
+ * 20 face turns, fast enough to make random-state scrambles.
4
+ *
5
+ * Phase 1 brings the cube into G1 = ⟨U, D, R2, L2, F2, B2⟩ (all twists and
6
+ * flips 0, the four middle-layer edges in the middle layer); phase 2 solves
7
+ * it using only G1 moves. Both are IDA* searches over small coordinates with
8
+ * pruning tables:
9
+ *
10
+ * phase 1: twist (3^7), flip (2^11), slice position (C(12,4) = 495)
11
+ * phase 2: corner permutation (8!), U/D edge permutation (8!), slice permutation (4!)
12
+ *
13
+ * Tables (~9 MB, move + pruning) are built once, on first use (about a
14
+ * second); build them early with `TwoPhase.create()` or in a worker.
15
+ * Pieces use the Kociemba numbering of core `cubies.ts`.
16
+ */
17
+ import { type Move, type State } from "../core/index.js";
18
+ import { MOVES } from "./pieces.js";
19
+ export { MOVES };
20
+ export interface SolveOptions {
21
+ /** Longest acceptable solution (face turns, HTM). Default 21. */
22
+ maxLength?: number;
23
+ /** Give up after this long (ms); returns null if nothing was found by then. Default 5000. */
24
+ timeoutMs?: number;
25
+ }
26
+ export declare class TwoPhase {
27
+ private readonly t;
28
+ private constructor();
29
+ /** Build the tables (about a second) — do it once, early, or in a worker. */
30
+ static create(): TwoPhase;
31
+ /** A solution for `state` (face turns, applied to `state` they solve it), or null. */
32
+ solve(state: State, options?: SolveOptions): Move[] | null;
33
+ /** Moves taking `from` to `to` (e.g. from the cube's current state to a scramble's target). */
34
+ solveBetween(from: State, to: State, options?: SolveOptions): Move[] | null;
35
+ private search;
36
+ }