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