scrabble-solver 2.16.7 → 2.17.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 (276) hide show
  1. package/.nx/workspace-data/d/server-process.json +2 -2
  2. package/.nx/workspace-data/file-map.json +548 -572
  3. package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
  4. package/.nx/workspace-data/lockfile-nodes.hash +1 -1
  5. package/.nx/workspace-data/nx_files.nxt +0 -0
  6. package/.nx/workspace-data/parsed-lock-file.dependencies.json +185 -160
  7. package/.nx/workspace-data/parsed-lock-file.nodes.json +406 -370
  8. package/.nx/workspace-data/project-graph.json +656 -552
  9. package/.nx/workspace-data/source-maps.json +96 -0
  10. package/.oxlintrc.json +6 -0
  11. package/CLAUDE.md +22 -17
  12. package/README.md +9 -0
  13. package/bun.lock +107 -133
  14. package/cypress/e2e/app.spec.cy.ts +4 -4
  15. package/cypress/e2e/features/solver-paths.ts +66 -0
  16. package/cypress/e2e/features.spec.cy.ts +1 -0
  17. package/cypress/support/lib/actions.ts +28 -9
  18. package/lerna.json +1 -1
  19. package/package.json +8 -7
  20. package/packages/configs/package.json +3 -3
  21. package/packages/constants/package.json +1 -1
  22. package/packages/dictionaries/README.md +1 -1
  23. package/packages/dictionaries/build/Dictionaries.d.ts +2 -2
  24. package/packages/dictionaries/build/Dictionaries.js +6 -6
  25. package/packages/dictionaries/build/lib/DiskCache.d.ts +11 -4
  26. package/packages/dictionaries/build/lib/DiskCache.js +27 -10
  27. package/packages/dictionaries/build/lib/LayeredCache.d.ts +4 -4
  28. package/packages/dictionaries/build/lib/LayeredCache.js +3 -3
  29. package/packages/dictionaries/build/lib/MemoryCache.d.ts +4 -4
  30. package/packages/dictionaries/build/lib/MemoryCache.js +2 -2
  31. package/packages/dictionaries/build/lib/createCacheTimestampComparator.d.ts +2 -2
  32. package/packages/dictionaries/build/lib/downloadDictionary.d.ts +2 -2
  33. package/packages/dictionaries/build/lib/downloadDictionary.js +4 -3
  34. package/packages/dictionaries/build/lib/getDictionaryFilepath.d.ts +3 -1
  35. package/packages/dictionaries/build/lib/getDictionaryFilepath.js +8 -3
  36. package/packages/dictionaries/package.json +7 -6
  37. package/packages/dictionaries/src/Dictionaries.ts +12 -12
  38. package/packages/dictionaries/src/lib/DiskCache.test.ts +85 -0
  39. package/packages/dictionaries/src/lib/DiskCache.ts +34 -14
  40. package/packages/dictionaries/src/lib/LayeredCache.ts +7 -7
  41. package/packages/dictionaries/src/lib/MemoryCache.ts +6 -6
  42. package/packages/dictionaries/src/lib/createCacheTimestampComparator.ts +2 -2
  43. package/packages/dictionaries/src/lib/downloadDictionary.ts +5 -4
  44. package/packages/dictionaries/src/lib/getDictionaryFilepath.ts +7 -2
  45. package/packages/gaddag/build/Gaddag.d.ts +39 -0
  46. package/packages/gaddag/build/Gaddag.js +128 -0
  47. package/packages/gaddag/build/buildGaddag.d.ts +10 -0
  48. package/packages/gaddag/build/buildGaddag.js +360 -0
  49. package/packages/gaddag/build/constants.d.ts +12 -0
  50. package/packages/gaddag/build/constants.js +15 -0
  51. package/packages/gaddag/build/index.d.ts +3 -0
  52. package/packages/gaddag/build/index.js +13 -0
  53. package/packages/logger/package.json +1 -1
  54. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  55. package/packages/scrabble-solver/.next/build-manifest.json +13 -13
  56. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  57. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  58. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  59. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  60. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  61. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  62. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  63. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  64. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  65. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  66. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  67. package/packages/scrabble-solver/.next/diagnostics/framework.json +1 -1
  68. package/packages/scrabble-solver/.next/next-server.js.nft.json +1 -1
  69. package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
  70. package/packages/scrabble-solver/.next/required-server-files.js +3 -2
  71. package/packages/scrabble-solver/.next/required-server-files.json +3 -2
  72. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  73. package/packages/scrabble-solver/.next/server/chunks/129.js +1 -0
  74. package/packages/scrabble-solver/.next/server/chunks/30.js +1 -1
  75. package/packages/scrabble-solver/.next/server/chunks/392.js +1 -1
  76. package/packages/scrabble-solver/.next/server/chunks/671.js +1 -1
  77. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  78. package/packages/scrabble-solver/.next/server/chunks/785.js +1 -0
  79. package/packages/scrabble-solver/.next/server/chunks/791.js +1 -1
  80. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  81. package/packages/scrabble-solver/.next/server/pages/404.html +1 -1
  82. package/packages/scrabble-solver/.next/server/pages/500.html +1 -1
  83. package/packages/scrabble-solver/.next/server/pages/_app.js +1 -1
  84. package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
  85. package/packages/scrabble-solver/.next/server/pages/_error.js +1 -1
  86. package/packages/scrabble-solver/.next/server/pages/_error.js.nft.json +1 -1
  87. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
  88. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  89. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js +1 -1
  90. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  91. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  92. package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
  93. package/packages/scrabble-solver/.next/server/pages/api/verify.js +1 -1
  94. package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
  95. package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
  96. package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
  97. package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
  98. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  99. package/packages/scrabble-solver/.next/server/pages/not-found.html +1 -1
  100. package/packages/scrabble-solver/.next/server/pages/not-found.js.nft.json +1 -1
  101. package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -1
  102. package/packages/scrabble-solver/.next/server/webpack-runtime.js +1 -1
  103. package/packages/scrabble-solver/.next/static/{vYpMmuC_yEurZGcyGVnLf → OLE8_ZInlGH7ENU0NIOpf}/_buildManifest.js +1 -1
  104. package/packages/scrabble-solver/.next/static/chunks/583.7a46807be14c377e.js +1 -0
  105. package/packages/scrabble-solver/.next/static/chunks/830.661816bf0de8c2f1.js +1 -0
  106. package/packages/scrabble-solver/.next/static/chunks/{main-852475ca502b4e49.js → main-5b999f3f24ebfa84.js} +2 -2
  107. package/packages/scrabble-solver/.next/static/chunks/pages/_app-7024e7a27ffbb2fa.js +7 -0
  108. package/packages/scrabble-solver/.next/static/chunks/pages/{index-ec0f6aeeaf7be227.js → index-8b398b0a5d870773.js} +1 -1
  109. package/packages/scrabble-solver/.next/static/chunks/webpack-3943d624bbb47a64.js +1 -0
  110. package/packages/scrabble-solver/.next/static/css/aedeaada218d7280.css +2 -0
  111. package/packages/scrabble-solver/.next/trace +21 -21
  112. package/packages/scrabble-solver/.next/trace-build +1 -1
  113. package/packages/scrabble-solver/next.config.js +0 -6
  114. package/packages/scrabble-solver/package.json +10 -10
  115. package/packages/scrabble-solver/public/service-worker.js +2 -2
  116. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +4 -1
  117. package/packages/scrabble-solver/src/components/Board/hooks/useGrid.ts +17 -6
  118. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.module.scss +7 -22
  119. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.tsx +1 -8
  120. package/packages/scrabble-solver/src/components/Results/Result.tsx +0 -3
  121. package/packages/scrabble-solver/src/components/Results/Results.module.scss +0 -1
  122. package/packages/scrabble-solver/src/components/Results/Results.tsx +10 -2
  123. package/packages/scrabble-solver/src/components/Results/types.ts +3 -3
  124. package/packages/scrabble-solver/src/components/Solver/Solver.tsx +4 -3
  125. package/packages/scrabble-solver/src/components/Tooltip/useTooltip.ts +1 -1
  126. package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/[word].ts +2 -2
  127. package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/index.ts +80 -3
  128. package/packages/scrabble-solver/src/pages/api/solve.ts +2 -2
  129. package/packages/scrabble-solver/src/pages/api/verify.ts +12 -3
  130. package/packages/scrabble-solver/src/sdk/solve.ts +13 -7
  131. package/packages/scrabble-solver/src/sdk/verify.ts +12 -6
  132. package/packages/scrabble-solver/src/service-worker/appShell.test.ts +86 -0
  133. package/packages/scrabble-solver/src/service-worker/appShell.ts +39 -0
  134. package/packages/scrabble-solver/src/service-worker/index.ts +4 -5
  135. package/packages/scrabble-solver/src/serviceWorkerManager.ts +1 -18
  136. package/packages/scrabble-solver/src/solver-worker/client.test.ts +151 -0
  137. package/packages/scrabble-solver/src/solver-worker/client.ts +106 -0
  138. package/packages/scrabble-solver/src/solver-worker/dictionaries/constants.ts +9 -0
  139. package/packages/scrabble-solver/src/solver-worker/dictionaries/getDictionary.ts +21 -0
  140. package/packages/scrabble-solver/src/solver-worker/dictionaries/index.ts +2 -0
  141. package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidateDictionary.test.ts +199 -0
  142. package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidateDictionary.ts +86 -0
  143. package/packages/scrabble-solver/src/solver-worker/dictionaries/revalidatedAt.ts +43 -0
  144. package/packages/scrabble-solver/src/solver-worker/getGaddag.test.ts +109 -0
  145. package/packages/scrabble-solver/src/solver-worker/getGaddag.ts +62 -0
  146. package/packages/scrabble-solver/src/solver-worker/index.ts +1 -0
  147. package/packages/scrabble-solver/src/solver-worker/index.worker.ts +124 -0
  148. package/packages/scrabble-solver/src/solver-worker/messages.ts +20 -0
  149. package/packages/scrabble-solver/src/state/index.ts +1 -0
  150. package/packages/scrabble-solver/src/state/sagas.ts +19 -9
  151. package/packages/scrabble-solver/src/state/useTypedStore.ts +6 -0
  152. package/packages/scrabble-solver/src/types/api.ts +1 -2
  153. package/packages/scrabble-solver/tsconfig.json +4 -0
  154. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  155. package/packages/solver/README.md +24 -0
  156. package/packages/solver/benchmarks/index.ts +15 -0
  157. package/packages/solver/benchmarks/lib.ts +31 -0
  158. package/packages/solver/benchmarks/renderChart.ts +165 -0
  159. package/packages/solver/benchmarks/results/chart.svg +65 -0
  160. package/packages/solver/benchmarks/runBenchmarks.ts +66 -0
  161. package/packages/solver/benchmarks/scenarios.ts +64 -0
  162. package/packages/solver/benchmarks/types.ts +16 -0
  163. package/packages/solver/benchmarks/updateReadme.ts +53 -0
  164. package/packages/solver/build/MoveGenerator.d.ts +116 -0
  165. package/packages/solver/build/MoveGenerator.js +686 -0
  166. package/packages/solver/build/index.d.ts +0 -13
  167. package/packages/solver/build/index.js +1 -27
  168. package/packages/solver/build/solve.d.ts +2 -2
  169. package/packages/solver/build/solve.js +3 -19
  170. package/packages/solver/package.json +7 -6
  171. package/packages/solver/src/MoveGenerator.test.ts +1009 -0
  172. package/packages/solver/src/MoveGenerator.ts +926 -0
  173. package/packages/solver/src/ReferenceSolver.ts +401 -0
  174. package/packages/solver/src/index.ts +0 -13
  175. package/packages/solver/src/solve.test.ts +21 -21
  176. package/packages/solver/src/solve.ts +4 -22
  177. package/packages/solver/tsconfig.json +1 -1
  178. package/packages/types/build/Cell.d.ts +1 -0
  179. package/packages/types/build/Cell.js +3 -0
  180. package/packages/types/build/Result.d.ts +3 -2
  181. package/packages/types/build/Result.js +11 -6
  182. package/packages/types/build/ResultJson.d.ts +5 -4
  183. package/packages/types/build/index.d.ts +0 -6
  184. package/packages/types/build/index.js +1 -9
  185. package/packages/types/build/readResultCells.d.ts +5 -0
  186. package/packages/types/build/readResultCells.js +75 -0
  187. package/packages/types/package.json +4 -3
  188. package/packages/types/src/Cell.ts +4 -0
  189. package/packages/types/src/Result.ts +15 -6
  190. package/packages/types/src/ResultJson.ts +5 -5
  191. package/packages/types/src/index.ts +0 -6
  192. package/packages/types/src/readResultCells.test.ts +140 -0
  193. package/packages/types/src/readResultCells.ts +90 -0
  194. package/packages/word-definitions/package.json +2 -2
  195. package/packages/word-lists/package.json +2 -2
  196. package/packages/scrabble-solver/.next/static/chunks/pages/_app-a5bbe02710f13d05.js +0 -7
  197. package/packages/scrabble-solver/.next/static/chunks/webpack-1a7a166b34ed8d24.js +0 -1
  198. package/packages/scrabble-solver/.next/static/css/0af35adb4bab56f0.css +0 -2
  199. package/packages/scrabble-solver/src/sdk/getDictionary.ts +0 -9
  200. package/packages/scrabble-solver/src/service-worker/average.ts +0 -7
  201. package/packages/scrabble-solver/src/service-worker/dictionaries/constants.ts +0 -5
  202. package/packages/scrabble-solver/src/service-worker/dictionaries/getDictionary.ts +0 -20
  203. package/packages/scrabble-solver/src/service-worker/dictionaries/index.ts +0 -2
  204. package/packages/scrabble-solver/src/service-worker/dictionaries/revalidateDictionary.ts +0 -33
  205. package/packages/scrabble-solver/src/service-worker/getTrie.ts +0 -24
  206. package/packages/scrabble-solver/src/service-worker/routeSolveRequests.ts +0 -91
  207. package/packages/scrabble-solver/src/service-worker/routeVerifyRequests.ts +0 -37
  208. package/packages/solver/build/areDigraphsValid.d.ts +0 -2
  209. package/packages/solver/build/areDigraphsValid.js +0 -17
  210. package/packages/solver/build/fillPattern.d.ts +0 -6
  211. package/packages/solver/build/fillPattern.js +0 -52
  212. package/packages/solver/build/generateEndIndices.d.ts +0 -2
  213. package/packages/solver/build/generateEndIndices.js +0 -17
  214. package/packages/solver/build/generateHorizontalPatterns.d.ts +0 -2
  215. package/packages/solver/build/generateHorizontalPatterns.js +0 -16
  216. package/packages/solver/build/generatePattern.d.ts +0 -7
  217. package/packages/solver/build/generatePattern.js +0 -20
  218. package/packages/solver/build/generatePatterns.d.ts +0 -2
  219. package/packages/solver/build/generatePatterns.js +0 -11
  220. package/packages/solver/build/generateStartIndices.d.ts +0 -2
  221. package/packages/solver/build/generateStartIndices.js +0 -16
  222. package/packages/solver/build/generateVectors.d.ts +0 -7
  223. package/packages/solver/build/generateVectors.js +0 -7
  224. package/packages/solver/build/generateVerticalPatterns.d.ts +0 -2
  225. package/packages/solver/build/generateVerticalPatterns.js +0 -16
  226. package/packages/solver/build/getCellsScore.d.ts +0 -2
  227. package/packages/solver/build/getCellsScore.js +0 -18
  228. package/packages/solver/build/getPatternHash.d.ts +0 -2
  229. package/packages/solver/build/getPatternHash.js +0 -14
  230. package/packages/solver/build/getPatternScore.d.ts +0 -2
  231. package/packages/solver/build/getPatternScore.js +0 -22
  232. package/packages/solver/build/getUniquePatterns.d.ts +0 -2
  233. package/packages/solver/build/getUniquePatterns.js +0 -17
  234. package/packages/solver/src/areDigraphsValid.ts +0 -18
  235. package/packages/solver/src/fillPattern.test.ts +0 -107
  236. package/packages/solver/src/fillPattern.ts +0 -68
  237. package/packages/solver/src/generateEndIndices.test.ts +0 -23
  238. package/packages/solver/src/generateEndIndices.ts +0 -19
  239. package/packages/solver/src/generateHorizontalPatterns.test.ts +0 -31
  240. package/packages/solver/src/generateHorizontalPatterns.ts +0 -15
  241. package/packages/solver/src/generatePattern.ts +0 -33
  242. package/packages/solver/src/generatePatterns.test.ts +0 -13
  243. package/packages/solver/src/generatePatterns.ts +0 -11
  244. package/packages/solver/src/generateStartIndices.test.ts +0 -19
  245. package/packages/solver/src/generateStartIndices.ts +0 -17
  246. package/packages/solver/src/generateVectors.test.ts +0 -12
  247. package/packages/solver/src/generateVectors.ts +0 -10
  248. package/packages/solver/src/generateVerticalPatterns.test.ts +0 -32
  249. package/packages/solver/src/generateVerticalPatterns.ts +0 -15
  250. package/packages/solver/src/getCellsScore.ts +0 -21
  251. package/packages/solver/src/getPatternHash.test.ts +0 -17
  252. package/packages/solver/src/getPatternHash.ts +0 -12
  253. package/packages/solver/src/getPatternScore.test.ts +0 -204
  254. package/packages/solver/src/getPatternScore.ts +0 -23
  255. package/packages/solver/src/getUniquePatterns.ts +0 -19
  256. package/packages/types/build/CollisionJson.d.ts +0 -2
  257. package/packages/types/build/CollisionJson.js +0 -2
  258. package/packages/types/build/FinalPattern.d.ts +0 -6
  259. package/packages/types/build/FinalPattern.js +0 -14
  260. package/packages/types/build/HorizontalPattern.d.ts +0 -5
  261. package/packages/types/build/HorizontalPattern.js +0 -34
  262. package/packages/types/build/Pattern.d.ts +0 -20
  263. package/packages/types/build/Pattern.js +0 -50
  264. package/packages/types/build/PatternJson.d.ts +0 -6
  265. package/packages/types/build/PatternJson.js +0 -2
  266. package/packages/types/build/VerticalPattern.d.ts +0 -5
  267. package/packages/types/build/VerticalPattern.js +0 -34
  268. package/packages/types/src/CollisionJson.ts +0 -3
  269. package/packages/types/src/FinalPattern.ts +0 -14
  270. package/packages/types/src/HorizontalPattern.ts +0 -37
  271. package/packages/types/src/Pattern.ts +0 -71
  272. package/packages/types/src/PatternJson.ts +0 -7
  273. package/packages/types/src/VerticalPattern.ts +0 -37
  274. /package/packages/scrabble-solver/.next/static/{vYpMmuC_yEurZGcyGVnLf → OLE8_ZInlGH7ENU0NIOpf}/_ssgManifest.js +0 -0
  275. /package/packages/scrabble-solver/src/{service-worker → solver-worker}/dictionaries/expirationManager.ts +0 -0
  276. /package/packages/scrabble-solver/src/{service-worker → solver-worker}/dictionaries/getDictionaryUrl.ts +0 -0
@@ -0,0 +1,926 @@
1
+ /* eslint-disable max-depth, max-lines, max-statements, no-bitwise */
2
+ import { type Gaddag, LAST_ARC_FLAG, LETTER_MASK, SEPARATOR } from '@kamilmielnik/gaddag';
3
+ import { BONUS_CHARACTER, BONUS_WORD } from '@scrabble-solver/constants';
4
+ import {
5
+ type Board,
6
+ type Config,
7
+ type ResultJson,
8
+ type Tile,
9
+ isMultiplierBingo,
10
+ isScoreBingo,
11
+ } from '@scrabble-solver/types';
12
+
13
+ const MAX_ALPHABET_SIZE = 64;
14
+
15
+ // The result sort key packs (direction, line, start, end) into one integer.
16
+ const MAX_BOARD_DIMENSION = 32;
17
+
18
+ /**
19
+ * Anchor-based move generation over a {@link Gaddag} (Gordon, 1994).
20
+ *
21
+ * For every anchor (an empty cell adjacent to a tile, or the board center when
22
+ * the board is empty) the generator extends leftwards from the anchor through
23
+ * the reversed-prefix part of the GADDAG, crosses the separator, and extends
24
+ * rightwards — validating perpendicular words with precomputed cross-check
25
+ * masks. Both directions run the same code path: the vertical pass remaps
26
+ * (line, position) to transposed coordinates. Each placement is scored and
27
+ * emitted as a compact {@link ResultJson} — placed tiles plus placement — from
28
+ * which {@link Result.fromJson} rebuilds the full cells and collisions against
29
+ * the board.
30
+ */
31
+ export class MoveGenerator {
32
+ private readonly gaddag: Gaddag;
33
+ private readonly config: Config;
34
+
35
+ private readonly width: number;
36
+ private readonly height: number;
37
+ private readonly cellsCount: number;
38
+
39
+ // Config alphabet (tile characters, including two-character tiles).
40
+ private readonly alphaSize: number;
41
+ private readonly alphaChars: string[];
42
+ private readonly alphaLetter1: Int32Array;
43
+ private readonly alphaLetter2: Int32Array;
44
+ private readonly alphaPoints: Int32Array;
45
+
46
+ // CSR mapping from a GADDAG letter to the alphabet entries whose leading
47
+ // letter (in traversal order) is that letter. Moving leftwards reads a
48
+ // tile's characters reversed, so a two-character tile leads with its second
49
+ // letter. alphaSecond* hold the remaining letter to consume (0 if none).
50
+ private readonly leftAlphaStart: Int32Array;
51
+ private readonly leftAlphaList: Int32Array;
52
+ private readonly rightAlphaStart: Int32Array;
53
+ private readonly rightAlphaList: Int32Array;
54
+ private readonly alphaSecondLeft: Int32Array;
55
+ private readonly alphaSecondRight: Int32Array;
56
+
57
+ // Rack.
58
+ private readonly rackCounts: Int32Array;
59
+ private blanksLeft = 0;
60
+ private rackTotal = 0;
61
+
62
+ // Bonuses indexed by global cell index (y * width + x). Types: 0 none, 1 character, 2 word.
63
+ private readonly bonusType: Uint8Array;
64
+ private readonly bonusMult: Int32Array;
65
+ private readonly bonusReq: Int32Array;
66
+
67
+ // Board indexed by global cell index.
68
+ private readonly boardFilled: Uint8Array;
69
+ private readonly boardChar: string[];
70
+ private readonly boardIsBlank: Uint8Array;
71
+ private readonly boardScore: Int32Array;
72
+ private readonly boardLetter1: Int32Array;
73
+ private readonly boardLetter2: Int32Array;
74
+ private readonly anchor: Uint8Array;
75
+ private boardIsEmpty = true;
76
+
77
+ // Current pass (direction) data, indexed by line * lineLength + position.
78
+ private isHorizontal = true;
79
+ private lineLength = 0;
80
+ private linesCount = 0;
81
+ private readonly passFilled: Uint8Array;
82
+ private readonly passLetter1: Int32Array;
83
+ private readonly passLetter2: Int32Array;
84
+ private readonly passScore: Int32Array;
85
+ private readonly passGlobal: Int32Array;
86
+ private readonly passAnchor: Uint8Array;
87
+ private readonly maskLo: Int32Array;
88
+ private readonly maskHi: Int32Array;
89
+ private readonly crossBase: Int32Array;
90
+ private readonly hasCross: Uint8Array;
91
+
92
+ // Current line/anchor state.
93
+ private line = 0;
94
+ private lineBase = 0;
95
+ private anchorPos = 0;
96
+ private limit = 0;
97
+ private anchorRightOpen = false;
98
+ private leftMost = 0;
99
+
100
+ // Placement stack (indexed by position within the current line).
101
+ private readonly placedAt: Int32Array;
102
+ private readonly placedBlankAt: Uint8Array;
103
+ private placedCount = 0;
104
+
105
+ private readonly digraphs: string[];
106
+ private readonly blankScore: number;
107
+ private readonly rackSize: number;
108
+
109
+ // Single-tile placements are the only ones both passes can emit; multi-tile
110
+ // placements determine their line, span, and anchor uniquely.
111
+ private readonly seenSingleTiles = new Set<number>();
112
+ private readonly results: ResultJson[] = [];
113
+
114
+ // Board cells are immutable during a solve, so their JSON is shared between results.
115
+
116
+ // Sort keys reproducing the previous solver's result order:
117
+ // horizontal before vertical, then by line, start, and end position.
118
+ private readonly sortKeys: number[] = [];
119
+
120
+ // Secondary sort keys resolving ties within the same word span: the
121
+ // previous solver filled cells left to right, trying rack tiles in their
122
+ // original order (and, for blanks, alphabet characters in config order).
123
+ private readonly rankKeys: string[] = [];
124
+ private readonly alphaFirstRackIndex: Int32Array;
125
+ private blankRackIndex = 0;
126
+
127
+ constructor(gaddag: Gaddag, config: Config, board: Board, tiles: Tile[]) {
128
+ this.gaddag = gaddag;
129
+ this.config = config;
130
+ this.width = board.columnsCount;
131
+ this.height = board.rowsCount;
132
+ this.cellsCount = this.width * this.height;
133
+ this.blankScore = config.blankScore;
134
+ this.rackSize = config.rackSize;
135
+ this.digraphs = config.twoCharacterTiles;
136
+
137
+ const alphabet = config.alphabet;
138
+
139
+ if (alphabet.length > MAX_ALPHABET_SIZE) {
140
+ throw new Error(`Alphabets larger than ${MAX_ALPHABET_SIZE} tiles are not supported`);
141
+ }
142
+
143
+ if (this.width > MAX_BOARD_DIMENSION || this.height > MAX_BOARD_DIMENSION) {
144
+ throw new Error(`Boards larger than ${MAX_BOARD_DIMENSION}x${MAX_BOARD_DIMENSION} are not supported`);
145
+ }
146
+
147
+ this.alphaSize = alphabet.length;
148
+ this.alphaChars = alphabet;
149
+ this.alphaLetter1 = new Int32Array(this.alphaSize);
150
+ this.alphaLetter2 = new Int32Array(this.alphaSize);
151
+ this.alphaPoints = new Int32Array(this.alphaSize);
152
+
153
+ for (let index = 0; index < this.alphaSize; ++index) {
154
+ const character = alphabet[index];
155
+ this.alphaLetter1[index] = gaddag.getLetter(character.charCodeAt(0));
156
+ this.alphaLetter2[index] = character.length > 1 ? gaddag.getLetter(character.charCodeAt(1)) : 0;
157
+ this.alphaPoints[index] = config.pointsMap[character] || 0;
158
+ }
159
+
160
+ const lettersCount = MAX_ALPHABET_SIZE;
161
+ this.leftAlphaStart = new Int32Array(lettersCount + 1);
162
+ this.rightAlphaStart = new Int32Array(lettersCount + 1);
163
+ this.leftAlphaList = new Int32Array(this.alphaSize);
164
+ this.rightAlphaList = new Int32Array(this.alphaSize);
165
+ this.alphaSecondLeft = new Int32Array(this.alphaSize);
166
+ this.alphaSecondRight = new Int32Array(this.alphaSize);
167
+
168
+ for (let pass = 0; pass < 2; ++pass) {
169
+ for (let index = 0; index < this.alphaSize; ++index) {
170
+ const letter1 = this.alphaLetter1[index];
171
+ const letter2 = this.alphaLetter2[index];
172
+
173
+ if (letter1 <= 0 || letter2 < 0) {
174
+ continue;
175
+ }
176
+
177
+ const leftLead = letter2 === 0 ? letter1 : letter2;
178
+
179
+ if (pass === 0) {
180
+ ++this.leftAlphaStart[leftLead + 1];
181
+ ++this.rightAlphaStart[letter1 + 1];
182
+ } else {
183
+ this.leftAlphaList[this.leftAlphaStart[leftLead]++] = index;
184
+ this.rightAlphaList[this.rightAlphaStart[letter1]++] = index;
185
+ this.alphaSecondLeft[index] = letter2 === 0 ? 0 : letter1;
186
+ this.alphaSecondRight[index] = letter2;
187
+ }
188
+ }
189
+
190
+ if (pass === 0) {
191
+ for (let letter = 0; letter < lettersCount; ++letter) {
192
+ this.leftAlphaStart[letter + 1] += this.leftAlphaStart[letter];
193
+ this.rightAlphaStart[letter + 1] += this.rightAlphaStart[letter];
194
+ }
195
+ }
196
+ }
197
+
198
+ // The second pass advanced the start offsets by each bucket's size; shift them back.
199
+ for (let letter = lettersCount; letter > 0; --letter) {
200
+ this.leftAlphaStart[letter] = this.leftAlphaStart[letter - 1];
201
+ this.rightAlphaStart[letter] = this.rightAlphaStart[letter - 1];
202
+ }
203
+
204
+ this.leftAlphaStart[0] = 0;
205
+ this.rightAlphaStart[0] = 0;
206
+
207
+ this.rackCounts = new Int32Array(this.alphaSize);
208
+ this.alphaFirstRackIndex = new Int32Array(this.alphaSize).fill(tiles.length);
209
+ this.blankRackIndex = tiles.length;
210
+
211
+ for (let tileIndex = 0; tileIndex < tiles.length; ++tileIndex) {
212
+ const tile = tiles[tileIndex];
213
+
214
+ if (tile.isBlank) {
215
+ if (this.blanksLeft === 0) {
216
+ this.blankRackIndex = tileIndex;
217
+ }
218
+
219
+ ++this.blanksLeft;
220
+ ++this.rackTotal;
221
+ continue;
222
+ }
223
+
224
+ const alphaIndex = alphabet.indexOf(tile.character);
225
+
226
+ if (alphaIndex !== -1) {
227
+ if (this.rackCounts[alphaIndex] === 0) {
228
+ this.alphaFirstRackIndex[alphaIndex] = tileIndex;
229
+ }
230
+
231
+ ++this.rackCounts[alphaIndex];
232
+ ++this.rackTotal;
233
+ }
234
+ }
235
+
236
+ this.bonusType = new Uint8Array(this.cellsCount);
237
+ this.bonusMult = new Int32Array(this.cellsCount);
238
+ this.bonusReq = new Int32Array(this.cellsCount).fill(-1);
239
+
240
+ // Iterate backwards so the first matching bonus wins, mirroring config.getCellBonus.
241
+ for (let index = config.bonuses.length - 1; index >= 0; --index) {
242
+ const bonus = config.bonuses[index];
243
+
244
+ if (bonus.x < 0 || bonus.x >= this.width || bonus.y < 0 || bonus.y >= this.height) {
245
+ continue;
246
+ }
247
+
248
+ const cellIndex = bonus.y * this.width + bonus.x;
249
+
250
+ if (bonus.type === BONUS_CHARACTER) {
251
+ this.bonusType[cellIndex] = 1;
252
+ } else if (bonus.type === BONUS_WORD) {
253
+ this.bonusType[cellIndex] = 2;
254
+ } else {
255
+ this.bonusType[cellIndex] = 0;
256
+ }
257
+
258
+ this.bonusMult[cellIndex] = bonus.multiplier;
259
+ this.bonusReq[cellIndex] = typeof bonus.score === 'number' ? bonus.score : -1;
260
+ }
261
+
262
+ this.boardFilled = new Uint8Array(this.cellsCount);
263
+ this.boardChar = Array.from<string>({ length: this.cellsCount });
264
+ this.boardIsBlank = new Uint8Array(this.cellsCount);
265
+ this.boardScore = new Int32Array(this.cellsCount);
266
+ this.boardLetter1 = new Int32Array(this.cellsCount);
267
+ this.boardLetter2 = new Int32Array(this.cellsCount);
268
+ this.anchor = new Uint8Array(this.cellsCount);
269
+
270
+ for (let y = 0; y < this.height; ++y) {
271
+ const row = board.rows[y];
272
+
273
+ for (let x = 0; x < this.width; ++x) {
274
+ const cell = row[x];
275
+
276
+ if (!cell.isFilled()) {
277
+ continue;
278
+ }
279
+
280
+ const cellIndex = y * this.width + x;
281
+ const character = cell.tile.character;
282
+ this.boardIsEmpty = false;
283
+ this.boardFilled[cellIndex] = 1;
284
+ this.boardChar[cellIndex] = character;
285
+ this.boardIsBlank[cellIndex] = cell.tile.isBlank ? 1 : 0;
286
+ this.boardScore[cellIndex] = cell.tile.isBlank ? this.blankScore : config.pointsMap[character] || 0;
287
+ this.boardLetter1[cellIndex] = gaddag.getLetter(character.charCodeAt(0));
288
+ this.boardLetter2[cellIndex] = character.length > 1 ? gaddag.getLetter(character.charCodeAt(1)) : 0;
289
+ }
290
+ }
291
+
292
+ for (let y = 0; y < this.height; ++y) {
293
+ for (let x = 0; x < this.width; ++x) {
294
+ const cellIndex = y * this.width + x;
295
+
296
+ if (this.boardFilled[cellIndex] === 1) {
297
+ continue;
298
+ }
299
+
300
+ const up = y > 0 && this.boardFilled[cellIndex - this.width] === 1;
301
+ const down = y < this.height - 1 && this.boardFilled[cellIndex + this.width] === 1;
302
+ const left = x > 0 && this.boardFilled[cellIndex - 1] === 1;
303
+ const right = x < this.width - 1 && this.boardFilled[cellIndex + 1] === 1;
304
+ this.anchor[cellIndex] = up || down || left || right ? 1 : 0;
305
+ }
306
+ }
307
+
308
+ this.passFilled = new Uint8Array(this.cellsCount);
309
+ this.passLetter1 = new Int32Array(this.cellsCount);
310
+ this.passLetter2 = new Int32Array(this.cellsCount);
311
+ this.passScore = new Int32Array(this.cellsCount);
312
+ this.passGlobal = new Int32Array(this.cellsCount);
313
+ this.passAnchor = new Uint8Array(this.cellsCount);
314
+ this.maskLo = new Int32Array(this.cellsCount);
315
+ this.maskHi = new Int32Array(this.cellsCount);
316
+ this.crossBase = new Int32Array(this.cellsCount);
317
+ this.hasCross = new Uint8Array(this.cellsCount);
318
+ const maxLineLength = Math.max(this.width, this.height);
319
+ this.placedAt = new Int32Array(maxLineLength).fill(-1);
320
+ this.placedBlankAt = new Uint8Array(maxLineLength);
321
+ }
322
+
323
+ public run(): ResultJson[] {
324
+ if (this.rackTotal === 0) {
325
+ return this.results;
326
+ }
327
+
328
+ this.runPass(true);
329
+ this.runPass(false);
330
+
331
+ // Reorder results to match the previous solver's enumeration order, so
332
+ // that ties (equal points) resolve to the same result in the UI.
333
+ const order = this.results.map((_, index) => index);
334
+ const { rankKeys, sortKeys } = this;
335
+ order.sort((a, b) => {
336
+ const difference = sortKeys[a] - sortKeys[b];
337
+
338
+ if (difference !== 0) {
339
+ return difference;
340
+ }
341
+
342
+ const rankA = rankKeys[a];
343
+ const rankB = rankKeys[b];
344
+
345
+ if (rankA < rankB) {
346
+ return -1;
347
+ }
348
+
349
+ return rankA > rankB ? 1 : 0;
350
+ });
351
+
352
+ return order.map((resultIndex, index) => {
353
+ const result = this.results[resultIndex];
354
+ result.id = index;
355
+ return result;
356
+ });
357
+ }
358
+
359
+ private runPass(horizontal: boolean): void {
360
+ this.isHorizontal = horizontal;
361
+ this.linesCount = horizontal ? this.height : this.width;
362
+ this.lineLength = horizontal ? this.width : this.height;
363
+ this.setupPass();
364
+
365
+ const centerLine = horizontal ? Math.floor(this.height / 2) : Math.floor(this.width / 2);
366
+ const centerPos = horizontal ? Math.floor(this.width / 2) : Math.floor(this.height / 2);
367
+
368
+ for (let line = 0; line < this.linesCount; ++line) {
369
+ this.line = line;
370
+ this.lineBase = line * this.lineLength;
371
+
372
+ for (let position = 0; position < this.lineLength; ++position) {
373
+ const isAnchor = this.boardIsEmpty
374
+ ? line === centerLine && position === centerPos
375
+ : this.passAnchor[this.lineBase + position] === 1;
376
+
377
+ if (!isAnchor) {
378
+ continue;
379
+ }
380
+
381
+ this.anchorPos = position;
382
+ this.anchorRightOpen = position + 1 >= this.lineLength || this.passFilled[this.lineBase + position + 1] === 0;
383
+
384
+ let limit = 0;
385
+ const maxLimit = this.rackTotal - 1;
386
+
387
+ for (let left = position - 1; left >= 0 && limit < maxLimit; --left) {
388
+ const index = this.lineBase + left;
389
+
390
+ if (this.passFilled[index] === 1 || (!this.boardIsEmpty && this.passAnchor[index] === 1)) {
391
+ break;
392
+ }
393
+
394
+ ++limit;
395
+ }
396
+
397
+ this.limit = limit;
398
+ this.generateLeft(position, this.gaddag.rootRef);
399
+ }
400
+ }
401
+ }
402
+
403
+ private setupPass(): void {
404
+ const { lineLength, linesCount } = this;
405
+
406
+ for (let line = 0; line < linesCount; ++line) {
407
+ const base = line * lineLength;
408
+
409
+ for (let position = 0; position < lineLength; ++position) {
410
+ const passIndex = base + position;
411
+ const globalIndex = this.isHorizontal ? passIndex : position * this.width + line;
412
+ this.passGlobal[passIndex] = globalIndex;
413
+ this.passFilled[passIndex] = this.boardFilled[globalIndex];
414
+ this.passLetter1[passIndex] = this.boardLetter1[globalIndex];
415
+ this.passLetter2[passIndex] = this.boardLetter2[globalIndex];
416
+ this.passScore[passIndex] = this.boardScore[globalIndex];
417
+ this.passAnchor[passIndex] = this.anchor[globalIndex];
418
+ }
419
+ }
420
+
421
+ this.computeCrossChecks();
422
+ }
423
+
424
+ /**
425
+ * For every empty cell with perpendicular neighbors, computes the set of
426
+ * placeable tile characters (as a 64-bit mask), the sum of the existing
427
+ * perpendicular tiles' scores, and the perpendicular word span.
428
+ */
429
+ private computeCrossChecks(): void {
430
+ const { gaddag, lineLength, linesCount } = this;
431
+ const preLetters: number[] = [];
432
+ const postLetters: number[] = [];
433
+
434
+ for (let line = 0; line < linesCount; ++line) {
435
+ for (let position = 0; position < lineLength; ++position) {
436
+ const passIndex = line * lineLength + position;
437
+
438
+ if (this.passFilled[passIndex] === 1) {
439
+ continue;
440
+ }
441
+
442
+ const upFilled = line > 0 && this.passFilled[passIndex - lineLength] === 1;
443
+ const downFilled = line < linesCount - 1 && this.passFilled[passIndex + lineLength] === 1;
444
+
445
+ if (!upFilled && !downFilled) {
446
+ this.maskLo[passIndex] = -1;
447
+ this.maskHi[passIndex] = -1;
448
+ this.hasCross[passIndex] = 0;
449
+ continue;
450
+ }
451
+
452
+ let spanLo = line;
453
+
454
+ while (spanLo > 0 && this.passFilled[(spanLo - 1) * lineLength + position] === 1) {
455
+ --spanLo;
456
+ }
457
+
458
+ let spanHi = line;
459
+
460
+ while (spanHi < linesCount - 1 && this.passFilled[(spanHi + 1) * lineLength + position] === 1) {
461
+ ++spanHi;
462
+ }
463
+
464
+ const hasDigraphs = this.digraphs.length > 0;
465
+ const previousCharacter =
466
+ hasDigraphs && spanLo < line ? this.boardChar[this.passGlobal[passIndex - lineLength]] : '';
467
+ const nextCharacter =
468
+ hasDigraphs && spanHi > line ? this.boardChar[this.passGlobal[passIndex + lineLength]] : '';
469
+
470
+ preLetters.length = 0;
471
+ postLetters.length = 0;
472
+ let base = 0;
473
+ let lettersValid = true;
474
+ let crossSpellingValid = true;
475
+ let previousCrossCharacter = '';
476
+
477
+ for (let crossLine = spanLo; crossLine <= spanHi; ++crossLine) {
478
+ if (crossLine === line) {
479
+ continue;
480
+ }
481
+
482
+ const crossIndex = crossLine * lineLength + position;
483
+ base += this.passScore[crossIndex];
484
+
485
+ if (hasDigraphs) {
486
+ const character = this.boardChar[this.passGlobal[crossIndex]];
487
+
488
+ if (previousCrossCharacter !== '' && this.digraphs.includes(previousCrossCharacter + character)) {
489
+ crossSpellingValid = false;
490
+ }
491
+
492
+ previousCrossCharacter = crossLine === line - 1 ? '' : character;
493
+ }
494
+
495
+ const letters = crossLine < line ? preLetters : postLetters;
496
+ const letter1 = this.passLetter1[crossIndex];
497
+ const letter2 = this.passLetter2[crossIndex];
498
+
499
+ if (letter1 <= 0 || letter2 < 0) {
500
+ lettersValid = false;
501
+ }
502
+
503
+ letters.push(letter1);
504
+
505
+ if (letter2 !== 0) {
506
+ letters.push(letter2);
507
+ }
508
+ }
509
+
510
+ this.hasCross[passIndex] = 1;
511
+ this.crossBase[passIndex] = base;
512
+
513
+ let maskLo = 0;
514
+ let maskHi = 0;
515
+
516
+ if (lettersValid && crossSpellingValid) {
517
+ // The perpendicular word is pre + tile + post; its pure-reverse GADDAG
518
+ // path is rev(post), rev(tile characters), rev(pre). rev(post) is shared
519
+ // across all candidate tiles.
520
+ let postRef = gaddag.rootRef;
521
+
522
+ for (let index = postLetters.length - 1; index >= 0 && postRef !== 0; --index) {
523
+ postRef = gaddag.getArc(postRef, postLetters[index]);
524
+ }
525
+
526
+ if (postRef !== 0) {
527
+ for (let alpha = 0; alpha < this.alphaSize; ++alpha) {
528
+ const letter1 = this.alphaLetter1[alpha];
529
+ const letter2 = this.alphaLetter2[alpha];
530
+
531
+ if (letter1 <= 0 || letter2 < 0) {
532
+ continue;
533
+ }
534
+
535
+ if (hasDigraphs && this.spellsDigraphWithNeighbor(alpha, previousCharacter, nextCharacter)) {
536
+ continue;
537
+ }
538
+
539
+ let ref = postRef;
540
+
541
+ if (letter2 !== 0) {
542
+ ref = gaddag.getArc(ref, letter2);
543
+
544
+ if (ref === 0) {
545
+ continue;
546
+ }
547
+ }
548
+
549
+ ref = gaddag.getArc(ref, letter1);
550
+
551
+ for (let index = preLetters.length - 1; index >= 0 && ref !== 0; --index) {
552
+ ref = gaddag.getArc(ref, preLetters[index]);
553
+ }
554
+
555
+ if ((ref & 1) === 1) {
556
+ if (alpha < 32) {
557
+ maskLo |= 1 << alpha;
558
+ } else {
559
+ maskHi |= 1 << (alpha - 32);
560
+ }
561
+ }
562
+ }
563
+ }
564
+ }
565
+
566
+ this.maskLo[passIndex] = maskLo;
567
+ this.maskHi[passIndex] = maskHi;
568
+ }
569
+ }
570
+ }
571
+
572
+ /**
573
+ * A digraph must be played as its single tile, so a candidate that would
574
+ * spell a digraph with the adjacent perpendicular tile is not placeable.
575
+ * {@link record} enforces the same rule along the main word.
576
+ */
577
+ private spellsDigraphWithNeighbor(alpha: number, previousCharacter: string, nextCharacter: string): boolean {
578
+ const character = this.alphaChars[alpha];
579
+
580
+ return (
581
+ (previousCharacter !== '' && this.digraphs.includes(previousCharacter + character)) ||
582
+ (nextCharacter !== '' && this.digraphs.includes(character + nextCharacter))
583
+ );
584
+ }
585
+
586
+ private isOpen(position: number): boolean {
587
+ return position < 0 || position >= this.lineLength || this.passFilled[this.lineBase + position] === 0;
588
+ }
589
+
590
+ private generateLeft(position: number, ref: number): void {
591
+ const { gaddag, lineBase } = this;
592
+ const inBounds = position >= 0;
593
+ const passIndex = lineBase + position;
594
+
595
+ if (inBounds && this.passFilled[passIndex] === 1) {
596
+ // Read an existing tile (reversed character order when moving left).
597
+ const letter2 = this.passLetter2[passIndex];
598
+ let nextRef = ref;
599
+
600
+ if (letter2 !== 0) {
601
+ nextRef = letter2 > 0 ? gaddag.getArc(nextRef, letter2) : 0;
602
+
603
+ if (nextRef === 0) {
604
+ return;
605
+ }
606
+ }
607
+
608
+ const letter1 = this.passLetter1[passIndex];
609
+ nextRef = letter1 > 0 ? gaddag.getArc(nextRef, letter1) : 0;
610
+
611
+ if (nextRef === 0) {
612
+ return;
613
+ }
614
+
615
+ this.recordLeftIfWord(nextRef, position);
616
+ this.generateLeft(position - 1, nextRef);
617
+ return;
618
+ }
619
+
620
+ // The cell at `position` is empty or off-board, so the left part may stop here.
621
+ const separatorRef = gaddag.getArc(ref, SEPARATOR);
622
+
623
+ if (separatorRef !== 0) {
624
+ this.leftMost = position + 1;
625
+ this.generateRight(this.anchorPos + 1, separatorRef);
626
+ }
627
+
628
+ if (!inBounds || (position !== this.anchorPos && this.anchorPos - position > this.limit)) {
629
+ return;
630
+ }
631
+
632
+ this.placeAt(position, passIndex, ref, true);
633
+ }
634
+
635
+ private generateRight(position: number, ref: number): void {
636
+ const { gaddag, lineBase } = this;
637
+ const inBounds = position < this.lineLength;
638
+ const passIndex = lineBase + position;
639
+
640
+ if (inBounds && this.passFilled[passIndex] === 1) {
641
+ const letter1 = this.passLetter1[passIndex];
642
+ let nextRef = letter1 > 0 ? gaddag.getArc(ref, letter1) : 0;
643
+
644
+ if (nextRef === 0) {
645
+ return;
646
+ }
647
+
648
+ const letter2 = this.passLetter2[passIndex];
649
+
650
+ if (letter2 !== 0) {
651
+ nextRef = letter2 > 0 ? gaddag.getArc(nextRef, letter2) : 0;
652
+
653
+ if (nextRef === 0) {
654
+ return;
655
+ }
656
+ }
657
+
658
+ if ((nextRef & 1) === 1 && this.isOpen(position + 1)) {
659
+ this.record(this.leftMost, position);
660
+ }
661
+
662
+ this.generateRight(position + 1, nextRef);
663
+ return;
664
+ }
665
+
666
+ if (!inBounds) {
667
+ return;
668
+ }
669
+
670
+ this.placeAt(position, passIndex, ref, false);
671
+ }
672
+
673
+ /**
674
+ * Tries every rack tile (and blank interpretation) at an empty cell,
675
+ * recursing further. Iterates the state's arcs instead of the alphabet:
676
+ * deep GADDAG states have very few arcs, so this is much cheaper than
677
+ * probing every letter.
678
+ */
679
+ private placeAt(position: number, passIndex: number, ref: number, leftward: boolean): void {
680
+ let arcIndex = ref >>> 1;
681
+
682
+ if (arcIndex === 0 || this.placedCount >= this.rackTotal) {
683
+ return;
684
+ }
685
+
686
+ const maskLo = this.maskLo[passIndex];
687
+ const maskHi = this.maskHi[passIndex];
688
+
689
+ if (maskLo === 0 && maskHi === 0) {
690
+ return;
691
+ }
692
+
693
+ const { gaddag } = this;
694
+ const labels = gaddag.arcLabels;
695
+ const targets = gaddag.arcTargets;
696
+ const alphaStart = leftward ? this.leftAlphaStart : this.rightAlphaStart;
697
+ const alphaList = leftward ? this.leftAlphaList : this.rightAlphaList;
698
+ const alphaSecond = leftward ? this.alphaSecondLeft : this.alphaSecondRight;
699
+ // Restored by every recursive call, so stable across loop iterations.
700
+ const hasBlank = this.blanksLeft > 0;
701
+
702
+ for (;;) {
703
+ const label = labels[arcIndex];
704
+ const letter = label & LETTER_MASK;
705
+
706
+ if (letter !== SEPARATOR) {
707
+ const target = targets[arcIndex];
708
+ const alphasEnd = alphaStart[letter + 1];
709
+
710
+ for (let alphaIndex = alphaStart[letter]; alphaIndex < alphasEnd; ++alphaIndex) {
711
+ const alpha = alphaList[alphaIndex];
712
+ const hasTile = this.rackCounts[alpha] > 0;
713
+
714
+ if (!hasTile && !hasBlank) {
715
+ continue;
716
+ }
717
+
718
+ const allowed = alpha < 32 ? (maskLo & (1 << alpha)) !== 0 : (maskHi & (1 << (alpha - 32))) !== 0;
719
+
720
+ if (!allowed) {
721
+ continue;
722
+ }
723
+
724
+ const second = alphaSecond[alpha];
725
+ const nextRef = second === 0 ? target : gaddag.getArc(target, second);
726
+
727
+ if (nextRef === 0) {
728
+ continue;
729
+ }
730
+
731
+ if (hasTile) {
732
+ --this.rackCounts[alpha];
733
+ this.pushPlacement(position, alpha, false);
734
+ this.afterPlacement(position, nextRef, leftward);
735
+ this.popPlacement(position);
736
+ ++this.rackCounts[alpha];
737
+ }
738
+
739
+ if (hasBlank) {
740
+ --this.blanksLeft;
741
+ this.pushPlacement(position, alpha, true);
742
+ this.afterPlacement(position, nextRef, leftward);
743
+ this.popPlacement(position);
744
+ ++this.blanksLeft;
745
+ }
746
+ }
747
+ }
748
+
749
+ if (label >= LAST_ARC_FLAG) {
750
+ return;
751
+ }
752
+
753
+ ++arcIndex;
754
+ }
755
+ }
756
+
757
+ private pushPlacement(position: number, alpha: number, isBlank: boolean): void {
758
+ this.placedAt[position] = alpha;
759
+ this.placedBlankAt[position] = isBlank ? 1 : 0;
760
+ ++this.placedCount;
761
+ }
762
+
763
+ private popPlacement(position: number): void {
764
+ this.placedAt[position] = -1;
765
+ --this.placedCount;
766
+ }
767
+
768
+ private afterPlacement(position: number, ref: number, leftward: boolean): void {
769
+ if (leftward) {
770
+ this.recordLeftIfWord(ref, position);
771
+ this.generateLeft(position - 1, ref);
772
+ } else {
773
+ if ((ref & 1) === 1 && this.isOpen(position + 1)) {
774
+ this.record(this.leftMost, position);
775
+ }
776
+
777
+ this.generateRight(position + 1, ref);
778
+ }
779
+ }
780
+
781
+ /**
782
+ * Records a word that ends at the anchor (a pure reversed-word GADDAG path).
783
+ * Words spanning a single cell are skipped: a lone tile's word is the
784
+ * perpendicular one, generated by the other pass.
785
+ */
786
+ private recordLeftIfWord(ref: number, position: number): void {
787
+ if ((ref & 1) === 1 && this.anchorRightOpen && position < this.anchorPos && this.isOpen(position - 1)) {
788
+ this.record(position, this.anchorPos);
789
+ }
790
+ }
791
+
792
+ private record(startPosition: number, endPosition: number): void {
793
+ const { lineBase } = this;
794
+
795
+ if (this.digraphs.length > 0 && this.hasInvalidDigraph(startPosition, endPosition)) {
796
+ return;
797
+ }
798
+
799
+ if (this.placedCount === 1) {
800
+ let placedPosition = startPosition;
801
+
802
+ while (this.passFilled[lineBase + placedPosition] === 1) {
803
+ ++placedPosition;
804
+ }
805
+
806
+ const key =
807
+ this.passGlobal[lineBase + placedPosition] * 128 +
808
+ this.placedAt[placedPosition] * 2 +
809
+ this.placedBlankAt[placedPosition];
810
+
811
+ if (this.seenSingleTiles.has(key)) {
812
+ return;
813
+ }
814
+
815
+ this.seenSingleTiles.add(key);
816
+ }
817
+
818
+ // Score and emit ResultJson in a single pass.
819
+ let mainScore = 0;
820
+ let wordMultiplier = 1;
821
+ let collisionsScore = 0;
822
+ const tiles: string[] = [];
823
+ const blankIndices: number[] = [];
824
+
825
+ for (let position = startPosition; position <= endPosition; ++position) {
826
+ const passIndex = lineBase + position;
827
+
828
+ if (this.passFilled[passIndex] === 1) {
829
+ mainScore += this.passScore[passIndex];
830
+ continue;
831
+ }
832
+
833
+ const globalIndex = this.passGlobal[passIndex];
834
+ const alpha = this.placedAt[position];
835
+ const isBlank = this.placedBlankAt[position] === 1;
836
+ const tileScore = isBlank ? this.blankScore : this.alphaPoints[alpha];
837
+ let characterMultiplier = 1;
838
+ let cellWordMultiplier = 1;
839
+ const bonusType = this.bonusType[globalIndex];
840
+
841
+ if (bonusType === 1) {
842
+ const requirement = this.bonusReq[globalIndex];
843
+
844
+ if (requirement === -1 || requirement === this.alphaPoints[alpha]) {
845
+ characterMultiplier = this.bonusMult[globalIndex];
846
+ }
847
+ } else if (bonusType === 2) {
848
+ cellWordMultiplier = this.bonusMult[globalIndex];
849
+ }
850
+
851
+ mainScore += tileScore * characterMultiplier;
852
+ wordMultiplier *= cellWordMultiplier;
853
+
854
+ if (isBlank) {
855
+ blankIndices.push(tiles.length);
856
+ }
857
+
858
+ tiles.push(this.alphaChars[alpha]);
859
+
860
+ if (this.hasCross[passIndex] === 1) {
861
+ collisionsScore += (this.crossBase[passIndex] + tileScore * characterMultiplier) * cellWordMultiplier;
862
+ }
863
+ }
864
+
865
+ let points = mainScore * wordMultiplier + collisionsScore;
866
+
867
+ if (this.placedCount === this.rackSize) {
868
+ const bingo = this.config.bingo;
869
+
870
+ if (isScoreBingo(bingo)) {
871
+ points += bingo.score;
872
+ } else if (isMultiplierBingo(bingo)) {
873
+ points = mainScore * wordMultiplier * bingo.multiplier + collisionsScore;
874
+ }
875
+ }
876
+
877
+ let rankKey = '';
878
+
879
+ for (let position = startPosition; position <= endPosition; ++position) {
880
+ if (this.passFilled[lineBase + position] === 0) {
881
+ const alpha = this.placedAt[position];
882
+ const isBlank = this.placedBlankAt[position] === 1;
883
+ const rackIndex = isBlank ? this.blankRackIndex : this.alphaFirstRackIndex[alpha];
884
+ rankKey += String.fromCharCode(rackIndex * 64 + (isBlank ? alpha : 0));
885
+ }
886
+ }
887
+
888
+ this.sortKeys.push((this.isHorizontal ? 0 : 1 << 20) | (this.line << 15) | (startPosition << 8) | endPosition);
889
+ this.rankKeys.push(rankKey);
890
+ this.results.push({
891
+ blankIndices,
892
+ id: this.results.length,
893
+ isHorizontal: this.isHorizontal,
894
+ points,
895
+ tiles,
896
+ x: this.isHorizontal ? startPosition : this.line,
897
+ y: this.isHorizontal ? this.line : startPosition,
898
+ });
899
+ }
900
+
901
+ private hasInvalidDigraph(startPosition: number, endPosition: number): boolean {
902
+ let previousCharacter = this.characterAt(startPosition);
903
+
904
+ for (let position = startPosition + 1; position <= endPosition; ++position) {
905
+ const character = this.characterAt(position);
906
+
907
+ if (this.digraphs.includes(previousCharacter + character)) {
908
+ return true;
909
+ }
910
+
911
+ previousCharacter = character;
912
+ }
913
+
914
+ return false;
915
+ }
916
+
917
+ private characterAt(position: number): string {
918
+ const passIndex = this.lineBase + position;
919
+
920
+ if (this.passFilled[passIndex] === 1) {
921
+ return this.boardChar[this.passGlobal[passIndex]];
922
+ }
923
+
924
+ return this.alphaChars[this.placedAt[position]];
925
+ }
926
+ }