@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
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { CENTER3_MOVES } from "./center3.js";
8
8
  import { getSymTables, getMvSymFrom, MOVE3_STD } from "./edge3.js";
9
+ import { HC_PDB_MOVES } from "./hc-pdb.js";
9
10
  /** The 17 exploration moves (the reference's m < 17 bound). */
10
11
  export const SEARCH_MOVES = CENTER3_MOVES.slice(0, 17);
11
12
  // Commutation / move-cancellation pruning tables
@@ -33,85 +34,510 @@ for (let i = 0; i < 20; i++) {
33
34
  }
34
35
  }
35
36
  }
36
- /**
37
- * Fast table-driven Phase 3 IDA* solver class.
38
- */
37
+ /** Popcount of 12-bit masks (the inlined sym-coordinate rank). */
38
+ const POPC12 = new Uint8Array(4096);
39
+ for (let i = 1; i < 4096; i++)
40
+ POPC12[i] = POPC12[i >> 1] + (i & 1);
41
+ /** Flat row-major ckmv3 (21x20) + typed skipAxis3 for the hot recursion. */
42
+ const ckmv3Flat = new Uint8Array(21 * 20);
43
+ for (let i = 0; i < 21; i++) {
44
+ for (let j = 0; j < 20; j++)
45
+ ckmv3Flat[i * 20 + j] = ckmv3[i][j] ? 1 : 0;
46
+ }
47
+ const skipAxis3Fast = Int32Array.from(skipAxis3);
48
+ const F_SLOT_MAP = [
49
+ [4, 5, 2, 3, 1, 0, 6, 7, 8, 9, 10, 11],
50
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 9, 10, 11],
51
+ [5, 4, 2, 3, 0, 1, 6, 7, 8, 9, 10, 11],
52
+ [0, 1, 2, 3, 4, 6, 5, 7, 8, 9, 11, 10],
53
+ [11, 1, 2, 8, 4, 5, 6, 7, 0, 9, 10, 3],
54
+ [3, 1, 2, 0, 4, 5, 6, 7, 11, 9, 10, 8],
55
+ [8, 1, 2, 11, 4, 5, 6, 7, 3, 9, 10, 0],
56
+ [0, 1, 7, 6, 4, 5, 2, 3, 8, 9, 10, 11],
57
+ [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 10, 11],
58
+ [0, 1, 6, 7, 4, 5, 3, 2, 8, 9, 10, 11],
59
+ [0, 1, 2, 3, 7, 5, 6, 4, 9, 8, 10, 11],
60
+ [0, 9, 10, 3, 4, 5, 6, 7, 8, 2, 1, 11],
61
+ [0, 2, 1, 3, 4, 5, 6, 7, 8, 10, 9, 11],
62
+ [0, 10, 9, 3, 4, 5, 6, 7, 8, 1, 2, 11],
63
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 11, 10, 9],
64
+ [0, 3, 2, 1, 4, 6, 5, 7, 8, 9, 11, 10],
65
+ [3, 1, 2, 0, 4, 7, 6, 5, 11, 9, 10, 8]
66
+ ];
67
+ const H_SLOT_MAP = [
68
+ [4, 5, 2, 3, 1, 0, 6, 7, 8, 9, 10, 11],
69
+ [1, 0, 2, 3, 5, 4, 6, 7, 8, 9, 10, 11],
70
+ [5, 4, 2, 3, 0, 1, 6, 7, 8, 9, 10, 11],
71
+ [0, 1, 2, 3, 4, 6, 5, 7, 8, 9, 11, 10],
72
+ [11, 1, 2, 8, 4, 5, 6, 7, 0, 9, 10, 3],
73
+ [3, 1, 2, 0, 4, 5, 6, 7, 11, 9, 10, 8],
74
+ [8, 1, 2, 11, 4, 5, 6, 7, 3, 9, 10, 0],
75
+ [0, 1, 7, 6, 4, 5, 2, 3, 8, 9, 10, 11],
76
+ [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 10, 11],
77
+ [0, 1, 6, 7, 4, 5, 3, 2, 8, 9, 10, 11],
78
+ [0, 1, 2, 3, 7, 5, 6, 4, 9, 8, 10, 11],
79
+ [0, 9, 10, 3, 4, 5, 6, 7, 8, 2, 1, 11],
80
+ [0, 2, 1, 3, 4, 5, 6, 7, 8, 10, 9, 11],
81
+ [0, 10, 9, 3, 4, 5, 6, 7, 8, 1, 2, 11],
82
+ [1, 0, 2, 3, 5, 4, 6, 7, 10, 9, 8, 11],
83
+ [2, 1, 0, 3, 4, 6, 5, 7, 8, 9, 11, 10],
84
+ [3, 1, 2, 0, 6, 5, 4, 7, 11, 9, 10, 8]
85
+ ];
39
86
  export class FastPhase3Solver {
40
87
  tempep;
41
88
  moveLog;
42
89
  sym;
90
+ /** Transposition Table size: 2^20 = 1,048,576 entries (12 MiB per solve).
91
+ * The previous 2M table used 24 MiB per process, plus a separate 8 MiB
92
+ * generation array. The compact 1M table leaves room for concurrent
93
+ * solvers under the 150 MiB reduction-table budget. */
94
+ static TT_SIZE = 1 << 20;
95
+ static TT_MASK = FastPhase3Solver.TT_SIZE - 1;
96
+ tt = new Uint32Array(FastPhase3Solver.TT_SIZE * 3);
97
+ // TT info packs (call id:20, previous move:5, remaining depth:6), avoiding
98
+ // a separate 8 MiB generation array while retaining exact proof tracking.
99
+ static CALL_ID_LIMIT = 1 << 20;
100
+ callSeq = 1;
101
+ callId = 0;
102
+ contextCenter3;
103
+ contextEdge3;
104
+ /** Reset this solver's cumulative node counter (legacy method name). */
105
+ resetEpochNodes() {
106
+ this.epochNodes = 0;
107
+ }
108
+ clearTranspositionTable() {
109
+ this.tt.fill(0);
110
+ this.callSeq = 1;
111
+ }
112
+ setTableContext(center3, edge3Distances) {
113
+ if (this.contextCenter3 === center3 && this.contextEdge3 === edge3Distances)
114
+ return;
115
+ this.clearTranspositionTable();
116
+ this.contextCenter3 = center3;
117
+ this.contextEdge3 = edge3Distances;
118
+ }
119
+ /** Fresh per-call id for TT-generation tagging. */
120
+ nextCallId() {
121
+ const id = this.callSeq++;
122
+ if (this.callSeq >= FastPhase3Solver.CALL_ID_LIMIT) {
123
+ this.tt.fill(0);
124
+ this.callSeq = 1;
125
+ }
126
+ return id;
127
+ }
43
128
  constructor() {
44
- this.tempep = Array.from({ length: 30 }, () => new Uint8Array(12));
45
- this.moveLog = new Array(30);
129
+ // Preallocated to well above the deepest phase-3 sweep (maxLength3 <= 27):
130
+ // `ensureDepth` stays as a fallback for out-of-band callers.
131
+ this.tempep = Array.from({ length: 48 }, () => new Uint8Array(12));
132
+ this.moveLog = new Array(48);
46
133
  this.sym = getSymTables();
47
134
  }
48
- getPrun(distances, idx) {
49
- const byte = distances[idx >> 1];
50
- return (idx & 1) === 0 ? byte & 0x0f : byte >> 4;
135
+ ensureDepth(depth) {
136
+ while (this.tempep.length <= depth) {
137
+ this.tempep.push(new Uint8Array(12));
138
+ this.moveLog.push(0);
139
+ }
51
140
  }
141
+ nodeCount = 0;
142
+ /** Cumulative nodes across calls (reset per solve generation). */
143
+ epochNodes = 0;
144
+ /**
145
+ * Proof bookkeeping for IDA* threshold propagation (reset per exact-length
146
+ * call). When a length-L search exhausts its tree WITHOUT hitting any of
147
+ * these flags, no (center, edge) solution exists at any length below
148
+ * `minExcess`, so the caller may skip straight to it:
149
+ * - `capped`: the node budget aborted the search (bounds unknown).
150
+ * - `limitHit`: goal enumeration hit its cap (tree not exhausted).
151
+ * - `sawGoal`: a goal was reached (even if the validator rejected it —
152
+ * a nearby length may still validate, so only step L+1).
153
+ * - `ttTouched`: a transposition-table hit from ANOTHER exact-length call
154
+ * skipped a subtree, and the hit was not certified goal-free across
155
+ * candidate validators.
156
+ * Bound-based axis skips need no flag: the skipped siblings are
157
+ * same-face turns, so noteExcess records their triangle-inequality bound.
158
+ */
159
+ capped = false;
160
+ limitHit = false;
161
+ sawGoal = false;
162
+ ttTouched = false;
163
+ minExcess = 0;
52
164
  /**
53
- * Recursive IDA* worker on table transitions.
165
+ * Root-length coverage of the tightest TT hit this call (depth +
166
+ * storedDepth): the pruned subtree proves no goal at or below that root
167
+ * length under the call's predicate. `Number.MAX_SAFE_INTEGER` when no hit
168
+ * pruned anything. Feeds `validatedThrough`.
54
169
  */
55
- async search(eplast, ct, prun, maxl, lm, depth, center3Tables, edge3Distances) {
56
- if (maxl === 0)
57
- return true;
58
- if (depth % 500 === 0)
59
- await new Promise(r => setTimeout(r, 0));
170
+ ttCoverage = Number.MAX_SAFE_INTEGER;
171
+ /**
172
+ * Recursive IDA* worker on table transitions with Transposition Table pruning.
173
+ */
174
+ search(eplast, ct, prun, maxl, lm, depth, center3Tables, edge3Distances, maxNodes = 15000, collect, hc, hcCoord = -1, c3PairDist) {
175
+ this.nodeCount++;
176
+ this.epochNodes++;
177
+ if (this.nodeCount > maxNodes) {
178
+ this.capped = true;
179
+ return false;
180
+ }
181
+ // Goal test at EVERY node (any length <= maxl), not only at maxl === 0
182
+ // leaves: a goal is (edge paired AND center3 at goal) — both PDB
183
+ // distances zero (the parent's gates guarantee leaves are goals, so this
184
+ // strictly generalizes the old leaf test). Exact-length-only detection
185
+ // made shorter goals on the same path invisible ("a nearby length may
186
+ // still validate"), which both blocked proofs on any raw goal and hid
187
+ // shorter solutions.
188
+ if (prun === 0 && center3Tables.pruning[ct] === 0) {
189
+ this.sawGoal = true;
190
+ if (!collect)
191
+ return true;
192
+ collect.examined++;
193
+ const moves = this.moveLog.slice(0, depth).map((m) => SEARCH_MOVES[m]);
194
+ if (collect.validate(moves))
195
+ return true;
196
+ if (collect.examined >= collect.limit) {
197
+ this.limitHit = true;
198
+ return true;
199
+ }
200
+ // A rejected goal is not a goal for the validator's predicate: keep
201
+ // exploring the subtree — validated goals may sit deeper on the same
202
+ // branches. At the fringe (maxl === 0) there is no subtree left, so
203
+ // record the branch's coverage claim here (nothing below `depth` on
204
+ // this branch was searched): "no validated goal at <= depth".
205
+ if (maxl === 0 && depth < this.minExcess)
206
+ this.minExcess = depth + 1;
207
+ }
208
+ else if (maxl === 0) {
209
+ return false;
210
+ }
211
+ if (depth >= this.tempep.length)
212
+ this.ensureDepth(depth);
213
+ const sym = this.sym;
214
+ const symMvrot = sym.mvrot;
215
+ const symMvroto = sym.mvroto;
60
216
  const ep = this.tempep[depth];
61
217
  if (lm !== 20) {
62
218
  const movoBase = (lm << 3) * 12;
63
- const movBase = (lm << 3) * 12;
64
219
  for (let i = 0; i < 12; i++) {
65
- ep[i] = this.sym.mvroto[movoBase + eplast[this.sym.mvrot[movBase + i]]];
220
+ ep[i] = symMvroto[movoBase + eplast[symMvrot[movoBase + i]]];
66
221
  }
67
222
  }
68
223
  else {
69
224
  ep.set(eplast);
70
225
  }
226
+ const hasHc = hc !== undefined && hcCoord >= 0 && hc.moves.length > 0;
227
+ // HC pruning depends on a second coordinate not represented in the TT
228
+ // key. The root c3-pair check also varies only at depth zero. Do not reuse
229
+ // entries in those contexts; all other phase-3 state is key-complete.
230
+ const useTt = !hasHc && !(depth === 0 && c3PairDist !== undefined);
231
+ // TT: entries claim "no RAW goal at any length <= maxl from (ep, lm)" —
232
+ // coordinate-complete and predicate-independent (the validator's
233
+ // full-state checks are NOT coordinate-complete, so no validator-scoped
234
+ // claims may be cached). Hits are `<=`-claims and therefore
235
+ // proof-compatible for `validatedThrough`/`provenThrough` coverage.
236
+ const ttActive = useTt && maxl >= 2;
237
+ let keyHigh = 0;
238
+ let keyLow = 0;
239
+ let slot = 0;
240
+ if (ttActive) {
241
+ keyHigh = ((ct << 16) | (ep[8] | (ep[9] << 4) | (ep[10] << 8) | (ep[11] << 12))) >>> 0;
242
+ keyLow = (ep[0] | (ep[1] << 4) | (ep[2] << 8) | (ep[3] << 12) |
243
+ (ep[4] << 16) | (ep[5] << 20) | (ep[6] << 24) | (ep[7] << 28)) >>> 0;
244
+ slot = (((keyLow ^ Math.imul(keyHigh, 0x9e3779b9)) & FastPhase3Solver.TT_MASK) * 3) >>> 0;
245
+ const storedHigh = this.tt[slot];
246
+ const storedLow = this.tt[slot + 1];
247
+ const storedInfo = this.tt[slot + 2];
248
+ if (storedHigh === keyHigh && storedLow === keyLow) {
249
+ const storedCallId = (storedInfo >>> 11) & 0xfffff;
250
+ const storedLm = (storedInfo >>> 6) & 0x1f;
251
+ const storedDepth = storedInfo & 0x3f;
252
+ if (storedDepth >= maxl && storedLm === lm) {
253
+ const coverage = depth + storedDepth;
254
+ if (coverage < this.ttCoverage)
255
+ this.ttCoverage = coverage;
256
+ if (storedCallId !== this.callId)
257
+ this.ttTouched = true;
258
+ return false;
259
+ }
260
+ }
261
+ }
262
+ const c3Move = center3Tables.moveTable;
263
+ const c3Prun = center3Tables.pruning;
264
+ const c3Row = ct * CENTER3_MOVES.length;
265
+ const sym2Raw = sym.sym2Raw;
266
+ const sym2Mask = sym.sym2Mask;
267
+ const raw2Sym = sym.raw2Sym;
268
+ const hcMoves = hasHc ? hc.moves : null;
269
+ const hcDist = hasHc ? hc.distances : null;
270
+ const hcRow = hasHc ? hcCoord * HC_PDB_MOVES.length : 0;
271
+ // Byte-expanded (tests/generators) vs nibble-packed (loaders) distances.
272
+ const edgeUnpacked = edge3Distances.length >= 31006080;
273
+ const examined0 = collect === undefined ? 0 : collect.examined;
274
+ const ckmvRow = lm * 20;
71
275
  for (let m = 0; m < 17; m++) {
72
- if (ckmv3[lm][m]) {
73
- m = skipAxis3[m];
276
+ if (ckmv3Flat[ckmvRow + m]) {
277
+ m = skipAxis3Fast[m];
74
278
  continue;
75
279
  }
76
- const nc = center3Tables.moveTable[ct * CENTER3_MOVES.length + m];
280
+ const nc = c3Move[c3Row + m];
77
281
  if (nc < 0)
78
282
  continue;
79
- const prun1 = center3Tables.pruning[nc];
283
+ const prun1 = c3Prun[nc];
80
284
  if (prun1 >= maxl) {
285
+ const ex1 = (prun1 > maxl && m < 14) ? depth + prun1 : depth + 1 + prun1;
286
+ if (ex1 < this.minExcess)
287
+ this.minExcess = ex1;
81
288
  if (prun1 > maxl && m < 14)
82
- m = skipAxis3[m];
289
+ m = skipAxis3Fast[m];
83
290
  continue;
84
291
  }
85
- const symIdx = getMvSymFrom(ep, ep, m, this.sym.sym2Raw, this.sym.sym2Mask, this.sym.raw2Sym, this.sym.mvrot, this.sym.mvroto);
86
- const prunx = this.getPrun(edge3Distances, symIdx >> 3);
292
+ let nhc = -1;
293
+ if (hasHc) {
294
+ nhc = hcMoves[hcRow + m];
295
+ if (nhc >= 0) {
296
+ const prunh = hcDist[nhc];
297
+ if (prunh >= maxl) {
298
+ const exH = (prunh > maxl && m < 14) ? depth + prunh : depth + 1 + prunh;
299
+ if (exH < this.minExcess)
300
+ this.minExcess = exH;
301
+ if (prunh > maxl && m < 14)
302
+ m = skipAxis3Fast[m];
303
+ continue;
304
+ }
305
+ }
306
+ }
307
+ if (c3PairDist && depth === 0) {
308
+ const c3Base = nc * 144;
309
+ const fSlot = F_SLOT_MAP[m];
310
+ const hSlot = H_SLOT_MAP[m];
311
+ let prunPair = 0;
312
+ for (let s = 0; s < 12; s++) {
313
+ const idx = c3Base + fSlot[s] * 12 + hSlot[ep[s]];
314
+ const d = (idx & 1) === 0 ? (c3PairDist[idx >> 1] & 0x0f) : (c3PairDist[idx >> 1] >> 4);
315
+ if (d >= maxl) {
316
+ prunPair = d;
317
+ break;
318
+ }
319
+ if (d > prunPair)
320
+ prunPair = d;
321
+ }
322
+ if (prunPair >= maxl) {
323
+ const exP = (prunPair > maxl && m < 14) ? depth + prunPair : depth + 1 + prunPair;
324
+ if (exP < this.minExcess)
325
+ this.minExcess = exP;
326
+ if (prunPair > maxl && m < 14)
327
+ m = skipAxis3Fast[m];
328
+ continue;
329
+ }
330
+ }
331
+ // Inlined getMvSymFrom(ep, ep, m, ...) (bit-identical; masks here are
332
+ // 12-bit, so one popcount lookup replaces bitCount's three).
333
+ let mrIdx = m << 3;
334
+ let movoBase = mrIdx * 12;
335
+ let raw = 0;
336
+ let bits = 0;
337
+ for (let i = 0; i < 4; i++) {
338
+ const val = symMvroto[movoBase + ep[symMvrot[movoBase + i]]];
339
+ raw = raw * (12 - i) + val - POPC12[bits & ((1 << val) - 1)];
340
+ bits |= 1 << val;
341
+ }
342
+ const symBase = raw2Sym[raw];
343
+ mrIdx |= symBase & 7;
344
+ let symOut = symBase >> 3;
345
+ bits = sym2Mask[symOut];
346
+ movoBase = mrIdx * 12;
347
+ for (let i = 4; i < 10; i++) {
348
+ const val = symMvroto[movoBase + ep[symMvrot[movoBase + i]]];
349
+ symOut = symOut * (12 - i) + val - POPC12[bits & ((1 << val) - 1)];
350
+ bits |= 1 << val;
351
+ }
352
+ const symIdx = ((symOut << 3) | (mrIdx & 7)) >>> 0;
353
+ // Format-aware read: loaders keep the nibble-packed table (15.5MB);
354
+ // tests and generators may pass byte-expanded buffers instead.
355
+ const eIdx = symIdx >> 3;
356
+ const prunx = edgeUnpacked
357
+ ? edge3Distances[eIdx]
358
+ : (edge3Distances[eIdx >> 1] >> ((eIdx & 1) * 4)) & 0x0f;
87
359
  if (prunx >= maxl) {
360
+ const exE = (prunx > maxl && m < 14) ? depth + prunx : depth + 1 + prunx;
361
+ if (exE < this.minExcess)
362
+ this.minExcess = exE;
88
363
  if (prunx > maxl && m < 14)
89
- m = skipAxis3[m];
364
+ m = skipAxis3Fast[m];
90
365
  continue;
91
366
  }
92
- if (await this.search(ep, nc, prunx, maxl - 1, m, depth + 1, center3Tables, edge3Distances)) {
93
- this.moveLog[depth] = m;
367
+ this.moveLog[depth] = m;
368
+ if (this.search(ep, nc, prunx, maxl - 1, m, depth + 1, center3Tables, edge3Distances, maxNodes, collect, hc, nhc, c3PairDist)) {
94
369
  return true;
95
370
  }
96
371
  }
372
+ // Cache only subtrees with NO raw goal reached in them (the examined
373
+ // count is the goal counter — rejections included, since the claim is
374
+ // raw). Goal-free subtrees are safe to reuse across calls, candidates
375
+ // and predicates.
376
+ if (ttActive &&
377
+ this.nodeCount <= maxNodes &&
378
+ (collect?.examined ?? 0) === examined0) {
379
+ this.tt[slot] = keyHigh;
380
+ this.tt[slot + 1] = keyLow;
381
+ this.tt[slot + 2] = ((this.callId << 11) | (lm << 6) | maxl) >>> 0;
382
+ }
97
383
  return false;
98
384
  }
99
385
  /**
100
- * Search for a phase-3 solution with exact target length.
386
+ * Record an IDA* bound prune (inlined at the four gate sites): the child at
387
+ * `depth + 1` needs at least `childBound` more moves, so no solution exists
388
+ * through this branch below root threshold `depth + 1 + childBound`. When a
389
+ * bound-based axis skip also fires, the skipped siblings are same-face turns
390
+ * (verified: every non-empty skipAxis3 run under m < 14 is a same-base
391
+ * triple), hence at most 1 legal move apart — their bound is at least
392
+ * `childBound - 1` by the triangle inequality on the exact PDBs, so the
393
+ * recorded excess `depth + childBound` stays a true lower bound.
394
+ */
395
+ /**
396
+ * Search for a phase-3 solution with exact target length (first found).
101
397
  */
102
- async solveLength(edge, ct3, edist, length3, center3Tables, edge3Distances) {
398
+ solveLength(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, hc, hcCoord = -1, c3PairDist) {
399
+ this.setTableContext(center3Tables, edge3Distances);
103
400
  this.tempep[0].set(edge);
104
- if (await this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances)) {
401
+ this.nodeCount = 0;
402
+ this.callId = this.nextCallId();
403
+ this.ttCoverage = Number.MAX_SAFE_INTEGER;
404
+ if (this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, undefined, hc, hcCoord, c3PairDist)) {
105
405
  return this.moveLog.slice(0, length3).map((m) => SEARCH_MOVES[m]);
106
406
  }
107
407
  return null;
108
408
  }
409
+ /**
410
+ * Search for a phase-3 solution with exact target length, accepting the
411
+ * first goal sequence that passes `validate` (up to `limit` examined,
412
+ * sharing `maxNodes`). With a validator that enforces the downstream
413
+ * requirements (reduction + standard orientation + 3x3 conversion), hard
414
+ * states whose first-found solutions are unusable get more draws from
415
+ * the same budget instead of an immediate miss.
416
+ */
417
+ solveLengthValid(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, limit = 16, validate, hc, hcCoord = -1, c3PairDist) {
418
+ return this.solveLengthProven(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes, limit, validate, hc, hcCoord, c3PairDist).moves;
419
+ }
420
+ /**
421
+ * Exact-length validated search that also reports what the call proved.
422
+ * `provenThrough` is the greatest length with provably no (center, edge)
423
+ * solution: set only when the tree was exhaustively explored with no goal
424
+ * reached, no node-cap abort, no TT hit, and complete excess info — the
425
+ * caller may then skip every shorter length for this candidate. Otherwise
426
+ * it is `length3 - 1` at most (only the attempted length is settled).
427
+ */
428
+ solveLengthProven(edge, ct3, edist, length3, center3Tables, edge3Distances, maxNodes = 15000, limit = 16, validate, hc, hcCoord = -1, c3PairDist) {
429
+ this.setTableContext(center3Tables, edge3Distances);
430
+ if (c3PairDist) {
431
+ const c3Base = ct3 * 144;
432
+ let maxPairDist = 0;
433
+ for (let s = 0; s < 12; s++) {
434
+ const idx = c3Base + s * 12 + edge[s];
435
+ const d = (idx & 1) === 0 ? (c3PairDist[idx >> 1] & 0x0f) : (c3PairDist[idx >> 1] >> 4);
436
+ if (d > maxPairDist)
437
+ maxPairDist = d;
438
+ }
439
+ if (maxPairDist > length3) {
440
+ return { moves: null, provenThrough: maxPairDist - 1, validatedThrough: maxPairDist - 1, nodesUsed: 0 };
441
+ }
442
+ }
443
+ this.tempep[0].set(edge);
444
+ this.nodeCount = 0;
445
+ this.capped = false;
446
+ this.limitHit = false;
447
+ this.sawGoal = false;
448
+ this.ttTouched = false;
449
+ this.minExcess = Number.MAX_SAFE_INTEGER;
450
+ this.ttCoverage = Number.MAX_SAFE_INTEGER;
451
+ this.callId = this.nextCallId();
452
+ let accepted = null;
453
+ const collect = {
454
+ limit,
455
+ examined: 0,
456
+ validate: (moves) => {
457
+ if (validate(moves)) {
458
+ accepted = moves;
459
+ return true;
460
+ }
461
+ // A rejected goal is not a validated solution. Enumeration callers
462
+ // may collect it themselves, but cannot use it as a search proof.
463
+ return false;
464
+ }
465
+ };
466
+ this.search(this.tempep[0], ct3, edist, length3, 20, 0, center3Tables, edge3Distances, maxNodes, collect, hc, hcCoord, c3PairDist);
467
+ if (accepted)
468
+ return { moves: accepted, provenThrough: length3, validatedThrough: length3, nodesUsed: this.nodeCount };
469
+ if (typeof process !== "undefined" && process.env?.CUBE4_PROOF_STATS === "1") {
470
+ proofStats.total++;
471
+ if (this.capped)
472
+ proofStats.capped++;
473
+ if (this.limitHit)
474
+ proofStats.limitHit++;
475
+ if (this.sawGoal)
476
+ proofStats.sawGoal++;
477
+ if (this.ttTouched)
478
+ proofStats.ttTouched++;
479
+ }
480
+ // `provenThrough` keeps its conservative raw-goal contract: it may only
481
+ // claim a depth when NO raw goal was reached at all (a rejected goal IS
482
+ // a raw goal), no TT hit intervened, and the bound prunes cover past the
483
+ // attempted length.
484
+ let provenThrough = -1;
485
+ if (!this.capped &&
486
+ !this.limitHit &&
487
+ !this.sawGoal &&
488
+ !this.ttTouched &&
489
+ this.minExcess > length3 &&
490
+ this.minExcess !== Number.MAX_SAFE_INTEGER) {
491
+ if (typeof process !== "undefined" && process.env?.CUBE4_PROOF_STATS === "1") {
492
+ proofStats.clean++;
493
+ proofStats.jumpSum += this.minExcess - 1 - length3;
494
+ }
495
+ provenThrough = this.minExcess - 1;
496
+ }
497
+ // `validatedThrough` is scoped to THIS call's validator predicate (all
498
+ // calls sharing one solver instance use one predicate): rejected raw
499
+ // goals are not goals for it, and TT hits are proof-compatible (entries
500
+ // mean "no goal under the call's predicate at any length <= stored").
501
+ // Every walked branch leaves a claim: bound prunes record their
502
+ // threshold (noteExcess), fringe rejections record `length3 + 1` below,
503
+ // and TT-hit subtrees record `ttCoverage`.
504
+ let validatedThrough = -1;
505
+ if (!this.capped && !this.limitHit) {
506
+ let bound = length3;
507
+ if (this.minExcess !== Number.MAX_SAFE_INTEGER && this.minExcess - 1 > bound) {
508
+ bound = this.minExcess - 1;
509
+ }
510
+ if (this.ttCoverage < bound)
511
+ bound = this.ttCoverage;
512
+ validatedThrough = bound;
513
+ }
514
+ return { moves: null, provenThrough, validatedThrough, nodesUsed: this.nodeCount };
515
+ }
516
+ }
517
+ /** Temporary instrumentation for proof-statistics (CUBE4_PROOF_STATS=1). */
518
+ export const proofStats = {
519
+ total: 0,
520
+ capped: 0,
521
+ limitHit: 0,
522
+ sawGoal: 0,
523
+ ttTouched: 0,
524
+ clean: 0,
525
+ jumpSum: 0
526
+ };
527
+ export function resetProofStats() {
528
+ proofStats.total = 0;
529
+ proofStats.capped = 0;
530
+ proofStats.limitHit = 0;
531
+ proofStats.sawGoal = 0;
532
+ proofStats.ttTouched = 0;
533
+ proofStats.clean = 0;
534
+ proofStats.jumpSum = 0;
109
535
  }
110
- const defaultSolver = new FastPhase3Solver();
111
536
  /**
112
537
  * Solve Phase 3 with iterative deepening on a single Phase3Start.
113
538
  */
114
- export async function solvePhase3(start, center3Tables, edge3Tables, maxLength = 20) {
539
+ export async function solvePhase3(start, center3Tables, edge3Tables, maxLength = 20, maxNodes = 80_000) {
540
+ const solver = new FastPhase3Solver();
115
541
  const sym = getSymTables();
116
542
  const symIdx = getMvSymFrom(start.edge, start.edge, 20, sym.sym2Raw, sym.sym2Mask, sym.raw2Sym, sym.mvrot, sym.mvroto);
117
543
  const edist = (edge3Tables.distances[(symIdx >> 3) >> 1] >> (((symIdx >> 3) & 1) * 4)) & 0x0f;
@@ -120,7 +546,7 @@ export async function solvePhase3(start, center3Tables, edge3Tables, maxLength =
120
546
  return null;
121
547
  const minLength = Math.max(cdist, edist);
122
548
  for (let l = minLength; l <= maxLength; l++) {
123
- const moves = await defaultSolver.solveLength(start.edge, start.ct3, edist, l, center3Tables, edge3Tables.distances);
549
+ const moves = solver.solveLength(start.edge, start.ct3, edist, l, center3Tables, edge3Tables.distances, maxNodes);
124
550
  if (moves) {
125
551
  return {
126
552
  moves,
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Placement of the whole-cube rotation fix inside a 4x4 solution.
3
+ *
4
+ * The reduction ends with the cube solved in some orientation, so the solve
5
+ * is `P ++ W(R) ++ Q` where `W(R)` are the 2-4 wide moves of the rotation R
6
+ * (`x` = `Rw Lw'`, ...). A rotation commutes with any move sequence once the
7
+ * sequence's faces are re-mapped:
8
+ *
9
+ * X ++ W(R) ≡ W(R) ++ rotateAlgorithm(X, R)
10
+ *
11
+ * so the fix can sit at ANY point of the solution (with the moves after it
12
+ * re-mapped), and a two-rotation fix can be split across two points. The
13
+ * total is unchanged except at the insertion junctions, where the fix can
14
+ * cancel against same-axis wide moves (`Uw2 · y2` = `Uw2 · Uw2 Dw2` = `Dw2`).
15
+ * `placeRotationFix` picks the placement with the shortest simplified total.
16
+ */
17
+ import type { Cube4Move } from "../types.js";
18
+ /** Wide-move words of the 9 single rotations. */
19
+ export declare const ROTATION_WORDS: Readonly<Record<string, readonly Cube4Move[]>>;
20
+ /**
21
+ * Shortest simplified solution equivalent to `prefix ++ W(rotation) ++
22
+ * suffix` (same effect on every state), over every run-boundary insertion
23
+ * point of the fix and every minimal factorization of the rotation. Only
24
+ * run boundaries matter: the fix can only merge with runs on its own axis,
25
+ * and re-mapping preserves the run structure of what follows it.
26
+ */
27
+ export declare function placeRotationFix(prefix: readonly Cube4Move[], rotation: readonly string[], suffix: readonly Cube4Move[]): Cube4Move[];
28
+ //# sourceMappingURL=rotation-fix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rotation-fix.d.ts","sourceRoot":"","sources":["../../../../src/puzzles/cube4/reduction/rotation-fix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,iDAAiD;AACjD,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,CAUzE,CAAC;AA0FF;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,SAAS,EAAE,EAC5B,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,SAAS,EAAE,CA0Fb"}