@cubingcenter/scrambler 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +17 -4
  2. package/dist/core/api.d.ts.map +1 -1
  3. package/dist/core/api.js +10 -2
  4. package/dist/puzzles/cube2/solver/browser-loader.d.ts +4 -7
  5. package/dist/puzzles/cube2/solver/browser-loader.d.ts.map +1 -1
  6. package/dist/puzzles/cube2/solver/browser-loader.js +42 -14
  7. package/dist/puzzles/cube2/solver/generated-assets.d.ts.map +1 -1
  8. package/dist/puzzles/cube2/solver/generated-assets.js +8 -3
  9. package/dist/puzzles/cube2/solver/search.d.ts.map +1 -1
  10. package/dist/puzzles/cube2/solver/search.js +3 -3
  11. package/dist/puzzles/cube2/solver/tables.d.ts +6 -0
  12. package/dist/puzzles/cube2/solver/tables.d.ts.map +1 -1
  13. package/dist/puzzles/cube2/solver/tables.js +16 -2
  14. package/dist/puzzles/cube3/index.d.ts +1 -0
  15. package/dist/puzzles/cube3/index.d.ts.map +1 -1
  16. package/dist/puzzles/cube3/moves.d.ts.map +1 -1
  17. package/dist/puzzles/cube3/moves.js +104 -18
  18. package/dist/puzzles/cube3/scrambler.d.ts +16 -1
  19. package/dist/puzzles/cube3/scrambler.d.ts.map +1 -1
  20. package/dist/puzzles/cube3/scrambler.js +1 -1
  21. package/dist/puzzles/cube3/solver/browser-loader.d.ts.map +1 -1
  22. package/dist/puzzles/cube3/solver/browser-loader.js +48 -22
  23. package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
  24. package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
  25. package/dist/puzzles/cube3/solver/index.d.ts +5 -3
  26. package/dist/puzzles/cube3/solver/index.d.ts.map +1 -1
  27. package/dist/puzzles/cube3/solver/index.js +2 -1
  28. package/dist/puzzles/cube3/solver/node-loader.js +9 -2
  29. package/dist/puzzles/cube3/solver/phase1.d.ts +37 -0
  30. package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
  31. package/dist/puzzles/cube3/solver/phase1.js +156 -15
  32. package/dist/puzzles/cube3/solver/phase2.d.ts +75 -0
  33. package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
  34. package/dist/puzzles/cube3/solver/phase2.js +279 -68
  35. package/dist/puzzles/cube3/solver/search-utils.d.ts +1 -1
  36. package/dist/puzzles/cube3/solver/search-utils.d.ts.map +1 -1
  37. package/dist/puzzles/cube3/solver/symmetry.d.ts +24 -0
  38. package/dist/puzzles/cube3/solver/symmetry.d.ts.map +1 -0
  39. package/dist/puzzles/cube3/solver/symmetry.js +74 -0
  40. package/dist/puzzles/cube3/solver/tables.d.ts +6 -1
  41. package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
  42. package/dist/puzzles/cube3/solver/tables.js +14 -12
  43. package/dist/puzzles/cube3/validation.d.ts.map +1 -1
  44. package/dist/puzzles/cube3/validation.js +15 -7
  45. package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
  46. package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
  47. package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
  48. package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
  49. package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
  50. package/dist/puzzles/cube4/browser-worker.js +31 -0
  51. package/dist/puzzles/cube4/index.d.ts +10 -2
  52. package/dist/puzzles/cube4/index.d.ts.map +1 -1
  53. package/dist/puzzles/cube4/index.js +6 -2
  54. package/dist/puzzles/cube4/moves.d.ts +5 -0
  55. package/dist/puzzles/cube4/moves.d.ts.map +1 -1
  56. package/dist/puzzles/cube4/moves.js +212 -11
  57. package/dist/puzzles/cube4/reduction/browser-loader.d.ts +29 -1
  58. package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
  59. package/dist/puzzles/cube4/reduction/browser-loader.js +227 -32
  60. package/dist/puzzles/cube4/reduction/center2.d.ts +44 -3
  61. package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
  62. package/dist/puzzles/cube4/reduction/center2.js +152 -10
  63. package/dist/puzzles/cube4/reduction/edge3.d.ts +4 -0
  64. package/dist/puzzles/cube4/reduction/edge3.d.ts.map +1 -1
  65. package/dist/puzzles/cube4/reduction/edge3.js +23 -3
  66. package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
  67. package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
  68. package/dist/puzzles/cube4/reduction/finish3.js +393 -0
  69. package/dist/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
  70. package/dist/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
  71. package/dist/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
  72. package/dist/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
  73. package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +44 -0
  74. package/dist/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
  75. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +52 -0
  76. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -0
  77. package/dist/puzzles/cube4/reduction/hc-pdb.js +220 -0
  78. package/dist/puzzles/cube4/reduction/node-loader.d.ts +15 -1
  79. package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
  80. package/dist/puzzles/cube4/reduction/node-loader.js +199 -25
  81. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
  82. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
  83. package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
  84. package/dist/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
  85. package/dist/puzzles/cube4/reduction/phase3-search.d.ts +116 -7
  86. package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
  87. package/dist/puzzles/cube4/reduction/phase3-search.js +458 -32
  88. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
  89. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
  90. package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
  91. package/dist/puzzles/cube4/reduction/solve.d.ts +172 -2
  92. package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
  93. package/dist/puzzles/cube4/reduction/solve.js +1201 -180
  94. package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
  95. package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
  96. package/dist/puzzles/cube4/scramble-cache.js +150 -0
  97. package/dist/puzzles/cube4/scrambler.d.ts +201 -6
  98. package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
  99. package/dist/puzzles/cube4/scrambler.js +414 -176
  100. package/dist/puzzles/cube4/state.d.ts.map +1 -1
  101. package/dist/puzzles/cube4/state.js +4 -1
  102. package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
  103. package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
  104. package/dist/puzzles/cube4/worker-pool.js +405 -0
  105. package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
  106. package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
  107. package/dist/puzzles/cube4/worker-protocol.js +58 -0
  108. package/dist/puzzles/cube4/worker.d.ts +26 -0
  109. package/dist/puzzles/cube4/worker.d.ts.map +1 -0
  110. package/dist/puzzles/cube4/worker.js +49 -0
  111. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
  112. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
  113. package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
  114. package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
  115. package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
  116. package/dist/puzzles/pyraminx/solver/search.d.ts.map +1 -1
  117. package/dist/puzzles/pyraminx/solver/search.js +3 -6
  118. package/dist/puzzles/pyraminx/solver/tables.d.ts +6 -0
  119. package/dist/puzzles/pyraminx/solver/tables.d.ts.map +1 -1
  120. package/dist/puzzles/pyraminx/solver/tables.js +17 -0
  121. package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
  122. package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
  123. package/dist/puzzles/shared/compressed-fetch.js +226 -31
  124. package/dist/puzzles/shared/load-once.d.ts +16 -0
  125. package/dist/puzzles/shared/load-once.d.ts.map +1 -0
  126. package/dist/puzzles/shared/load-once.js +11 -0
  127. package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
  128. package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
  129. package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
  130. package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
  131. package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
  132. package/dist/puzzles/skewb/solver/search.js +3 -3
  133. package/dist/puzzles/skewb/solver/tables.d.ts +6 -0
  134. package/dist/puzzles/skewb/solver/tables.d.ts.map +1 -1
  135. package/dist/puzzles/skewb/solver/tables.js +17 -0
  136. package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
  137. package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
  138. package/dist-cjs/core/api.js +10 -2
  139. package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
  140. package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
  141. package/dist-cjs/puzzles/cube2/solver/search.js +2 -2
  142. package/dist-cjs/puzzles/cube2/solver/tables.js +17 -3
  143. package/dist-cjs/puzzles/cube3/moves.js +104 -18
  144. package/dist-cjs/puzzles/cube3/scrambler.js +1 -1
  145. package/dist-cjs/puzzles/cube3/solver/browser-loader.js +48 -22
  146. package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
  147. package/dist-cjs/puzzles/cube3/solver/index.js +8 -1
  148. package/dist-cjs/puzzles/cube3/solver/node-loader.js +9 -2
  149. package/dist-cjs/puzzles/cube3/solver/phase1.js +157 -15
  150. package/dist-cjs/puzzles/cube3/solver/phase2.js +276 -65
  151. package/dist-cjs/puzzles/cube3/solver/symmetry.js +80 -0
  152. package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
  153. package/dist-cjs/puzzles/cube3/validation.js +15 -7
  154. package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
  155. package/dist-cjs/puzzles/cube4/index.js +23 -1
  156. package/dist-cjs/puzzles/cube4/moves.js +213 -11
  157. package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +227 -32
  158. package/dist-cjs/puzzles/cube4/reduction/center2.js +155 -10
  159. package/dist-cjs/puzzles/cube4/reduction/edge3.js +24 -3
  160. package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
  161. package/dist-cjs/puzzles/cube4/reduction/generated/c3_pair_dist.bin +1 -0
  162. package/dist-cjs/puzzles/cube4/reduction/generated/center2-manifest.json +36 -0
  163. package/dist-cjs/puzzles/cube4/reduction/generated/center2-pruning-wide.bin +1 -0
  164. package/dist-cjs/puzzles/cube4/reduction/generated/hc-distances.bin +1 -0
  165. package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +44 -0
  166. package/dist-cjs/puzzles/cube4/reduction/generated/hc-move.bin +0 -0
  167. package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +232 -0
  168. package/dist-cjs/puzzles/cube4/reduction/node-loader.js +200 -25
  169. package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
  170. package/dist-cjs/puzzles/cube4/reduction/pairing-coordinate.js +5 -5
  171. package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +460 -33
  172. package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
  173. package/dist-cjs/puzzles/cube4/reduction/solve.js +1203 -172
  174. package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
  175. package/dist-cjs/puzzles/cube4/scrambler.js +416 -174
  176. package/dist-cjs/puzzles/cube4/state.js +4 -1
  177. package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
  178. package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
  179. package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
  180. package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
  181. package/dist-cjs/puzzles/pyraminx/solver/search.js +2 -5
  182. package/dist-cjs/puzzles/pyraminx/solver/tables.js +18 -1
  183. package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
  184. package/dist-cjs/puzzles/shared/load-once.js +14 -0
  185. package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
  186. package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
  187. package/dist-cjs/puzzles/skewb/solver/search.js +2 -2
  188. package/dist-cjs/puzzles/skewb/solver/tables.js +18 -1
  189. package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
  190. package/docs/development.md +19 -7
  191. package/package.json +17 -7
@@ -2,6 +2,7 @@ import { fetchTableBuffer } from "../../shared/compressed-fetch.js";
2
2
  const TABLES_DIR = "puzzles/cube3/solver/generated";
3
3
  const CACHE_NAME = "cubingcenter-scrambler-tables-v1";
4
4
  let browserTables;
5
+ let browserTablesPromise;
5
6
  function resolveBaseUrl(provided) {
6
7
  if (provided) {
7
8
  return provided.replace(/\/$/, "");
@@ -20,11 +21,28 @@ export function getCachedBrowserTables() {
20
21
  }
21
22
  export function clearCachedBrowserTables() {
22
23
  browserTables = undefined;
24
+ browserTablesPromise = undefined;
23
25
  }
24
26
  export async function loadBrowserTables(baseUrl) {
25
27
  if (browserTables) {
26
28
  return browserTables;
27
29
  }
30
+ if (browserTablesPromise) {
31
+ // One shared in-flight promise: concurrent calls trigger exactly one
32
+ // fetch pass (dedup).
33
+ return browserTablesPromise;
34
+ }
35
+ const promise = fetchTables(baseUrl);
36
+ browserTablesPromise = promise;
37
+ promise.then(() => { }, () => {
38
+ // A failed load must not poison the dedup slot: purge the rejected
39
+ // promise so a later call retries from scratch.
40
+ if (browserTablesPromise === promise)
41
+ browserTablesPromise = undefined;
42
+ });
43
+ return promise;
44
+ }
45
+ async function fetchTables(baseUrl) {
28
46
  const resolvedBase = resolveBaseUrl(baseUrl);
29
47
  const manifest = await fetchManifest(resolvedBase);
30
48
  browserTables = {
@@ -58,19 +76,15 @@ async function fetchJointTables(baseUrl, entries) {
58
76
  return undefined;
59
77
  }
60
78
  const fetchJoint = async (entry) => {
79
+ // Verified by fetchTableBuffer with the SAME checks as node-loader.ts:
80
+ // nibble-packed byte length + FNV-1a over the packed bytes.
61
81
  const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, {
62
82
  cacheName: CACHE_NAME,
63
- compressed: entry.compressed !== false
83
+ compressed: entry.compressed !== false,
84
+ expectedBytes: Math.ceil((entry.rangeA * entry.rangeB) / 2),
85
+ expectedFnv1a: { value: entry.checksum, elementSize: 1 }
64
86
  });
65
- const expectedBytes = Math.ceil((entry.rangeA * entry.rangeB) / 2);
66
- if (buffer.byteLength !== expectedBytes) {
67
- throw new Error(`Invalid byte size for ${entry.file}.`);
68
- }
69
- const values = new Uint8Array(buffer);
70
- if (checksum(values) !== entry.checksum) {
71
- throw new Error(`Checksum mismatch for ${entry.file}.`);
72
- }
73
- return values;
87
+ return new Uint8Array(buffer);
74
88
  };
75
89
  return {
76
90
  jointUdCo: await fetchJoint(entries.udCo),
@@ -90,15 +104,23 @@ async function fetchManifest(baseUrl) {
90
104
  return manifest;
91
105
  }
92
106
  async function fetchMoveTable(baseUrl, entry) {
93
- const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, { cacheName: CACHE_NAME, compressed: entry.compressed !== false });
94
- const expectedBytes = entry.range * entry.moves.length * 4;
95
- if (buffer.byteLength !== expectedBytes) {
96
- throw new Error(`Invalid byte size for ${entry.coordinate} move table.`);
97
- }
98
- const values = new Int32Array(entry.range * entry.moves.length);
107
+ // Verified by fetchTableBuffer with the SAME checks as node-loader.ts:
108
+ // int32 wire length + FNV-1a over the decoded coordinate values (element
109
+ // size 4 reproduces node's hash over the uint16 values — same numbers).
110
+ const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, {
111
+ cacheName: CACHE_NAME,
112
+ compressed: entry.compressed !== false,
113
+ expectedBytes: entry.range * entry.moves.length * 4,
114
+ expectedFnv1a: { value: entry.checksum, elementSize: 4 }
115
+ });
116
+ const values = new Uint16Array(entry.range * entry.moves.length);
99
117
  const view = new DataView(buffer);
100
118
  for (let index = 0; index < values.length; index += 1) {
101
- values[index] = view.getInt32(index * 4, true);
119
+ const value = view.getInt32(index * 4, true);
120
+ if (value < 0 || value > 0xffff) {
121
+ throw new Error(`Move table value out of range for ${entry.coordinate}: ${value}.`);
122
+ }
123
+ values[index] = value;
102
124
  }
103
125
  return {
104
126
  coordinate: entry.coordinate,
@@ -110,11 +132,15 @@ async function fetchMoveTable(baseUrl, entry) {
110
132
  };
111
133
  }
112
134
  async function fetchPruningTable(baseUrl, entry) {
113
- const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, { cacheName: CACHE_NAME, compressed: entry.compressed !== false });
114
- const expectedBytes = Math.ceil(entry.range / 2);
115
- if (buffer.byteLength !== expectedBytes) {
116
- throw new Error(`Invalid byte size for ${entry.coordinate} pruning table.`);
117
- }
135
+ // Verified by fetchTableBuffer with the SAME checks as node-loader.ts:
136
+ // nibble-packed byte length + FNV-1a over the packed bytes (this browser
137
+ // path did NOT compare the pruning checksum before — only the length).
138
+ const buffer = await fetchTableBuffer(baseUrl, TABLES_DIR, entry.file, {
139
+ cacheName: CACHE_NAME,
140
+ compressed: entry.compressed !== false,
141
+ expectedBytes: Math.ceil(entry.range / 2),
142
+ expectedFnv1a: { value: entry.checksum, elementSize: 1 }
143
+ });
118
144
  const values = new Uint8Array(buffer);
119
145
  return {
120
146
  coordinate: entry.coordinate,
@@ -1 +1 @@
1
- {"version":3,"file":"generated-assets.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/generated-assets.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AASrB,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAejF;AAWD,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAkBjF;AAED,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAkBjF"}
1
+ {"version":3,"file":"generated-assets.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/generated-assets.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAgBrB,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAcjF;AAWD,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAkBjF;AAED,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAkBjF"}
@@ -1,8 +1,15 @@
1
1
  import { getCachedBrowserTables } from "./browser-loader.js";
2
- let cachedGeneratedTables;
2
+ import { loadOnce } from "../../shared/load-once.js";
3
3
  function isBrowser() {
4
4
  return typeof self !== "undefined";
5
5
  }
6
+ // One shared in-flight promise for the node fs load: mixed concurrent
7
+ // phase-1 + phase-2 loads (separate slots in tables.ts) converge on ONE
8
+ // `readGeneratedCube3SolverTables()` call. The previous `cached ??= await …`
9
+ // raced — two callers interleaving on the await both ran `loadNodeTables()`
10
+ // (two filesystem reads, two table sets, last assignment wins). On
11
+ // rejection the slot is purged so the next call can retry.
12
+ const nodeTablesSlot = { promise: undefined };
6
13
  export async function loadGeneratedCube3SolverTables() {
7
14
  if (isBrowser()) {
8
15
  const browserTables = getCachedBrowserTables();
@@ -13,8 +20,7 @@ export async function loadGeneratedCube3SolverTables() {
13
20
  "In browser environments, call `initializeScrambler(baseUrl)` " +
14
21
  "before generating scrambles.");
15
22
  }
16
- cachedGeneratedTables ??= await loadNodeTables();
17
- return cachedGeneratedTables;
23
+ return loadOnce(nodeTablesSlot, loadNodeTables);
18
24
  }
19
25
  async function loadNodeTables() {
20
26
  const loader = await import(
@@ -1,10 +1,12 @@
1
1
  export { applyCube3Phase2Moves, encodeCube3Phase2Coordinates, isCube3Phase2Solved, solveCube3Phase2, solveCube3State, Cube3Phase2SearchError } from "./phase2.js";
2
- export { applyCube3Phase1Moves, encodeCube3Phase1Coordinates, isCube3Phase1Solved, solveCube3Phase1, Cube3Phase1SearchError } from "./phase1.js";
2
+ export { applyCube3Phase1Moves, collectCube3Phase1Solutions, encodeCube3Phase1Coordinates, isCube3Phase1Solved, solveCube3Phase1, Cube3Phase1SearchError } from "./phase1.js";
3
+ export { CUBE3_ORIENTATIONS_24, CUBE3_ROTATION_INVERSE, invertCube3RotationSequence, rotateCube3Algorithm, rotateCube3AlgorithmBySequence } from "./symmetry.js";
4
+ export type { Cube3Rotation, Cube3RotationSequence } from "./symmetry.js";
3
5
  export { CUBE3_COORDINATE_RANGES, createRepresentativeStateForCoordinate, decodeCornerOrientation, decodeEdgeOrientation, decodePermutation, decodeSlicePermutation, decodeUdEdgePermutation, decodeUdSlice, encodeCube3Coordinate, encodeCornerOrientation, encodeCube3Coordinates, encodeEdgeOrientation, encodePermutation, encodeSlicePermutation, encodeUdEdgePermutation, encodeUdSlice, moveCoordinate } from "./coordinates.js";
4
6
  export { CUBE3_SOLVER_TABLE_METADATA } from "./table-metadata.js";
5
7
  export { CUBE3_PHASE1_MOVES, CUBE3_PHASE2_MOVES, checksum, generateCube3Phase1Tables, generateCube3Phase2Tables, generateCube3MoveTable, generateCube3PruningTable, generateCube3SolverTables, loadCube3Phase1Tables, loadCube3Phase2Tables, loadCube3SolverTables, lookupCube3MoveTable } from "./tables.js";
6
- export type { Cube3Phase2Coordinates, Cube3Phase2Options, Cube3Phase2Result, Cube3SolveOptions, Cube3SolveResult } from "./phase2.js";
7
- export type { Cube3Phase1Coordinates, Cube3Phase1Options, Cube3Phase1Result } from "./phase1.js";
8
+ export type { Cube3Phase2Coordinates, Cube3Phase2Options, Cube3Phase2Result, Cube3SolveOptions, Cube3SolveQuality, Cube3SolveResult } from "./phase2.js";
9
+ export type { Cube3Phase1CollectOptions, Cube3Phase1Coordinates, Cube3Phase1Options, Cube3Phase1Result } from "./phase1.js";
8
10
  export type { Cube3CoordinateState } from "./coordinates.js";
9
11
  export type { Cube3CoordinateName, Cube3Phase1Tables, Cube3Phase2Tables, Cube3MoveTable, Cube3PruningTable, Cube3SolverTables } from "./tables.js";
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,4BAA4B,EAC5B,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,qBAAqB,EACrB,4BAA4B,EAC5B,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,uBAAuB,EACvB,sCAAsC,EACtC,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,aAAa,EACb,cAAc,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,QAAQ,EACR,yBAAyB,EACzB,yBAAyB,EACzB,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EACzB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,4BAA4B,EAC5B,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAC3B,4BAA4B,EAC5B,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,2BAA2B,EAC3B,oBAAoB,EACpB,8BAA8B,EAC/B,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,uBAAuB,EACvB,sCAAsC,EACtC,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,aAAa,EACb,cAAc,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,QAAQ,EACR,yBAAyB,EACzB,yBAAyB,EACzB,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EACzB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,yBAAyB,EACzB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC"}
@@ -1,5 +1,6 @@
1
1
  export { applyCube3Phase2Moves, encodeCube3Phase2Coordinates, isCube3Phase2Solved, solveCube3Phase2, solveCube3State, Cube3Phase2SearchError } from "./phase2.js";
2
- export { applyCube3Phase1Moves, encodeCube3Phase1Coordinates, isCube3Phase1Solved, solveCube3Phase1, Cube3Phase1SearchError } from "./phase1.js";
2
+ export { applyCube3Phase1Moves, collectCube3Phase1Solutions, encodeCube3Phase1Coordinates, isCube3Phase1Solved, solveCube3Phase1, Cube3Phase1SearchError } from "./phase1.js";
3
+ export { CUBE3_ORIENTATIONS_24, CUBE3_ROTATION_INVERSE, invertCube3RotationSequence, rotateCube3Algorithm, rotateCube3AlgorithmBySequence } from "./symmetry.js";
3
4
  export { CUBE3_COORDINATE_RANGES, createRepresentativeStateForCoordinate, decodeCornerOrientation, decodeEdgeOrientation, decodePermutation, decodeSlicePermutation, decodeUdEdgePermutation, decodeUdSlice, encodeCube3Coordinate, encodeCornerOrientation, encodeCube3Coordinates, encodeEdgeOrientation, encodePermutation, encodeSlicePermutation, encodeUdEdgePermutation, encodeUdSlice, moveCoordinate } from "./coordinates.js";
4
5
  export { CUBE3_SOLVER_TABLE_METADATA } from "./table-metadata.js";
5
6
  export { CUBE3_PHASE1_MOVES, CUBE3_PHASE2_MOVES, checksum, generateCube3Phase1Tables, generateCube3Phase2Tables, generateCube3MoveTable, generateCube3PruningTable, generateCube3SolverTables, loadCube3Phase1Tables, loadCube3Phase2Tables, loadCube3SolverTables, lookupCube3MoveTable } from "./tables.js";
@@ -65,9 +65,16 @@ function readMoveTable(assetDirectory, entry) {
65
65
  if (buffer.length !== expectedBytes) {
66
66
  throw new Error(`Invalid byte size for ${entry.coordinate} move table.`);
67
67
  }
68
- const values = new Int32Array(entry.range * entry.moves.length);
68
+ // Every coordinate fits in 16 bits (max range 40320): store half the
69
+ // bytes the int32 wire format uses. Checksums are order-sensitive FNV
70
+ // over the same numeric sequence, so they still match the manifest.
71
+ const values = new Uint16Array(entry.range * entry.moves.length);
69
72
  for (let index = 0; index < values.length; index += 1) {
70
- values[index] = buffer.readInt32LE(index * 4);
73
+ const value = buffer.readInt32LE(index * 4);
74
+ if (value < 0 || value > 0xffff) {
75
+ throw new Error(`Move table value out of range for ${entry.coordinate}: ${value}.`);
76
+ }
77
+ values[index] = value;
71
78
  }
72
79
  const table = {
73
80
  coordinate: entry.coordinate,
@@ -7,7 +7,20 @@ export interface Cube3Phase1Coordinates {
7
7
  }
8
8
  export interface Cube3Phase1Options {
9
9
  maxDepth?: number;
10
+ /**
11
+ * Legacy wall-clock deadline (checked every 1024 nodes). The stopping
12
+ * point — and therefore the result — depends on machine speed and CPU
13
+ * load. Prefer `maxNodes` when the caller needs a reproducible outcome.
14
+ */
10
15
  timeoutMs?: number;
16
+ /**
17
+ * Deterministic work cap: stop after visiting this many nodes (checked on
18
+ * every node; overshoot ≤ 1). The result then depends only on
19
+ * (state, tables, budgets) — never on the clock — so the same input
20
+ * yields the same solution under any CPU load. Composes with `timeoutMs`
21
+ * when both are set (whichever trips first stops the search).
22
+ */
23
+ maxNodes?: number;
11
24
  tables?: Cube3Phase1Tables;
12
25
  /** Phase-1 joint tables (ud x co / ud x eo), experimental. */
13
26
  jointTables?: Cube3JointTables | undefined;
@@ -27,4 +40,28 @@ export declare function solveCube3Phase1(state: Cube3State, options?: Cube3Phase
27
40
  export declare function encodeCube3Phase1Coordinates(state: Cube3State): Cube3Phase1Coordinates;
28
41
  export declare function isCube3Phase1Solved(input: Cube3State | Cube3Phase1Coordinates, tables: Cube3Phase1Tables): boolean;
29
42
  export declare function applyCube3Phase1Moves(state: Cube3State, moves: readonly Cube3Move[]): Cube3State;
43
+ export interface Cube3Phase1CollectOptions {
44
+ tables?: Cube3Phase1Tables;
45
+ /** Phase-1 joint tables (ud x co / ud x eo), experimental. */
46
+ jointTables?: Cube3JointTables | undefined;
47
+ timeoutMs?: number;
48
+ /**
49
+ * Deterministic node budget for this enumeration (see
50
+ * `Cube3Phase1Options.maxNodes`). Preferred over `timeoutMs` when the
51
+ * collected set must not depend on CPU load.
52
+ */
53
+ maxNodes?: number;
54
+ /** Stop after collecting this many solutions. Defaults to 32. */
55
+ limit?: number;
56
+ }
57
+ /**
58
+ * Enumerates up to `limit` phase-1 solutions of exactly `depth` moves for
59
+ * the given state, most promising first (same ordered DFS as the search).
60
+ * Used to try several G1 landing states and keep the best phase-1+phase-2
61
+ * total — a slightly longer phase 1 often collapses phase 2.
62
+ */
63
+ export declare function collectCube3Phase1Solutions(state: Cube3State, depth: number, options?: Cube3Phase1CollectOptions): Promise<{
64
+ solutions: Cube3Move[][];
65
+ visitedNodes: number;
66
+ }>;
30
67
  //# sourceMappingURL=phase1.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"phase1.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/phase1.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAMzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOvE,MAAM,WAAW,sBAAsB;IACrC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,8DAA8D;IAC9D,WAAW,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,CAAC;IAClB,WAAW,EAAE,sBAAsB,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;CAKlD;AA+BD,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAkF5B;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,UAAU,GAAG,sBAAsB,CAMtF;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,UAAU,GAAG,sBAAsB,EAC1C,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAQT;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,SAAS,SAAS,EAAE,GAC1B,UAAU,CAEZ"}
1
+ {"version":3,"file":"phase1.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/phase1.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAMzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOvE,MAAM,WAAW,sBAAsB;IACrC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,8DAA8D;IAC9D,WAAW,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,CAAC;IAClB,WAAW,EAAE,sBAAsB,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;CAKlD;AA4CD,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAwF5B;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,UAAU,GAAG,sBAAsB,CAMtF;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,UAAU,GAAG,sBAAsB,EAC1C,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAQT;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,SAAS,SAAS,EAAE,GAC1B,UAAU,CAEZ;AAyFD,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,8DAA8D;IAC9D,WAAW,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAC/C,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC;IAAE,SAAS,EAAE,SAAS,EAAE,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CA2B7D"}
@@ -28,14 +28,7 @@ export async function solveCube3Phase1(state, options = {}) {
28
28
  const maxDepth = options.maxDepth ?? 12;
29
29
  const start = encodeCube3Phase1Coordinates(state);
30
30
  const deadline = options.timeoutMs === undefined ? undefined : performance.now() + options.timeoutMs;
31
- const context = {
32
- tables,
33
- jointTables,
34
- maxDepth,
35
- deadline,
36
- visitedNodes: 0,
37
- timedOut: false
38
- };
31
+ const context = createPhase1SearchContext(tables, jointTables, maxDepth, deadline, options.maxNodes);
39
32
  if (await isCube3Phase1Solved(start, tables)) {
40
33
  return {
41
34
  moves: [],
@@ -77,6 +70,9 @@ export async function solveCube3Phase1(state, options = {}) {
77
70
  visitedNodes: context.visitedNodes
78
71
  };
79
72
  }
73
+ if (context.nodesExceeded) {
74
+ throw new Cube3Phase1SearchError(`3x3 Phase 1 node budget exceeded (${options.maxNodes} nodes).`, context.visitedNodes);
75
+ }
80
76
  if (context.timedOut) {
81
77
  throw new Cube3Phase1SearchError(`3x3 Phase 1 search timed out after ${options.timeoutMs}ms.`, context.visitedNodes);
82
78
  }
@@ -107,8 +103,17 @@ const PHASE1_MOVE_FACES = CUBE3_PHASE1_MOVES.map((move) => {
107
103
  });
108
104
  function searchPhase1(cornerOrientation, edgeOrientation, udSlice, previousFace, previousPreviousFace, remainingDepth, path, context) {
109
105
  context.visitedNodes += 1;
110
- if (context.deadline !== undefined && (context.visitedNodes & 1023) === 0 && performance.now() > context.deadline) {
106
+ if (context.nodeCap !== undefined &&
107
+ context.visitedNodes > context.nodeCap) {
108
+ context.nodesExceeded = true;
109
+ }
110
+ if (!context.nodesExceeded &&
111
+ context.deadline !== undefined &&
112
+ (context.visitedNodes & 1023) === 0 &&
113
+ performance.now() > context.deadline) {
111
114
  context.timedOut = true;
115
+ }
116
+ if (context.timedOut || context.nodesExceeded) {
112
117
  return false;
113
118
  }
114
119
  if (remainingDepth === 0) {
@@ -116,6 +121,68 @@ function searchPhase1(cornerOrientation, edgeOrientation, udSlice, previousFace,
116
121
  edgeOrientation === 0 &&
117
122
  udSlice === context.tables.moveTables.udSlice.solvedCoordinate;
118
123
  }
124
+ const moveCount = context.tables.phase1Moves.length;
125
+ const childCount = collectPhase1Children(cornerOrientation, edgeOrientation, udSlice, previousFace, previousPreviousFace, remainingDepth, context);
126
+ const slot = remainingDepth * moveCount;
127
+ const scratchMove = context.scratchMove;
128
+ const scratchCo = context.scratchCo;
129
+ const scratchEo = context.scratchEo;
130
+ const scratchUd = context.scratchUd;
131
+ for (let i = 0; i < childCount; i += 1) {
132
+ const moveIndex = scratchMove[slot + i];
133
+ const face = PHASE1_MOVE_FACES[moveIndex];
134
+ path.push(context.tables.phase1Moves[moveIndex]);
135
+ if (searchPhase1(scratchCo[slot + i], scratchEo[slot + i], scratchUd[slot + i], face, previousFace, remainingDepth - 1, path, context)) {
136
+ return true;
137
+ }
138
+ path.pop();
139
+ if (context.timedOut || context.nodesExceeded) {
140
+ return false;
141
+ }
142
+ }
143
+ return false;
144
+ }
145
+ /**
146
+ * Enumerates up to `limit` phase-1 solutions of exactly `depth` moves for
147
+ * the given state, most promising first (same ordered DFS as the search).
148
+ * Used to try several G1 landing states and keep the best phase-1+phase-2
149
+ * total — a slightly longer phase 1 often collapses phase 2.
150
+ */
151
+ export async function collectCube3Phase1Solutions(state, depth, options = {}) {
152
+ const tables = options.tables ?? await loadCube3Phase1Tables();
153
+ const jointTables = options.jointTables ?? tables.jointTables;
154
+ const limit = options.limit ?? 32;
155
+ const deadline = options.timeoutMs === undefined ? undefined : performance.now() + options.timeoutMs;
156
+ const context = createPhase1SearchContext(tables, jointTables, depth, deadline, options.maxNodes);
157
+ const start = encodeCube3Phase1Coordinates(state);
158
+ const solutions = [];
159
+ searchPhase1Collect(start.cornerOrientation, start.edgeOrientation, start.udSlice, -1, -1, depth, [], solutions, limit, context);
160
+ return { solutions, visitedNodes: context.visitedNodes };
161
+ }
162
+ function createPhase1SearchContext(tables, jointTables, maxDepth, deadline, nodeCap) {
163
+ const scratchLevels = maxDepth + 1;
164
+ const scratchSize = scratchLevels * CUBE3_PHASE1_MOVES.length;
165
+ return {
166
+ tables,
167
+ jointTables,
168
+ maxDepth,
169
+ deadline,
170
+ nodeCap,
171
+ visitedNodes: 0,
172
+ timedOut: false,
173
+ nodesExceeded: false,
174
+ scratchMove: new Int32Array(scratchSize),
175
+ scratchBound: new Int32Array(scratchSize),
176
+ scratchCo: new Int32Array(scratchSize),
177
+ scratchEo: new Int32Array(scratchSize),
178
+ scratchUd: new Int32Array(scratchSize)
179
+ };
180
+ }
181
+ /**
182
+ * Fills the scratch slot for `remainingDepth` with the surviving children
183
+ * in ascending lower-bound order (stable). Returns the child count.
184
+ */
185
+ function collectPhase1Children(cornerOrientation, edgeOrientation, udSlice, previousFace, previousPreviousFace, remainingDepth, context) {
119
186
  const moveCount = context.tables.phase1Moves.length;
120
187
  const validMoves = PHASE1_VALID_MOVES[previousFace + 1][previousPreviousFace + 1];
121
188
  const coTable = context.tables.moveTables.cornerOrientation.values;
@@ -123,9 +190,17 @@ function searchPhase1(cornerOrientation, edgeOrientation, udSlice, previousFace,
123
190
  const udTable = context.tables.moveTables.udSlice.values;
124
191
  const coeoTable = context.tables.pruningTables.cornerOrientationEdgeOrientation.values;
125
192
  const udPruningTable = context.tables.pruningTables.udSlice.values;
193
+ const jointTables = context.jointTables;
126
194
  const coOffset = cornerOrientation * moveCount;
127
195
  const eoOffset = edgeOrientation * moveCount;
128
196
  const udOffset = udSlice * moveCount;
197
+ const slot = remainingDepth * moveCount;
198
+ const scratchMove = context.scratchMove;
199
+ const scratchBound = context.scratchBound;
200
+ const scratchCo = context.scratchCo;
201
+ const scratchEo = context.scratchEo;
202
+ const scratchUd = context.scratchUd;
203
+ let childCount = 0;
129
204
  for (let i = 0; i < validMoves.length; i += 1) {
130
205
  const moveIndex = validMoves[i];
131
206
  const nextCornerOrientation = coTable[coOffset + moveIndex];
@@ -138,7 +213,6 @@ function searchPhase1(cornerOrientation, edgeOrientation, udSlice, previousFace,
138
213
  if (nextLowerBound > remainingDepth - 1) {
139
214
  continue;
140
215
  }
141
- const jointTables = context.jointTables;
142
216
  if (jointTables) {
143
217
  const udCoIndex = nextUdSlice * 2187 + nextCornerOrientation;
144
218
  const jc = (jointTables.jointUdCo[udCoIndex >> 1] >> ((udCoIndex & 1) << 2)) & 0x0f;
@@ -154,14 +228,81 @@ function searchPhase1(cornerOrientation, edgeOrientation, udSlice, previousFace,
154
228
  continue;
155
229
  }
156
230
  }
231
+ scratchMove[slot + childCount] = moveIndex;
232
+ scratchBound[slot + childCount] = nextLowerBound;
233
+ scratchCo[slot + childCount] = nextCornerOrientation;
234
+ scratchEo[slot + childCount] = nextEdgeOrientation;
235
+ scratchUd[slot + childCount] = nextUdSlice;
236
+ childCount += 1;
237
+ }
238
+ // Stable insertion sort by bound (n <= 18; ties keep reference order).
239
+ for (let i = 1; i < childCount; i += 1) {
240
+ const move = scratchMove[slot + i];
241
+ const bound = scratchBound[slot + i];
242
+ const co = scratchCo[slot + i];
243
+ const eo = scratchEo[slot + i];
244
+ const ud = scratchUd[slot + i];
245
+ let j = i - 1;
246
+ while (j >= 0 && scratchBound[slot + j] > bound) {
247
+ scratchMove[slot + j + 1] = scratchMove[slot + j];
248
+ scratchBound[slot + j + 1] = scratchBound[slot + j];
249
+ scratchCo[slot + j + 1] = scratchCo[slot + j];
250
+ scratchEo[slot + j + 1] = scratchEo[slot + j];
251
+ scratchUd[slot + j + 1] = scratchUd[slot + j];
252
+ j -= 1;
253
+ }
254
+ scratchMove[slot + j + 1] = move;
255
+ scratchBound[slot + j + 1] = bound;
256
+ scratchCo[slot + j + 1] = co;
257
+ scratchEo[slot + j + 1] = eo;
258
+ scratchUd[slot + j + 1] = ud;
259
+ }
260
+ return childCount;
261
+ }
262
+ /**
263
+ * Ordered exact-depth DFS that collects solutions instead of stopping at
264
+ * the first one. Returns true when collection should stop (limit reached
265
+ * or timed out).
266
+ */
267
+ function searchPhase1Collect(cornerOrientation, edgeOrientation, udSlice, previousFace, previousPreviousFace, remainingDepth, path, solutions, limit, context) {
268
+ context.visitedNodes += 1;
269
+ if (context.nodeCap !== undefined &&
270
+ context.visitedNodes > context.nodeCap) {
271
+ context.nodesExceeded = true;
272
+ }
273
+ if (!context.nodesExceeded &&
274
+ context.deadline !== undefined &&
275
+ (context.visitedNodes & 1023) === 0 &&
276
+ performance.now() > context.deadline) {
277
+ context.timedOut = true;
278
+ }
279
+ if (context.timedOut || context.nodesExceeded) {
280
+ return true;
281
+ }
282
+ if (remainingDepth === 0) {
283
+ if (cornerOrientation === 0 &&
284
+ edgeOrientation === 0 &&
285
+ udSlice === context.tables.moveTables.udSlice.solvedCoordinate) {
286
+ solutions.push([...path]);
287
+ return solutions.length >= limit;
288
+ }
289
+ return false;
290
+ }
291
+ const moveCount = context.tables.phase1Moves.length;
292
+ const childCount = collectPhase1Children(cornerOrientation, edgeOrientation, udSlice, previousFace, previousPreviousFace, remainingDepth, context);
293
+ const slot = remainingDepth * moveCount;
294
+ const scratchMove = context.scratchMove;
295
+ const scratchCo = context.scratchCo;
296
+ const scratchEo = context.scratchEo;
297
+ const scratchUd = context.scratchUd;
298
+ for (let i = 0; i < childCount; i += 1) {
299
+ const moveIndex = scratchMove[slot + i];
157
300
  const face = PHASE1_MOVE_FACES[moveIndex];
158
301
  path.push(context.tables.phase1Moves[moveIndex]);
159
- if (searchPhase1(nextCornerOrientation, nextEdgeOrientation, nextUdSlice, face, previousFace, remainingDepth - 1, path, context)) {
160
- return true;
161
- }
302
+ const stop = searchPhase1Collect(scratchCo[slot + i], scratchEo[slot + i], scratchUd[slot + i], face, previousFace, remainingDepth - 1, path, solutions, limit, context);
162
303
  path.pop();
163
- if (context.timedOut) {
164
- return false;
304
+ if (stop) {
305
+ return true;
165
306
  }
166
307
  }
167
308
  return false;
@@ -9,10 +9,29 @@ export interface Cube3Phase2Coordinates {
9
9
  }
10
10
  export interface Cube3Phase2Options {
11
11
  maxDepth?: number;
12
+ /**
13
+ * Legacy wall-clock deadline (checked every 1024 nodes): the stopping
14
+ * point — and the result — depends on machine speed and CPU load.
15
+ * Prefer `maxNodes` for reproducible outcomes.
16
+ */
12
17
  timeoutMs?: number;
18
+ /**
19
+ * Deterministic work cap: stop after visiting this many nodes (checked
20
+ * on every node; overshoot ≤ 1). Same input + same cap ⇒ same result
21
+ * regardless of load/clock. Composes with `timeoutMs` when both are set.
22
+ */
23
+ maxNodes?: number;
13
24
  tables?: Cube3Phase2Tables;
14
25
  /** Combined (cp x uep) pair-distance pruning tables (experimental). */
15
26
  pairTables?: Cube3PairTables | undefined;
27
+ /**
28
+ * Skip the pair tables entirely (no lazy load). All pair-bound uses are
29
+ * already guarded, so the search runs without pair pruning. Used by the
30
+ * 4x4 finish, where pair pruning was measured to change nothing (identical
31
+ * lengths on a 44-state corpus, zero extra retries) while costing a 54MB
32
+ * table load.
33
+ */
34
+ skipPairTables?: boolean;
16
35
  previousFace?: number;
17
36
  previousPreviousFace?: number;
18
37
  }
@@ -23,9 +42,65 @@ export interface Cube3Phase2Result {
23
42
  coordinates: Cube3Phase2Coordinates;
24
43
  visitedNodes: number;
25
44
  }
45
+ export type Cube3SolveQuality = "fast" | "short" | "best";
26
46
  export interface Cube3SolveOptions {
27
47
  phase1?: Cube3Phase1Options;
28
48
  phase2?: Cube3Phase2Options;
49
+ targetLength?: number;
50
+ timeoutMs?: number;
51
+ /**
52
+ * Quality tier (defaults to `"short"`):
53
+ * - `"fast"`: legacy min-P1 + min-P2 (~14 ms, ~23.1 moves, browser tables).
54
+ * - `"short"`: best of several G1 landings (~36 ms, ~21.9 moves).
55
+ * - `"best"`: short x 3 orientations (~116 ms, ~20.8 moves, max 22).
56
+ * Explicit `phase1ExtraDepth` / `phase1SolutionLimit` / `orientations`
57
+ * always override the tier preset.
58
+ */
59
+ quality?: Cube3SolveQuality;
60
+ /**
61
+ * Extra phase-1 depths to explore beyond the minimal one, keeping the
62
+ * best phase-1+phase-2 total. A slightly longer phase 1 often collapses
63
+ * phase 2, shortening the total (and sometimes the search itself).
64
+ * Defaults to 0 (legacy min-P1 + min-P2 behavior). The `targetLength`
65
+ * path always explores up to 4 extra depths.
66
+ */
67
+ phase1ExtraDepth?: number;
68
+ /**
69
+ * Max phase-1 solutions enumerated per extra depth (most promising
70
+ * first). Defaults to 32.
71
+ */
72
+ phase1SolutionLimit?: number;
73
+ /**
74
+ * Per-candidate phase-2 budget (ms) inside the improvement loop.
75
+ * Defaults to `phase2.timeoutMs` when set, else 25. The initial solve
76
+ * always uses `phase2.timeoutMs` untouched, so this never starves it.
77
+ * Legacy wall-clock; superseded by `improvePhase2MaxNodes`.
78
+ */
79
+ improvePhase2TimeoutMs?: number;
80
+ /**
81
+ * Deterministic node budget for the improvement loop (phase-1 collection
82
+ * + candidate phase-2 searches), counted as visited search nodes across
83
+ * the whole loop of ONE solve call. When set, the wall deadline
84
+ * (`timeoutMs`, default 800 ms) is NOT armed: the returned solution
85
+ * depends only on (state, tables, budgets) and is reproducible under any
86
+ * CPU load — this is the mode the 4x4 finish uses.
87
+ */
88
+ improveMaxNodes?: number;
89
+ /**
90
+ * Deterministic per-candidate phase-2 node cap inside the improvement
91
+ * loop (counterpart of `improvePhase2TimeoutMs`). Defaults to
92
+ * `phase2.maxNodes` when set, else the shared `improveMaxNodes`
93
+ * remainder. Ignored unless `improveMaxNodes` is set.
94
+ */
95
+ improvePhase2MaxNodes?: number;
96
+ /**
97
+ * Solve rotated copies of the state and keep the shortest solution
98
+ * (1 = UD only, 3 = UD/x/z triple, up to 24 for the full rotation
99
+ * group). Each extra orientation buys roughly 0.3-0.5 moves for ~1x
100
+ * single-solve time. Composes with the other options (each orientation
101
+ * is solved with the same settings). Defaults to 1.
102
+ */
103
+ orientations?: number;
29
104
  }
30
105
  export interface Cube3SolveResult {
31
106
  phase1: Cube3Phase1Result;
@@ -1 +1 @@
1
- {"version":3,"file":"phase2.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/phase2.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAczD,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAarD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,MAAM,WAAW,sBAAsB;IACrC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,uEAAuE;IACvE,UAAU,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,CAAC;IAClB,WAAW,EAAE,sBAAsB,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;CAKlD;AA8BD,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAuF5B;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CA+B3B;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,UAAU,GAAG,sBAAsB,CAMtF;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,sBAAsB,GAAG,OAAO,CAQvF;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,SAAS,SAAS,EAAE,GAC1B,UAAU,CAEZ"}
1
+ {"version":3,"file":"phase2.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube3/solver/phase2.ts"],"names":[],"mappings":"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"}