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,401 @@
1
+ /* eslint-disable max-lines */
2
+
3
+ import { type Gaddag } from '@kamilmielnik/gaddag';
4
+ import { BONUS_CHARACTER, BONUS_WORD } from '@scrabble-solver/constants';
5
+ import {
6
+ type Board,
7
+ type Bonus,
8
+ type Cell,
9
+ type Config,
10
+ isMultiplierBingo,
11
+ isScoreBingo,
12
+ type Tile,
13
+ } from '@scrabble-solver/types';
14
+
15
+ interface ReferencePlacement {
16
+ alpha: number;
17
+ isBlank: boolean;
18
+ }
19
+
20
+ /**
21
+ * Brute-force oracle: enumerates every span and rack assignment, validating and
22
+ * scoring moves with the same semantics as MoveGenerator.
23
+ */
24
+ export class ReferenceSolver {
25
+ private readonly gaddag: Gaddag;
26
+ private readonly config: Config;
27
+ private readonly width: number;
28
+ private readonly height: number;
29
+ private readonly filled: boolean[] = [];
30
+ private readonly boardCharacters: string[] = [];
31
+ private readonly boardScores: number[] = [];
32
+ private readonly boardIsEmpty: boolean;
33
+ private readonly alphabet: string[];
34
+ private readonly rackCounts: number[];
35
+ private blanksLeft = 0;
36
+
37
+ private horizontal = true;
38
+ private line = 0;
39
+ private start = 0;
40
+ private end = 0;
41
+ private emptyPositions: number[] = [];
42
+ private placements: ReferencePlacement[] = [];
43
+ private readonly moves = new Set<string>();
44
+
45
+ constructor(gaddag: Gaddag, config: Config, board: Board, tiles: Tile[]) {
46
+ this.gaddag = gaddag;
47
+ this.config = config;
48
+ this.width = board.columnsCount;
49
+ this.height = board.rowsCount;
50
+
51
+ for (const row of board.rows) {
52
+ for (const cell of row) {
53
+ const hasTile = !cell.isEmpty && cell.hasTile();
54
+ this.filled.push(hasTile);
55
+ this.boardCharacters.push(hasTile ? cell.tile.character : '');
56
+ this.boardScores.push(this.boardTileScore(cell, hasTile));
57
+ }
58
+ }
59
+
60
+ this.boardIsEmpty = this.filled.every((isFilled) => !isFilled);
61
+ this.alphabet = config.alphabet;
62
+ this.rackCounts = this.alphabet.map(() => 0);
63
+
64
+ for (const tile of tiles) {
65
+ if (tile.isBlank) {
66
+ ++this.blanksLeft;
67
+ continue;
68
+ }
69
+
70
+ const alpha = this.alphabet.indexOf(tile.character);
71
+
72
+ if (alpha !== -1) {
73
+ ++this.rackCounts[alpha];
74
+ }
75
+ }
76
+ }
77
+
78
+ public solve(): string[] {
79
+ for (const horizontal of [true, false]) {
80
+ this.horizontal = horizontal;
81
+ this.enumerateSpans();
82
+ }
83
+
84
+ return [...this.moves].sort();
85
+ }
86
+
87
+ private enumerateSpans(): void {
88
+ const rackTotal = this.rackCounts.reduce((sum, count) => sum + count, this.blanksLeft);
89
+
90
+ for (let line = 0; line < this.linesCount; ++line) {
91
+ for (let start = 0; start < this.lineLength; ++start) {
92
+ for (let end = start + 1; end < this.lineLength; ++end) {
93
+ this.line = line;
94
+ this.start = start;
95
+ this.end = end;
96
+ this.trySpan(rackTotal);
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ private trySpan(rackTotal: number): void {
103
+ if (!this.isSpanMaximal()) {
104
+ return;
105
+ }
106
+
107
+ this.emptyPositions = this.spanPositions().filter((position) => !this.isFilled(position));
108
+
109
+ if (this.emptyPositions.length === 0 || this.emptyPositions.length > rackTotal) {
110
+ return;
111
+ }
112
+
113
+ this.placements = this.emptyPositions.map(() => ({ alpha: -1, isBlank: false }));
114
+ this.tryPlacements(0);
115
+ }
116
+
117
+ private isSpanMaximal(): boolean {
118
+ const beforeFilled = this.start > 0 && this.isFilled(this.start - 1);
119
+ const afterFilled = this.end < this.lineLength - 1 && this.isFilled(this.end + 1);
120
+ return !beforeFilled && !afterFilled;
121
+ }
122
+
123
+ private tryPlacements(index: number): void {
124
+ if (!this.gaddag.hasPrefix(this.prefixBefore(index))) {
125
+ return;
126
+ }
127
+
128
+ if (index === this.emptyPositions.length) {
129
+ this.tryMove();
130
+ return;
131
+ }
132
+
133
+ for (let alpha = 0; alpha < this.alphabet.length; ++alpha) {
134
+ if (this.rackCounts[alpha] > 0) {
135
+ --this.rackCounts[alpha];
136
+ this.placements[index] = { alpha, isBlank: false };
137
+ this.tryPlacements(index + 1);
138
+ ++this.rackCounts[alpha];
139
+ }
140
+
141
+ if (this.blanksLeft > 0) {
142
+ --this.blanksLeft;
143
+ this.placements[index] = { alpha, isBlank: true };
144
+ this.tryPlacements(index + 1);
145
+ ++this.blanksLeft;
146
+ }
147
+ }
148
+ }
149
+
150
+ private prefixBefore(index: number): string {
151
+ const limit = index < this.emptyPositions.length ? this.emptyPositions[index] : this.end + 1;
152
+ let prefix = '';
153
+
154
+ for (let position = this.start; position < limit; ++position) {
155
+ prefix += this.characterAt(position);
156
+ }
157
+
158
+ return prefix;
159
+ }
160
+
161
+ private tryMove(): void {
162
+ const characters = this.spanPositions().map((position) => this.characterAt(position));
163
+
164
+ if (!this.gaddag.has(characters.join(''))) {
165
+ return;
166
+ }
167
+
168
+ if (this.hasAdjacentDigraph(characters)) {
169
+ return;
170
+ }
171
+
172
+ if (!this.isConnected()) {
173
+ return;
174
+ }
175
+
176
+ const crossWords: string[] = [];
177
+
178
+ for (const position of this.emptyPositions) {
179
+ const cross = this.crossAt(position);
180
+
181
+ if (!cross) {
182
+ continue;
183
+ }
184
+
185
+ const crossWord = cross.characters.join('');
186
+
187
+ if (!this.gaddag.has(crossWord) || this.hasAdjacentDigraph(cross.characters)) {
188
+ return;
189
+ }
190
+
191
+ crossWords.push(crossWord);
192
+ }
193
+
194
+ this.moves.add(this.createSignature(characters.join(''), crossWords));
195
+ }
196
+
197
+ private hasAdjacentDigraph(characters: string[]): boolean {
198
+ const digraphs = this.config.twoCharacterTiles;
199
+ return characters.some((character, index) => index > 0 && digraphs.includes(characters[index - 1] + character));
200
+ }
201
+
202
+ private isConnected(): boolean {
203
+ if (this.boardIsEmpty) {
204
+ const centerLine = Math.floor((this.horizontal ? this.height : this.width) / 2);
205
+ const centerPosition = Math.floor((this.horizontal ? this.width : this.height) / 2);
206
+ return this.line === centerLine && this.start <= centerPosition && centerPosition <= this.end;
207
+ }
208
+
209
+ return this.emptyPositions.some((position) => this.touchesBoardTile(position));
210
+ }
211
+
212
+ private touchesBoardTile(position: number): boolean {
213
+ const x = this.xOf(position);
214
+ const y = this.yOf(position);
215
+ return (
216
+ this.isFilledAt(x - 1, y) || this.isFilledAt(x + 1, y) || this.isFilledAt(x, y - 1) || this.isFilledAt(x, y + 1)
217
+ );
218
+ }
219
+
220
+ private createSignature(mainWord: string, crossWords: string[]): string {
221
+ const placed = this.emptyPositions
222
+ .map((position, index) => {
223
+ const { alpha, isBlank } = this.placements[index];
224
+ return `${this.xOf(position)},${this.yOf(position)},${this.alphabet[alpha]},${isBlank ? 1 : 0}`;
225
+ })
226
+ .sort()
227
+ .join(' ');
228
+ const words = [mainWord, ...crossWords].sort().join(' ');
229
+ return `${placed} | ${words} | ${this.score()}`;
230
+ }
231
+
232
+ private score(): number {
233
+ let collisionsScore = 0;
234
+ let mainScore = 0;
235
+ let wordMultiplier = 1;
236
+
237
+ for (const position of this.spanPositions()) {
238
+ if (this.isFilled(position)) {
239
+ mainScore += this.boardScores[this.globalIndex(position)];
240
+ continue;
241
+ }
242
+
243
+ const placement = this.placementAt(position);
244
+ const bonus = this.bonusAt(position);
245
+ const cellWordMultiplier = bonus?.type === BONUS_WORD ? bonus.multiplier : 1;
246
+ const tileScore = this.tileScore(placement) * this.characterMultiplier(placement, bonus);
247
+ mainScore += tileScore;
248
+ wordMultiplier *= cellWordMultiplier;
249
+ const cross = this.crossAt(position);
250
+
251
+ if (cross) {
252
+ collisionsScore += (cross.baseScore + tileScore) * cellWordMultiplier;
253
+ }
254
+ }
255
+
256
+ return this.applyBingo(mainScore * wordMultiplier, collisionsScore);
257
+ }
258
+
259
+ private applyBingo(wordsScore: number, collisionsScore: number): number {
260
+ if (this.emptyPositions.length !== this.config.rackSize) {
261
+ return wordsScore + collisionsScore;
262
+ }
263
+
264
+ const { bingo } = this.config;
265
+
266
+ if (isScoreBingo(bingo)) {
267
+ return wordsScore + collisionsScore + bingo.score;
268
+ }
269
+
270
+ if (isMultiplierBingo(bingo)) {
271
+ return wordsScore * bingo.multiplier + collisionsScore;
272
+ }
273
+
274
+ return wordsScore + collisionsScore;
275
+ }
276
+
277
+ private characterMultiplier(placement: ReferencePlacement, bonus: Bonus | undefined): number {
278
+ if (bonus?.type !== BONUS_CHARACTER) {
279
+ return 1;
280
+ }
281
+
282
+ const characterScore = this.config.pointsMap[this.alphabet[placement.alpha]] || 0;
283
+
284
+ if (typeof bonus.score === 'number' && bonus.score !== characterScore) {
285
+ return 1;
286
+ }
287
+
288
+ return bonus.multiplier;
289
+ }
290
+
291
+ private tileScore({ alpha, isBlank }: ReferencePlacement): number {
292
+ return isBlank ? this.config.blankScore : this.config.pointsMap[this.alphabet[alpha]] || 0;
293
+ }
294
+
295
+ private bonusAt(position: number): Bonus | undefined {
296
+ const x = this.xOf(position);
297
+ const y = this.yOf(position);
298
+ return this.config.bonuses.find((bonus) => bonus.x === x && bonus.y === y);
299
+ }
300
+
301
+ private crossAt(position: number): { baseScore: number; characters: string[] } | undefined {
302
+ const dx = this.horizontal ? 0 : 1;
303
+ const dy = this.horizontal ? 1 : 0;
304
+ const [startX, startY] = this.crossRunEnd(position, -dx, -dy);
305
+ const [endX, endY] = this.crossRunEnd(position, dx, dy);
306
+
307
+ if (startX === endX && startY === endY) {
308
+ return undefined;
309
+ }
310
+
311
+ const characters: string[] = [];
312
+ let baseScore = 0;
313
+
314
+ for (let x = startX, y = startY; x <= endX && y <= endY; x += dx, y += dy) {
315
+ if (x === this.xOf(position) && y === this.yOf(position)) {
316
+ characters.push(this.characterAt(position));
317
+ continue;
318
+ }
319
+
320
+ const cellIndex = y * this.width + x;
321
+ baseScore += this.boardScores[cellIndex];
322
+ characters.push(this.boardCharacters[cellIndex]);
323
+ }
324
+
325
+ return { baseScore, characters };
326
+ }
327
+
328
+ private crossRunEnd(position: number, dx: number, dy: number): [number, number] {
329
+ let x = this.xOf(position);
330
+ let y = this.yOf(position);
331
+
332
+ while (this.isFilledAt(x + dx, y + dy)) {
333
+ x += dx;
334
+ y += dy;
335
+ }
336
+
337
+ return [x, y];
338
+ }
339
+
340
+ private boardTileScore(cell: Cell, hasTile: boolean): number {
341
+ if (!hasTile) {
342
+ return 0;
343
+ }
344
+
345
+ if (cell.tile.isBlank) {
346
+ return this.config.blankScore;
347
+ }
348
+
349
+ return this.config.pointsMap[cell.tile.character] || 0;
350
+ }
351
+
352
+ private characterAt(position: number): string {
353
+ if (this.isFilled(position)) {
354
+ return this.boardCharacters[this.globalIndex(position)];
355
+ }
356
+
357
+ return this.alphabet[this.placementAt(position).alpha];
358
+ }
359
+
360
+ private placementAt(position: number): ReferencePlacement {
361
+ return this.placements[this.emptyPositions.indexOf(position)];
362
+ }
363
+
364
+ private spanPositions(): number[] {
365
+ const positions: number[] = [];
366
+
367
+ for (let position = this.start; position <= this.end; ++position) {
368
+ positions.push(position);
369
+ }
370
+
371
+ return positions;
372
+ }
373
+
374
+ private isFilled(position: number): boolean {
375
+ return this.filled[this.globalIndex(position)];
376
+ }
377
+
378
+ private isFilledAt(x: number, y: number): boolean {
379
+ return x >= 0 && x < this.width && y >= 0 && y < this.height && this.filled[y * this.width + x];
380
+ }
381
+
382
+ private globalIndex(position: number): number {
383
+ return this.yOf(position) * this.width + this.xOf(position);
384
+ }
385
+
386
+ private get linesCount(): number {
387
+ return this.horizontal ? this.height : this.width;
388
+ }
389
+
390
+ private get lineLength(): number {
391
+ return this.horizontal ? this.width : this.height;
392
+ }
393
+
394
+ private xOf(position: number): number {
395
+ return this.horizontal ? position : this.line;
396
+ }
397
+
398
+ private yOf(position: number): number {
399
+ return this.horizontal ? this.line : position;
400
+ }
401
+ }
@@ -1,14 +1 @@
1
- export { areDigraphsValid } from './areDigraphsValid';
2
- export { fillPattern } from './fillPattern';
3
- export { generateEndIndices } from './generateEndIndices';
4
- export { generateHorizontalPatterns } from './generateHorizontalPatterns';
5
- export { generatePattern } from './generatePattern';
6
- export { generatePatterns } from './generatePatterns';
7
- export { generateStartIndices } from './generateStartIndices';
8
- export { generateVectors } from './generateVectors';
9
- export { generateVerticalPatterns } from './generateVerticalPatterns';
10
- export { getCellsScore } from './getCellsScore';
11
- export { getPatternHash } from './getPatternHash';
12
- export { getPatternScore } from './getPatternScore';
13
- export { getUniquePatterns } from './getUniquePatterns';
14
1
  export { solve } from './solve';
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable max-lines */
2
2
 
3
- import { type Trie } from '@kamilmielnik/trie';
3
+ import { type Gaddag } from '@kamilmielnik/gaddag';
4
4
  import { getConfig } from '@scrabble-solver/configs';
5
5
  import { dictionaries } from '@scrabble-solver/dictionaries';
6
6
  import { Board, Game, Locale, Result, Tile } from '@scrabble-solver/types';
@@ -21,11 +21,11 @@ const getBestResult = ([firstResult, ...results]: Result[]): Result => {
21
21
  describe('solve - pl-PL', () => {
22
22
  const locale = Locale.PL_PL;
23
23
  const config = getConfig(Game.Literaki, locale);
24
- let trie: Trie | undefined;
24
+ let gaddag: Gaddag | undefined;
25
25
 
26
26
  beforeAll(() => {
27
- return dictionaries.get(locale).then((loadedTrie) => {
28
- trie = loadedTrie;
27
+ return dictionaries.get(locale).then((loadedGaddag) => {
28
+ gaddag = loadedGaddag;
29
29
  });
30
30
  });
31
31
 
@@ -48,7 +48,7 @@ describe('solve - pl-PL', () => {
48
48
  ' ',
49
49
  ]);
50
50
  const tiles = generateTiles(['l', 'i', 'n', 'o']);
51
- const results = solve(trie!, config, board, tiles);
51
+ const results = solve(gaddag!, config, board, tiles);
52
52
  expect(results.length).toBe(60);
53
53
  });
54
54
 
@@ -71,8 +71,8 @@ describe('solve - pl-PL', () => {
71
71
  ' ',
72
72
  ]);
73
73
  const tiles = generateTiles(['a', 'a', 'ą', 'r', 't', 'w', 'z']);
74
- const results = solve(trie!, config, board, tiles);
75
- const bestResult = getBestResult(results.map((result) => Result.fromJson(result)));
74
+ const results = solve(gaddag!, config, board, tiles);
75
+ const bestResult = getBestResult(results.map((result) => Result.fromJson(result, board)));
76
76
  expect(bestResult.word).toBe('zmartwychwstałą');
77
77
  expect(bestResult.points).toBe(682);
78
78
  });
@@ -96,8 +96,8 @@ describe('solve - pl-PL', () => {
96
96
  'ar ń m ',
97
97
  ]);
98
98
  const tiles = generateTiles(['ą', 'c', 'h', 't', 'w', 'w', 'z']);
99
- const results = solve(trie!, config, board, tiles);
100
- const bestResult = getBestResult(results.map((result) => Result.fromJson(result)));
99
+ const results = solve(gaddag!, config, board, tiles);
100
+ const bestResult = getBestResult(results.map((result) => Result.fromJson(result, board)));
101
101
  expect(bestResult.word).toBe('zmartwychwstałą');
102
102
  expect(bestResult.points).toBe(1157);
103
103
  });
@@ -121,7 +121,7 @@ describe('solve - pl-PL', () => {
121
121
  ' ',
122
122
  ]);
123
123
  const tiles = generateTiles(['d']);
124
- const results = solve(trie!, config, board, tiles);
124
+ const results = solve(gaddag!, config, board, tiles);
125
125
  expect(results.length).toBe(4);
126
126
  });
127
127
  });
@@ -129,11 +129,11 @@ describe('solve - pl-PL', () => {
129
129
  describe('solve - es-ES', () => {
130
130
  const locale = Locale.ES_ES;
131
131
  const config = getConfig(Game.Scrabble, locale);
132
- let trie: Trie | undefined;
132
+ let gaddag: Gaddag | undefined;
133
133
 
134
134
  beforeAll(() => {
135
- return dictionaries.get(locale).then((loadedTrie) => {
136
- trie = loadedTrie;
135
+ return dictionaries.get(locale).then((loadedGaddag) => {
136
+ gaddag = loadedGaddag;
137
137
  });
138
138
  });
139
139
 
@@ -156,8 +156,8 @@ describe('solve - es-ES', () => {
156
156
  ' ',
157
157
  ]);
158
158
  const tiles = generateTiles(['ll', 'a', 'n', 'a']);
159
- const results = solve(trie!, config, board, tiles);
160
- const bestResult = getBestResult(results.map((result) => Result.fromJson(result)));
159
+ const results = solve(gaddag!, config, board, tiles);
160
+ const bestResult = getBestResult(results.map((result) => Result.fromJson(result, board)));
161
161
  expect(results.length).toBe(24);
162
162
  expect(bestResult.points).toBe(22);
163
163
  });
@@ -181,8 +181,8 @@ describe('solve - es-ES', () => {
181
181
  ' ',
182
182
  ]);
183
183
  const tiles = generateTiles(['ch', 'o', 'o', 'c', 'h']);
184
- const results = solve(trie!, config, board, tiles);
185
- const words = results.map((result) => result.cells.map((cell) => cell.tile?.character).join(''));
184
+ const results = solve(gaddag!, config, board, tiles);
185
+ const words = results.map((result) => Result.fromJson(result, board).word);
186
186
  expect(words).not.toContain('chocho');
187
187
  });
188
188
  });
@@ -190,11 +190,11 @@ describe('solve - es-ES', () => {
190
190
  describe('solve - en-GB', () => {
191
191
  const locale = Locale.EN_GB;
192
192
  const config = getConfig(Game.Scrabble, locale);
193
- let trie: Trie | undefined;
193
+ let gaddag: Gaddag | undefined;
194
194
 
195
195
  beforeAll(() => {
196
- return dictionaries.get(locale).then((loadedTrie) => {
197
- trie = loadedTrie;
196
+ return dictionaries.get(locale).then((loadedGaddag) => {
197
+ gaddag = loadedGaddag;
198
198
  });
199
199
  });
200
200
 
@@ -217,7 +217,7 @@ describe('solve - en-GB', () => {
217
217
  ' ',
218
218
  ]);
219
219
  const tiles = generateTiles(['n']);
220
- const results = solve(trie!, config, board, tiles).map(Result.fromJson);
220
+ const results = solve(gaddag!, config, board, tiles).map((json) => Result.fromJson(json, board));
221
221
  expect(results.some((result) => result.word === 'no')).toBe(true);
222
222
  expect(results.some((result) => result.word === 'on')).toBe(true);
223
223
  expect(results.length).toBe(2);
@@ -1,26 +1,8 @@
1
- import { type Trie } from '@kamilmielnik/trie';
1
+ import { type Gaddag } from '@kamilmielnik/gaddag';
2
2
  import { type Board, type Config, type ResultJson, type Tile } from '@scrabble-solver/types';
3
3
 
4
- import { areDigraphsValid } from './areDigraphsValid';
5
- import { fillPattern } from './fillPattern';
6
- import { generatePatterns } from './generatePatterns';
7
- import { getPatternScore } from './getPatternScore';
8
- import { getUniquePatterns } from './getUniquePatterns';
4
+ import { MoveGenerator } from './MoveGenerator';
9
5
 
10
- export const solve = (trie: Trie, config: Config, board: Board, tiles: Tile[]): ResultJson[] => {
11
- const patterns = generatePatterns(config, board);
12
- const filledPatterns = patterns.flatMap((pattern) => fillPattern(trie, config, pattern, tiles));
13
- const validPatterns =
14
- config.twoCharacterTiles.length > 0
15
- ? filledPatterns.filter((pattern) => areDigraphsValid(config, pattern))
16
- : filledPatterns;
17
- const uniquePatterns = getUniquePatterns(validPatterns);
18
- const results = uniquePatterns.map((pattern, index) => ({
19
- cells: pattern.cells.map((cell) => cell.toJson()),
20
- collisions: pattern.getCollisions().map((collision) => collision.cells.map((cell) => cell.toJson())),
21
- id: index,
22
- points: getPatternScore(config, pattern),
23
- }));
24
-
25
- return results;
6
+ export const solve = (gaddag: Gaddag, config: Config, board: Board, tiles: Tile[]): ResultJson[] => {
7
+ return new MoveGenerator(gaddag, config, board, tiles).run();
26
8
  };
@@ -5,6 +5,6 @@
5
5
  "outDir": "./build",
6
6
  "rootDir": "./src"
7
7
  },
8
- "exclude": ["node_modules", "**/*.test.ts"],
8
+ "exclude": ["node_modules", "**/*.test.ts", "src/ReferenceSolver.ts"],
9
9
  "include": ["./src", "index.ts"]
10
10
  }
@@ -16,6 +16,7 @@ export declare class Cell {
16
16
  equals(other: Cell): boolean;
17
17
  hasTile(): boolean;
18
18
  isCandidate(): boolean;
19
+ isFilled(): boolean;
19
20
  toJson(): CellJson;
20
21
  toString(): string;
21
22
  }
@@ -26,6 +26,9 @@ class Cell {
26
26
  isCandidate() {
27
27
  return this.isEmpty && this.hasTile();
28
28
  }
29
+ isFilled() {
30
+ return !this.isEmpty && this.hasTile();
31
+ }
29
32
  toJson() {
30
33
  return {
31
34
  isEmpty: this.isEmpty,
@@ -1,9 +1,10 @@
1
- import { Cell } from './Cell';
1
+ import { type Board } from './Board';
2
+ import { type Cell } from './Cell';
2
3
  import { type ResultJson } from './ResultJson';
3
4
  import { type Tile } from './Tile';
4
5
  type Collision = Cell[];
5
6
  export declare class Result {
6
- static fromJson: (json: ResultJson) => Result;
7
+ static fromJson: (json: ResultJson, board: Board) => Result;
7
8
  readonly blanksCount: number;
8
9
  readonly cells: Cell[];
9
10
  readonly collisions: Collision[];
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Result = void 0;
4
4
  const constants_1 = require("@scrabble-solver/constants");
5
- const Cell_1 = require("./Cell");
5
+ const readResultCells_1 = require("./readResultCells");
6
6
  class Result {
7
7
  constructor({ cells, id, collisions, points, }) {
8
8
  const tiles = getTiles(cells);
@@ -22,11 +22,15 @@ class Result {
22
22
  this.wordsCount = 1 + this.collisions.length;
23
23
  }
24
24
  toJson() {
25
+ const placed = this.cells.filter((cell) => cell.isEmpty);
25
26
  return {
26
- cells: this.cells.map((cell) => cell.toJson()),
27
+ blankIndices: placed.flatMap((cell, index) => (cell.tile.isBlank ? [index] : [])),
27
28
  id: this.id,
28
- collisions: this.collisions.map((collision) => collision.map((cell) => cell.toJson())),
29
+ isHorizontal: this.isHorizontal(),
29
30
  points: this.points,
31
+ tiles: placed.map((cell) => cell.tile.character),
32
+ x: this.cells[0].x,
33
+ y: this.cells[0].y,
30
34
  };
31
35
  }
32
36
  isHorizontal() {
@@ -37,11 +41,12 @@ class Result {
37
41
  }
38
42
  }
39
43
  exports.Result = Result;
40
- Result.fromJson = (json) => {
44
+ Result.fromJson = (json, board) => {
45
+ const cells = (0, readResultCells_1.readCells)(json, board);
41
46
  return new Result({
47
+ cells,
48
+ collisions: (0, readResultCells_1.readCollisions)(cells, board, json.isHorizontal),
42
49
  id: json.id,
43
- cells: json.cells.map(Cell_1.Cell.fromJson),
44
- collisions: json.collisions.map((collision) => collision.map(Cell_1.Cell.fromJson)),
45
50
  points: json.points,
46
51
  });
47
52
  };
@@ -1,8 +1,9 @@
1
- import { type CellJson } from './CellJson';
2
- import { type CollisionJson } from './CollisionJson';
3
1
  export interface ResultJson {
4
- cells: CellJson[];
5
- collisions: CollisionJson[];
2
+ blankIndices: number[];
6
3
  id: number;
4
+ isHorizontal: boolean;
7
5
  points: number;
6
+ tiles: string[];
7
+ x: number;
8
+ y: number;
8
9
  }