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,1009 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
|
|
3
|
+
import { Gaddag } from '@kamilmielnik/gaddag';
|
|
4
|
+
import { getConfig } from '@scrabble-solver/configs';
|
|
5
|
+
import { BONUS_CHARACTER, BONUS_WORD } from '@scrabble-solver/constants';
|
|
6
|
+
import {
|
|
7
|
+
Board,
|
|
8
|
+
Cell,
|
|
9
|
+
Config,
|
|
10
|
+
type ConfigJson,
|
|
11
|
+
Game,
|
|
12
|
+
Locale,
|
|
13
|
+
Result,
|
|
14
|
+
type ResultJson,
|
|
15
|
+
Tile,
|
|
16
|
+
} from '@scrabble-solver/types';
|
|
17
|
+
|
|
18
|
+
import { MoveGenerator } from './MoveGenerator';
|
|
19
|
+
import { ReferenceSolver } from './ReferenceSolver';
|
|
20
|
+
|
|
21
|
+
type TileScores = Record<string, number>;
|
|
22
|
+
|
|
23
|
+
type BoardTilePlacement = [x: number, y: number, character: string, isBlank?: boolean];
|
|
24
|
+
|
|
25
|
+
type PlacedTile = [x: number, y: number, character: string, isBlank: boolean];
|
|
26
|
+
|
|
27
|
+
interface MoveSummary {
|
|
28
|
+
placed: PlacedTile[];
|
|
29
|
+
points: number;
|
|
30
|
+
words: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const BLANK_CHARACTER = '?';
|
|
34
|
+
|
|
35
|
+
const BASIC_TILES: TileScores = { a: 1, b: 3, c: 3, d: 2 };
|
|
36
|
+
|
|
37
|
+
const SPANISH_MINI_TILES: TileScores = { a: 1, c: 3, ch: 5, h: 4, i: 1, l: 1, ll: 8, o: 1 };
|
|
38
|
+
|
|
39
|
+
describe('MoveGenerator - input validation', () => {
|
|
40
|
+
const gaddag = Gaddag.fromArray(['ab']);
|
|
41
|
+
|
|
42
|
+
it('throws when the alphabet exceeds 64 tiles', () => {
|
|
43
|
+
const config = createConfig(createWideAlphabet(65));
|
|
44
|
+
expect(() => new MoveGenerator(gaddag, config, createBoard(5, 5), createRack(['a']))).toThrow(
|
|
45
|
+
'Alphabets larger than 64 tiles are not supported',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('accepts an alphabet of exactly 64 tiles', () => {
|
|
50
|
+
const config = createConfig(createWideAlphabet(64));
|
|
51
|
+
const results = generate(gaddag, config, createBoard(5, 5), ['a', 'b']);
|
|
52
|
+
expect(results.length).toBe(4);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('throws when the board is wider than 32 cells', () => {
|
|
56
|
+
const config = createConfig(BASIC_TILES);
|
|
57
|
+
expect(() => new MoveGenerator(gaddag, config, createBoard(33, 5), createRack(['a']))).toThrow(
|
|
58
|
+
'Boards larger than 32x32 are not supported',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('throws when the board is taller than 32 cells', () => {
|
|
63
|
+
const config = createConfig(BASIC_TILES);
|
|
64
|
+
expect(() => new MoveGenerator(gaddag, config, createBoard(5, 33), createRack(['a']))).toThrow(
|
|
65
|
+
'Boards larger than 32x32 are not supported',
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('accepts a 32x32 board', () => {
|
|
70
|
+
const config = createConfig(BASIC_TILES);
|
|
71
|
+
const results = generate(gaddag, config, createBoard(32, 32), ['a', 'b']);
|
|
72
|
+
expect(results.length).toBe(4);
|
|
73
|
+
expect(results.every((result) => coversCell(result, 16, 16))).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('MoveGenerator - rack edge cases', () => {
|
|
78
|
+
const gaddag = Gaddag.fromArray(['ab']);
|
|
79
|
+
const config = createConfig(BASIC_TILES);
|
|
80
|
+
const board = createBoard(5, 5, [[2, 2, 'a']]);
|
|
81
|
+
|
|
82
|
+
it('returns nothing for an empty rack', () => {
|
|
83
|
+
expect(generate(gaddag, config, board, [])).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns nothing when the rack has only characters outside the config alphabet', () => {
|
|
87
|
+
expect(generate(gaddag, config, board, ['x'])).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('ignores rack tiles with characters outside the config alphabet', () => {
|
|
91
|
+
expect(generate(gaddag, config, board, ['b', 'x'])).toEqual(generate(gaddag, config, board, ['b']));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns nothing when rack characters do not occur in the dictionary', () => {
|
|
95
|
+
const configWithZ = createConfig({ ...BASIC_TILES, z: 10 });
|
|
96
|
+
expect(generate(gaddag, configWithZ, board, ['z'])).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('MoveGenerator - first move on an empty board', () => {
|
|
101
|
+
it('generates every word placement through the center, in both directions', () => {
|
|
102
|
+
const gaddag = Gaddag.fromArray(['ab', 'ba']);
|
|
103
|
+
const config = createConfig(BASIC_TILES);
|
|
104
|
+
const results = generate(gaddag, config, createBoard(5, 5), ['a', 'b']);
|
|
105
|
+
expect(summarizeAll(results)).toEqual([
|
|
106
|
+
{
|
|
107
|
+
placed: [
|
|
108
|
+
[1, 2, 'a', false],
|
|
109
|
+
[2, 2, 'b', false],
|
|
110
|
+
],
|
|
111
|
+
points: 4,
|
|
112
|
+
words: ['ab'],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
placed: [
|
|
116
|
+
[1, 2, 'b', false],
|
|
117
|
+
[2, 2, 'a', false],
|
|
118
|
+
],
|
|
119
|
+
points: 4,
|
|
120
|
+
words: ['ba'],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
placed: [
|
|
124
|
+
[2, 2, 'a', false],
|
|
125
|
+
[3, 2, 'b', false],
|
|
126
|
+
],
|
|
127
|
+
points: 4,
|
|
128
|
+
words: ['ab'],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
placed: [
|
|
132
|
+
[2, 2, 'b', false],
|
|
133
|
+
[3, 2, 'a', false],
|
|
134
|
+
],
|
|
135
|
+
points: 4,
|
|
136
|
+
words: ['ba'],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
placed: [
|
|
140
|
+
[2, 1, 'a', false],
|
|
141
|
+
[2, 2, 'b', false],
|
|
142
|
+
],
|
|
143
|
+
points: 4,
|
|
144
|
+
words: ['ab'],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
placed: [
|
|
148
|
+
[2, 1, 'b', false],
|
|
149
|
+
[2, 2, 'a', false],
|
|
150
|
+
],
|
|
151
|
+
points: 4,
|
|
152
|
+
words: ['ba'],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
placed: [
|
|
156
|
+
[2, 2, 'a', false],
|
|
157
|
+
[2, 3, 'b', false],
|
|
158
|
+
],
|
|
159
|
+
points: 4,
|
|
160
|
+
words: ['ab'],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
placed: [
|
|
164
|
+
[2, 2, 'b', false],
|
|
165
|
+
[2, 3, 'a', false],
|
|
166
|
+
],
|
|
167
|
+
points: 4,
|
|
168
|
+
words: ['ba'],
|
|
169
|
+
},
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('starts from the center of rectangular boards', () => {
|
|
174
|
+
const gaddag = Gaddag.fromArray(['ab']);
|
|
175
|
+
const config = createConfig(BASIC_TILES);
|
|
176
|
+
const wideResults = generate(gaddag, config, createBoard(9, 5), ['a', 'b']);
|
|
177
|
+
expect(wideResults.length).toBe(4);
|
|
178
|
+
expect(wideResults.every((result) => coversCell(result, 4, 2))).toBe(true);
|
|
179
|
+
const tallResults = generate(gaddag, config, createBoard(5, 9), ['a', 'b']);
|
|
180
|
+
expect(tallResults.length).toBe(4);
|
|
181
|
+
expect(tallResults.every((result) => coversCell(result, 2, 4))).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('respects the rack multiset', () => {
|
|
185
|
+
const gaddag = Gaddag.fromArray(['aa']);
|
|
186
|
+
const config = createConfig(BASIC_TILES);
|
|
187
|
+
expect(generate(gaddag, config, createBoard(5, 5), ['a'])).toEqual([]);
|
|
188
|
+
expect(generate(gaddag, config, createBoard(5, 5), ['a', 'a']).length).toBe(4);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('substitutes missing tiles with a blank', () => {
|
|
192
|
+
const gaddag = Gaddag.fromArray(['aa']);
|
|
193
|
+
const config = createConfig(BASIC_TILES);
|
|
194
|
+
const results = generate(gaddag, config, createBoard(5, 5), ['a', BLANK_CHARACTER]);
|
|
195
|
+
expect(results.length).toBe(8);
|
|
196
|
+
expect(results.every((result) => result.points === 1)).toBe(true);
|
|
197
|
+
expect(results.every((result) => result.blanksCount === 1)).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('does not generate one-letter words', () => {
|
|
201
|
+
const gaddag = Gaddag.fromArray(['a', 'ab']);
|
|
202
|
+
const config = createConfig(BASIC_TILES);
|
|
203
|
+
expect(generate(gaddag, config, createBoard(5, 5), ['a'])).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('MoveGenerator - anchoring and connectivity', () => {
|
|
208
|
+
it('requires contact with existing tiles', () => {
|
|
209
|
+
const config = createConfig(BASIC_TILES);
|
|
210
|
+
const board = createBoard(5, 5, [[0, 0, 'a']]);
|
|
211
|
+
expect(generate(Gaddag.fromArray(['bc']), config, board, ['b', 'c'])).toEqual([]);
|
|
212
|
+
expect(generate(Gaddag.fromArray(['abc']), config, board, ['b', 'c']).length).toBe(2);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('extends existing words on either side and on both at once', () => {
|
|
216
|
+
const gaddag = Gaddag.fromArray(['dab', 'abe', 'dabe']);
|
|
217
|
+
const config = createConfig({ a: 1, b: 3, d: 2, e: 1 });
|
|
218
|
+
const board = createBoard(5, 5, [
|
|
219
|
+
[1, 2, 'a'],
|
|
220
|
+
[2, 2, 'b'],
|
|
221
|
+
]);
|
|
222
|
+
const results = generate(gaddag, config, board, ['d', 'e']);
|
|
223
|
+
expect(toWords(results)).toEqual(['dab', 'dabe', 'abe']);
|
|
224
|
+
expect(results.map((result) => result.points)).toEqual([6, 7, 5]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('plays through existing tiles', () => {
|
|
228
|
+
const gaddag = Gaddag.fromArray(['abc']);
|
|
229
|
+
const config = createConfig(BASIC_TILES);
|
|
230
|
+
const board = createBoard(5, 5, [
|
|
231
|
+
[1, 2, 'a'],
|
|
232
|
+
[3, 2, 'c'],
|
|
233
|
+
]);
|
|
234
|
+
const results = generateJson(gaddag, config, board, ['b']);
|
|
235
|
+
expect(results).toEqual([{ blankIndices: [], id: 0, isHorizontal: true, points: 7, tiles: ['b'], x: 1, y: 2 }]);
|
|
236
|
+
const [result] = results.map((json) => Result.fromJson(json, board));
|
|
237
|
+
expect(result.word).toBe('abc');
|
|
238
|
+
expect(result.cells.map(({ isEmpty, x, y }) => ({ isEmpty, x, y }))).toEqual([
|
|
239
|
+
{ isEmpty: false, x: 1, y: 2 },
|
|
240
|
+
{ isEmpty: true, x: 2, y: 2 },
|
|
241
|
+
{ isEmpty: false, x: 3, y: 2 },
|
|
242
|
+
]);
|
|
243
|
+
expect(result.collisions).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('returns nothing on a full board', () => {
|
|
247
|
+
const gaddag = Gaddag.fromArray(['ab']);
|
|
248
|
+
const config = createConfig(BASIC_TILES);
|
|
249
|
+
const placements: BoardTilePlacement[] = [];
|
|
250
|
+
|
|
251
|
+
for (let y = 0; y < 3; ++y) {
|
|
252
|
+
for (let x = 0; x < 3; ++x) {
|
|
253
|
+
placements.push([x, y, 'a']);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
expect(generate(gaddag, config, createBoard(3, 3, placements), ['b'])).toEqual([]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('generates nothing around board characters missing from the dictionary', () => {
|
|
261
|
+
const gaddag = Gaddag.fromArray(['ab']);
|
|
262
|
+
const config = createConfig(BASIC_TILES);
|
|
263
|
+
const board = createBoard(5, 5, [[2, 2, 'z']]);
|
|
264
|
+
expect(generate(gaddag, config, board, ['a', 'b'])).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('treats candidate cells (empty cells holding a tile) as empty', () => {
|
|
268
|
+
const gaddag = Gaddag.fromArray(['ab']);
|
|
269
|
+
const config = createConfig(BASIC_TILES);
|
|
270
|
+
const board = Board.create(5, 5);
|
|
271
|
+
board.updateCell(2, 2, () => new Cell({ isEmpty: true, tile: new Tile({ character: 'a' }), x: 2, y: 2 }));
|
|
272
|
+
expect(generate(gaddag, config, board, ['a', 'b'])).toEqual(
|
|
273
|
+
generate(gaddag, config, Board.create(5, 5), ['a', 'b']),
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('MoveGenerator - perpendicular words (cross-checks)', () => {
|
|
279
|
+
it('rejects placements whose perpendicular word is not in the dictionary', () => {
|
|
280
|
+
const gaddag = Gaddag.fromArray(['bb']);
|
|
281
|
+
const config = createConfig(BASIC_TILES);
|
|
282
|
+
const board = createBoard(5, 5, [[2, 1, 'a']]);
|
|
283
|
+
expect(generate(gaddag, config, board, ['b', 'b'])).toEqual([]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('validates the perpendicular word at every placed tile', () => {
|
|
287
|
+
const gaddag = Gaddag.fromArray(['bb', 'ab']);
|
|
288
|
+
const config = createConfig(BASIC_TILES);
|
|
289
|
+
const board = createBoard(5, 5, [
|
|
290
|
+
[1, 1, 'a'],
|
|
291
|
+
[2, 1, 'a'],
|
|
292
|
+
]);
|
|
293
|
+
const results = generate(gaddag, config, board, ['b', 'b']);
|
|
294
|
+
expect(toSortedPoints(results)).toEqual([4, 4, 10, 10, 14]);
|
|
295
|
+
expect(summarizeAll(results)).toContainEqual({
|
|
296
|
+
placed: [
|
|
297
|
+
[1, 2, 'b', false],
|
|
298
|
+
[2, 2, 'b', false],
|
|
299
|
+
],
|
|
300
|
+
points: 14,
|
|
301
|
+
words: ['bb', 'ab', 'ab'],
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('applies cross-check masks to tiles beyond the 32nd alphabet entry', () => {
|
|
306
|
+
const tileScores = createWideAlphabet(40);
|
|
307
|
+
const alphabet = Object.keys(tileScores);
|
|
308
|
+
const high = alphabet[35];
|
|
309
|
+
const other = alphabet[36];
|
|
310
|
+
const gaddag = Gaddag.fromArray([`a${high}`, `b${high}`, `a${other}`]);
|
|
311
|
+
const config = createConfig(tileScores);
|
|
312
|
+
const board = createBoard(5, 5, [
|
|
313
|
+
[1, 2, 'a'],
|
|
314
|
+
[2, 1, 'b'],
|
|
315
|
+
]);
|
|
316
|
+
const results = generate(gaddag, config, board, [high, other]);
|
|
317
|
+
expect(summarizeAll(results)).toEqual([
|
|
318
|
+
{ placed: [[3, 1, high, false]], points: 4, words: [`b${high}`] },
|
|
319
|
+
{ placed: [[2, 2, high, false]], points: 6, words: [`a${high}`, `b${high}`] },
|
|
320
|
+
{ placed: [[1, 3, high, false]], points: 2, words: [`a${high}`] },
|
|
321
|
+
{ placed: [[1, 3, other, false]], points: 2, words: [`a${other}`] },
|
|
322
|
+
]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('reads perpendicular tiles on both sides of the placed tile', () => {
|
|
326
|
+
const gaddag = Gaddag.fromArray(['abc', 'bd']);
|
|
327
|
+
const config = createConfig(BASIC_TILES);
|
|
328
|
+
const board = createBoard(5, 5, [
|
|
329
|
+
[2, 1, 'a'],
|
|
330
|
+
[2, 3, 'c'],
|
|
331
|
+
[3, 2, 'd'],
|
|
332
|
+
]);
|
|
333
|
+
const results = generateJson(gaddag, config, board, ['b']);
|
|
334
|
+
expect(results).toEqual([{ blankIndices: [], id: 0, isHorizontal: true, points: 12, tiles: ['b'], x: 2, y: 2 }]);
|
|
335
|
+
const [result] = results.map((json) => Result.fromJson(json, board));
|
|
336
|
+
expect(result.words).toEqual(['bd', 'abc']);
|
|
337
|
+
expect(result.collisions).toHaveLength(1);
|
|
338
|
+
expect(result.collisions[0].map(({ x, y }) => ({ x, y }))).toEqual([
|
|
339
|
+
{ x: 2, y: 1 },
|
|
340
|
+
{ x: 2, y: 2 },
|
|
341
|
+
{ x: 2, y: 3 },
|
|
342
|
+
]);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('MoveGenerator - single-tile moves', () => {
|
|
347
|
+
const gaddag = Gaddag.fromArray(['aba']);
|
|
348
|
+
const config = createConfig(BASIC_TILES);
|
|
349
|
+
const crossroads = () =>
|
|
350
|
+
createBoard(5, 5, [
|
|
351
|
+
[1, 2, 'a'],
|
|
352
|
+
[3, 2, 'a'],
|
|
353
|
+
[2, 1, 'a'],
|
|
354
|
+
[2, 3, 'a'],
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
it('emits one result when a tile completes words in both directions', () => {
|
|
358
|
+
const results = generate(gaddag, config, crossroads(), ['b']);
|
|
359
|
+
expect(summarizeAll(results)).toEqual([{ placed: [[2, 2, 'b', false]], points: 10, words: ['aba', 'aba'] }]);
|
|
360
|
+
expect(results[0].cells.every((cell) => cell.y === 2)).toBe(true);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('keeps blank and real-tile versions of the same placement distinct', () => {
|
|
364
|
+
const results = generate(gaddag, config, crossroads(), ['b', BLANK_CHARACTER]);
|
|
365
|
+
expect(summarizeAll(results)).toEqual([
|
|
366
|
+
{ placed: [[2, 2, 'b', false]], points: 10, words: ['aba', 'aba'] },
|
|
367
|
+
{ placed: [[2, 2, 'b', true]], points: 4, words: ['aba', 'aba'] },
|
|
368
|
+
]);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe('MoveGenerator - blanks', () => {
|
|
373
|
+
it('scores blanks with blankScore in both the main and perpendicular word', () => {
|
|
374
|
+
const gaddag = Gaddag.fromArray(['ab']);
|
|
375
|
+
const config = createConfig(BASIC_TILES, { blankScore: 2 });
|
|
376
|
+
const board = createBoard(5, 5, [
|
|
377
|
+
[2, 1, 'a'],
|
|
378
|
+
[1, 2, 'a'],
|
|
379
|
+
]);
|
|
380
|
+
const results = generate(gaddag, config, board, [BLANK_CHARACTER]);
|
|
381
|
+
expect(summarizeAll(results)).toEqual([
|
|
382
|
+
{ placed: [[3, 1, 'b', true]], points: 3, words: ['ab'] },
|
|
383
|
+
{ placed: [[2, 2, 'b', true]], points: 6, words: ['ab', 'ab'] },
|
|
384
|
+
{ placed: [[1, 3, 'b', true]], points: 3, words: ['ab'] },
|
|
385
|
+
]);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('scores blanks already on the board with blankScore', () => {
|
|
389
|
+
const gaddag = Gaddag.fromArray(['ab']);
|
|
390
|
+
const config = createConfig(BASIC_TILES);
|
|
391
|
+
const board = createBoard(5, 5, [[2, 2, 'a', true]]);
|
|
392
|
+
const results = generate(gaddag, config, board, ['b']);
|
|
393
|
+
expect(results.map((result) => result.points)).toEqual([3, 3]);
|
|
394
|
+
expect(results[0].cells[0]).toEqual(
|
|
395
|
+
new Cell({ isEmpty: false, tile: new Tile({ character: 'a', isBlank: true }), x: 2, y: 2 }),
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('plays multiple blanks in a single move', () => {
|
|
400
|
+
const gaddag = Gaddag.fromArray(['bb']);
|
|
401
|
+
const config = createConfig(BASIC_TILES);
|
|
402
|
+
const results = generate(gaddag, config, createBoard(5, 5), [BLANK_CHARACTER, BLANK_CHARACTER]);
|
|
403
|
+
expect(results.length).toBe(4);
|
|
404
|
+
expect(results.every((result) => result.points === 0)).toBe(true);
|
|
405
|
+
expect(results.every((result) => result.blanksCount === 2)).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('MoveGenerator - digraphs', () => {
|
|
410
|
+
const config = createConfig(SPANISH_MINI_TILES);
|
|
411
|
+
|
|
412
|
+
it('plays a digraph tile within words in both directions', () => {
|
|
413
|
+
const gaddag = Gaddag.fromArray(['chico']);
|
|
414
|
+
const results = generate(gaddag, config, createBoard(7, 7), ['ch', 'i', 'c', 'o']);
|
|
415
|
+
expect(results.length).toBe(8);
|
|
416
|
+
expect(toWords(results).every((word) => word === 'chico')).toBe(true);
|
|
417
|
+
expect(results.every((result) => result.points === 10)).toBe(true);
|
|
418
|
+
expect(results.every((result) => result.cells.length === 4)).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('does not compose a digraph from two single tiles', () => {
|
|
422
|
+
const gaddag = Gaddag.fromArray(['chico']);
|
|
423
|
+
expect(generate(gaddag, config, createBoard(7, 7), ['c', 'h', 'i', 'c', 'o'])).toEqual([]);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('does not compose a digraph from a board tile and a placed tile', () => {
|
|
427
|
+
const gaddag = Gaddag.fromArray(['chico']);
|
|
428
|
+
const board = createBoard(7, 7, [[2, 2, 'c']]);
|
|
429
|
+
expect(generate(gaddag, config, board, ['h', 'i', 'c', 'o'])).toEqual([]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('reads a board digraph tile when extending to the right', () => {
|
|
433
|
+
const gaddag = Gaddag.fromArray(['chico']);
|
|
434
|
+
const board = createBoard(7, 7, [
|
|
435
|
+
[1, 3, 'ch'],
|
|
436
|
+
[2, 3, 'i'],
|
|
437
|
+
[3, 3, 'c'],
|
|
438
|
+
]);
|
|
439
|
+
const results = generate(gaddag, config, board, ['o']);
|
|
440
|
+
expect(summarizeAll(results)).toEqual([{ placed: [[4, 3, 'o', false]], points: 10, words: ['chico'] }]);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('reads a board digraph tile when extending to the left', () => {
|
|
444
|
+
const gaddag = Gaddag.fromArray(['chico']);
|
|
445
|
+
const board = createBoard(7, 7, [
|
|
446
|
+
[3, 3, 'i'],
|
|
447
|
+
[4, 3, 'c'],
|
|
448
|
+
[5, 3, 'o'],
|
|
449
|
+
]);
|
|
450
|
+
const results = generate(gaddag, config, board, ['ch']);
|
|
451
|
+
expect(summarizeAll(results)).toEqual([{ placed: [[2, 3, 'ch', false]], points: 10, words: ['chico'] }]);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('validates perpendicular words through a board digraph tile', () => {
|
|
455
|
+
const gaddag = Gaddag.fromArray(['cho', 'ol']);
|
|
456
|
+
const board = createBoard(7, 7, [[3, 2, 'ch']]);
|
|
457
|
+
const results = generate(gaddag, config, board, ['o', 'l']);
|
|
458
|
+
expect(summarizeAll(results)).toEqual([
|
|
459
|
+
{ placed: [[4, 2, 'o', false]], points: 6, words: ['cho'] },
|
|
460
|
+
{
|
|
461
|
+
placed: [
|
|
462
|
+
[3, 3, 'o', false],
|
|
463
|
+
[4, 3, 'l', false],
|
|
464
|
+
],
|
|
465
|
+
points: 8,
|
|
466
|
+
words: ['ol', 'cho'],
|
|
467
|
+
},
|
|
468
|
+
{ placed: [[3, 3, 'o', false]], points: 6, words: ['cho'] },
|
|
469
|
+
{
|
|
470
|
+
placed: [
|
|
471
|
+
[4, 2, 'o', false],
|
|
472
|
+
[4, 3, 'l', false],
|
|
473
|
+
],
|
|
474
|
+
points: 8,
|
|
475
|
+
words: ['ol', 'cho'],
|
|
476
|
+
},
|
|
477
|
+
]);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('plays a blank as a digraph tile', () => {
|
|
481
|
+
const gaddag = Gaddag.fromArray(['cho']);
|
|
482
|
+
const board = createBoard(7, 7, [[3, 3, 'o']]);
|
|
483
|
+
const results = generate(gaddag, config, board, [BLANK_CHARACTER]);
|
|
484
|
+
expect(summarizeAll(results)).toEqual([
|
|
485
|
+
{ placed: [[2, 3, 'ch', true]], points: 1, words: ['cho'] },
|
|
486
|
+
{ placed: [[3, 2, 'ch', true]], points: 1, words: ['cho'] },
|
|
487
|
+
]);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('counts a digraph as a single tile for the bingo', () => {
|
|
491
|
+
const gaddag = Gaddag.fromArray(['cho']);
|
|
492
|
+
const bingoConfig = createConfig(SPANISH_MINI_TILES, { rackSize: 2 });
|
|
493
|
+
const results = generate(gaddag, bingoConfig, createBoard(7, 7), ['ch', 'o']);
|
|
494
|
+
expect(results.length).toBe(4);
|
|
495
|
+
expect(results.every((result) => result.points === 56)).toBe(true);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('rejects digraphs composed from single tiles in perpendicular words', () => {
|
|
499
|
+
const gaddag = Gaddag.fromArray(['chico', 'ha']);
|
|
500
|
+
const board = createBoard(7, 7, [
|
|
501
|
+
[2, 1, 'c'],
|
|
502
|
+
[2, 3, 'i'],
|
|
503
|
+
[2, 4, 'c'],
|
|
504
|
+
[2, 5, 'o'],
|
|
505
|
+
]);
|
|
506
|
+
expect(generate(gaddag, config, board, ['h', 'a'])).toEqual([]);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('rejects perpendicular words through board tiles that spell a digraph', () => {
|
|
510
|
+
const gaddag = Gaddag.fromArray(['cho', 'oa']);
|
|
511
|
+
const board = createBoard(7, 7, [
|
|
512
|
+
[2, 1, 'c'],
|
|
513
|
+
[2, 2, 'h'],
|
|
514
|
+
[3, 3, 'a'],
|
|
515
|
+
]);
|
|
516
|
+
expect(generate(gaddag, config, board, ['o'])).toEqual([]);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('allows the digraph tile itself in perpendicular words', () => {
|
|
520
|
+
const gaddag = Gaddag.fromArray(['cha', 'cho']);
|
|
521
|
+
const board = createBoard(7, 7, [[2, 3, 'o']]);
|
|
522
|
+
const results = generate(gaddag, config, board, ['ch', 'a']);
|
|
523
|
+
expect(summarizeAll(results)).toEqual([
|
|
524
|
+
{
|
|
525
|
+
placed: [
|
|
526
|
+
[2, 2, 'ch', false],
|
|
527
|
+
[3, 2, 'a', false],
|
|
528
|
+
],
|
|
529
|
+
points: 12,
|
|
530
|
+
words: ['cha', 'cho'],
|
|
531
|
+
},
|
|
532
|
+
{ placed: [[1, 3, 'ch', false]], points: 6, words: ['cho'] },
|
|
533
|
+
{
|
|
534
|
+
placed: [
|
|
535
|
+
[1, 3, 'ch', false],
|
|
536
|
+
[1, 4, 'a', false],
|
|
537
|
+
],
|
|
538
|
+
points: 12,
|
|
539
|
+
words: ['cha', 'cho'],
|
|
540
|
+
},
|
|
541
|
+
{ placed: [[2, 2, 'ch', false]], points: 6, words: ['cho'] },
|
|
542
|
+
]);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
describe('MoveGenerator - digraphs with the real Spanish config', () => {
|
|
547
|
+
const config = getConfig(Game.Scrabble, Locale.ES_ES);
|
|
548
|
+
|
|
549
|
+
it('plays chico through the center double-word bonus', () => {
|
|
550
|
+
const gaddag = Gaddag.fromArray(['chico']);
|
|
551
|
+
const results = generate(gaddag, config, createBoard(15, 15), ['ch', 'i', 'c', 'o']);
|
|
552
|
+
expect(results.length).toBe(8);
|
|
553
|
+
expect(toWords(results).every((word) => word === 'chico')).toBe(true);
|
|
554
|
+
expect(results.every((result) => result.points === 20)).toBe(true);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('orders blank interpretations by tile order (ll before l)', () => {
|
|
558
|
+
const gaddag = Gaddag.fromArray(['pillo', 'pilo']);
|
|
559
|
+
const board = createBoard(15, 15, [
|
|
560
|
+
[5, 4, 'p'],
|
|
561
|
+
[6, 4, 'i'],
|
|
562
|
+
[8, 4, 'o'],
|
|
563
|
+
]);
|
|
564
|
+
const results = generate(gaddag, config, board, [BLANK_CHARACTER]);
|
|
565
|
+
expect(toWords(results)).toEqual(['pillo', 'pilo']);
|
|
566
|
+
expect(results.map((result) => result.points)).toEqual([5, 5]);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('requires the rr tile to play words with a double r', () => {
|
|
570
|
+
const gaddag = Gaddag.fromArray(['carro']);
|
|
571
|
+
const board = createBoard(15, 15, [
|
|
572
|
+
[5, 4, 'c'],
|
|
573
|
+
[6, 4, 'a'],
|
|
574
|
+
]);
|
|
575
|
+
expect(generate(gaddag, config, board, ['r', 'r', 'o'])).toEqual([]);
|
|
576
|
+
const results = generate(gaddag, config, board, ['rr', 'o']);
|
|
577
|
+
expect(summarizeAll(results)).toEqual([
|
|
578
|
+
{
|
|
579
|
+
placed: [
|
|
580
|
+
[7, 4, 'rr', false],
|
|
581
|
+
[8, 4, 'o', false],
|
|
582
|
+
],
|
|
583
|
+
points: 13,
|
|
584
|
+
words: ['carro'],
|
|
585
|
+
},
|
|
586
|
+
]);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
describe('MoveGenerator - bonuses', () => {
|
|
591
|
+
const gaddag = Gaddag.fromArray(['ab']);
|
|
592
|
+
const board = () => createBoard(5, 5, [[2, 2, 'a']]);
|
|
593
|
+
const characterBonus = (x: number, y: number, multiplier: number, score?: number) => ({
|
|
594
|
+
multiplier,
|
|
595
|
+
score,
|
|
596
|
+
type: BONUS_CHARACTER,
|
|
597
|
+
x,
|
|
598
|
+
y,
|
|
599
|
+
});
|
|
600
|
+
const wordBonus = (x: number, y: number, multiplier: number) => ({ multiplier, type: BONUS_WORD, x, y });
|
|
601
|
+
|
|
602
|
+
it('multiplies the placed tile on a character bonus', () => {
|
|
603
|
+
const config = createConfig(BASIC_TILES, { bonuses: [characterBonus(3, 2, 2)] });
|
|
604
|
+
expect(generate(gaddag, config, board(), ['b']).map((result) => result.points)).toEqual([7, 4]);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('multiplies the whole word on a word bonus', () => {
|
|
608
|
+
const config = createConfig(BASIC_TILES, { bonuses: [wordBonus(3, 2, 3)] });
|
|
609
|
+
expect(generate(gaddag, config, board(), ['b']).map((result) => result.points)).toEqual([12, 4]);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('applies a score-gated character bonus only to tiles with the matching score', () => {
|
|
613
|
+
const matching = createConfig(BASIC_TILES, { bonuses: [characterBonus(3, 2, 2, 3)] });
|
|
614
|
+
expect(generate(gaddag, matching, board(), ['b']).map((result) => result.points)).toEqual([7, 4]);
|
|
615
|
+
const mismatched = createConfig(BASIC_TILES, { bonuses: [characterBonus(3, 2, 2, 2)] });
|
|
616
|
+
expect(generate(gaddag, mismatched, board(), ['b']).map((result) => result.points)).toEqual([4, 4]);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('gates blanks by the score of the character they represent', () => {
|
|
620
|
+
const config = createConfig(BASIC_TILES, { blankScore: 2, bonuses: [characterBonus(3, 2, 2, 3)] });
|
|
621
|
+
expect(generate(gaddag, config, board(), [BLANK_CHARACTER]).map((result) => result.points)).toEqual([5, 3]);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('applies the first bonus when several share coordinates', () => {
|
|
625
|
+
const config = createConfig(BASIC_TILES, { bonuses: [characterBonus(3, 2, 2), wordBonus(3, 2, 3)] });
|
|
626
|
+
expect(generate(gaddag, config, board(), ['b']).map((result) => result.points)).toEqual([7, 4]);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('does not apply bonuses under existing tiles', () => {
|
|
630
|
+
const config = createConfig(BASIC_TILES, { bonuses: [characterBonus(2, 2, 2)] });
|
|
631
|
+
expect(generate(gaddag, config, board(), ['b']).map((result) => result.points)).toEqual([4, 4]);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('ignores bonuses outside the board', () => {
|
|
635
|
+
const config = createConfig(BASIC_TILES, { bonuses: [wordBonus(10, 10, 3)] });
|
|
636
|
+
expect(generate(gaddag, config, board(), ['b']).map((result) => result.points)).toEqual([4, 4]);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('applies a bonus to both the main and the perpendicular word', () => {
|
|
640
|
+
const crossBoard = createBoard(5, 5, [
|
|
641
|
+
[2, 1, 'a'],
|
|
642
|
+
[1, 2, 'a'],
|
|
643
|
+
]);
|
|
644
|
+
const characterConfig = createConfig(BASIC_TILES, { bonuses: [characterBonus(2, 2, 2)] });
|
|
645
|
+
expect(generate(gaddag, characterConfig, crossBoard, ['b']).map((result) => result.points)).toEqual([4, 14, 4]);
|
|
646
|
+
const wordConfig = createConfig(BASIC_TILES, { bonuses: [wordBonus(2, 2, 3)] });
|
|
647
|
+
expect(generate(gaddag, wordConfig, crossBoard, ['b']).map((result) => result.points)).toEqual([4, 24, 4]);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('multiplies multiple word bonuses together', () => {
|
|
651
|
+
const config = createConfig(BASIC_TILES, { bonuses: [wordBonus(2, 2, 2), wordBonus(4, 2, 2)] });
|
|
652
|
+
const results = generate(Gaddag.fromArray(['abcd']), config, createBoard(5, 5, [[1, 2, 'a']]), ['b', 'c', 'd']);
|
|
653
|
+
expect(summarizeAll(results)).toEqual([
|
|
654
|
+
{
|
|
655
|
+
placed: [
|
|
656
|
+
[2, 2, 'b', false],
|
|
657
|
+
[3, 2, 'c', false],
|
|
658
|
+
[4, 2, 'd', false],
|
|
659
|
+
],
|
|
660
|
+
points: 36,
|
|
661
|
+
words: ['abcd'],
|
|
662
|
+
},
|
|
663
|
+
]);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
describe('MoveGenerator - bingo', () => {
|
|
668
|
+
const gaddag = Gaddag.fromArray(['abcd', 'ac']);
|
|
669
|
+
const board = () =>
|
|
670
|
+
createBoard(5, 5, [
|
|
671
|
+
[1, 2, 'a'],
|
|
672
|
+
[3, 1, 'a'],
|
|
673
|
+
]);
|
|
674
|
+
|
|
675
|
+
it('adds the score bingo when the whole rack is used', () => {
|
|
676
|
+
const config = createConfig(BASIC_TILES, { rackSize: 3 });
|
|
677
|
+
const results = generate(gaddag, config, board(), ['b', 'c', 'd']);
|
|
678
|
+
expect(toSortedPoints(results)).toEqual([4, 4, 4, 4, 59, 63]);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('does not apply the bingo when fewer tiles than the rack size are placed', () => {
|
|
682
|
+
const config = createConfig(BASIC_TILES, { rackSize: 4 });
|
|
683
|
+
const results = generate(gaddag, config, board(), ['b', 'c', 'd']);
|
|
684
|
+
expect(toSortedPoints(results)).toEqual([4, 4, 4, 4, 9, 13]);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('applies a multiplier bingo to the main word but not to perpendicular words', () => {
|
|
688
|
+
const config = createConfig(BASIC_TILES, { bingo: { multiplier: 3 }, rackSize: 3 });
|
|
689
|
+
const results = generate(gaddag, config, board(), ['b', 'c', 'd']);
|
|
690
|
+
expect(toSortedPoints(results)).toEqual([4, 4, 4, 4, 27, 31]);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
describe('MoveGenerator - result ordering', () => {
|
|
695
|
+
it('orders results by direction, line, start, and end like the previous solver', () => {
|
|
696
|
+
const gaddag = Gaddag.fromArray(['ab', 'ba']);
|
|
697
|
+
const config = createConfig(BASIC_TILES);
|
|
698
|
+
const results = generate(gaddag, config, createBoard(5, 5), ['a', 'b']);
|
|
699
|
+
expectEnumerationOrder(results);
|
|
700
|
+
expect(toWords(results)).toEqual(['ab', 'ba', 'ab', 'ba', 'ab', 'ba', 'ab', 'ba']);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('resolves same-span ties by rack order', () => {
|
|
704
|
+
const gaddag = Gaddag.fromArray(['ab', 'ba']);
|
|
705
|
+
const config = createConfig(BASIC_TILES);
|
|
706
|
+
const results = generate(gaddag, config, createBoard(5, 5), ['b', 'a']);
|
|
707
|
+
expect(toWords(results)).toEqual(['ba', 'ab', 'ba', 'ab', 'ba', 'ab', 'ba', 'ab']);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('orders blank interpretations by tile order, after real tiles', () => {
|
|
711
|
+
const gaddag = Gaddag.fromArray(['ab', 'ac', 'ad']);
|
|
712
|
+
const config = createConfig(BASIC_TILES);
|
|
713
|
+
const board = createBoard(5, 5, [[2, 2, 'a']]);
|
|
714
|
+
const results = generate(gaddag, config, board, ['d', BLANK_CHARACTER]);
|
|
715
|
+
expect(summarizeAll(results)).toEqual([
|
|
716
|
+
{ placed: [[3, 2, 'd', false]], points: 3, words: ['ad'] },
|
|
717
|
+
{ placed: [[3, 2, 'b', true]], points: 1, words: ['ab'] },
|
|
718
|
+
{ placed: [[3, 2, 'c', true]], points: 1, words: ['ac'] },
|
|
719
|
+
{ placed: [[3, 2, 'd', true]], points: 1, words: ['ad'] },
|
|
720
|
+
{
|
|
721
|
+
placed: [
|
|
722
|
+
[1, 3, 'a', true],
|
|
723
|
+
[2, 3, 'd', false],
|
|
724
|
+
],
|
|
725
|
+
points: 5,
|
|
726
|
+
words: ['ad', 'ad'],
|
|
727
|
+
},
|
|
728
|
+
{ placed: [[2, 3, 'd', false]], points: 3, words: ['ad'] },
|
|
729
|
+
{ placed: [[2, 3, 'b', true]], points: 1, words: ['ab'] },
|
|
730
|
+
{ placed: [[2, 3, 'c', true]], points: 1, words: ['ac'] },
|
|
731
|
+
{ placed: [[2, 3, 'd', true]], points: 1, words: ['ad'] },
|
|
732
|
+
{
|
|
733
|
+
placed: [
|
|
734
|
+
[3, 1, 'a', true],
|
|
735
|
+
[3, 2, 'd', false],
|
|
736
|
+
],
|
|
737
|
+
points: 5,
|
|
738
|
+
words: ['ad', 'ad'],
|
|
739
|
+
},
|
|
740
|
+
]);
|
|
741
|
+
expect(results.map(({ id }) => id)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
describe('MoveGenerator - equivalence with a brute-force reference solver', () => {
|
|
746
|
+
const words = [
|
|
747
|
+
'ab',
|
|
748
|
+
'ace',
|
|
749
|
+
'ad',
|
|
750
|
+
'ae',
|
|
751
|
+
'ba',
|
|
752
|
+
'bad',
|
|
753
|
+
'be',
|
|
754
|
+
'bead',
|
|
755
|
+
'bed',
|
|
756
|
+
'bee',
|
|
757
|
+
'cab',
|
|
758
|
+
'cad',
|
|
759
|
+
'cede',
|
|
760
|
+
'dab',
|
|
761
|
+
'dace',
|
|
762
|
+
'dad',
|
|
763
|
+
'deb',
|
|
764
|
+
'deed',
|
|
765
|
+
'ea',
|
|
766
|
+
'ed',
|
|
767
|
+
];
|
|
768
|
+
const gaddag = Gaddag.fromArray(words);
|
|
769
|
+
const tiles: TileScores = { a: 1, b: 3, c: 3, d: 2, e: 1 };
|
|
770
|
+
const config = createConfig(tiles);
|
|
771
|
+
const cluster = (): BoardTilePlacement[] => [
|
|
772
|
+
[1, 1, 'b'],
|
|
773
|
+
[1, 2, 'a'],
|
|
774
|
+
[1, 3, 'd'],
|
|
775
|
+
[2, 2, 'b'],
|
|
776
|
+
[3, 2, 'e'],
|
|
777
|
+
];
|
|
778
|
+
|
|
779
|
+
it('matches on a hand-made cluster', () => {
|
|
780
|
+
const results = expectMatchesReference(gaddag, config, createBoard(5, 5, cluster()), ['a', 'd', 'e']);
|
|
781
|
+
expect(results.length).toBeGreaterThan(0);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it('matches with blanks', () => {
|
|
785
|
+
const results = expectMatchesReference(gaddag, config, createBoard(5, 5, cluster()), ['b', BLANK_CHARACTER]);
|
|
786
|
+
expect(results.length).toBeGreaterThan(0);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('matches on a rectangular board', () => {
|
|
790
|
+
const board = createBoard(7, 5, [
|
|
791
|
+
[3, 2, 'a'],
|
|
792
|
+
[4, 2, 'b'],
|
|
793
|
+
]);
|
|
794
|
+
const results = expectMatchesReference(gaddag, config, board, ['a', 'e', 'd', BLANK_CHARACTER]);
|
|
795
|
+
expect(results.length).toBeGreaterThan(0);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('matches on the empty board', () => {
|
|
799
|
+
const results = expectMatchesReference(gaddag, config, createBoard(5, 5), ['b', 'a', 'd']);
|
|
800
|
+
expect(results.length).toBeGreaterThan(0);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('matches with bonuses', () => {
|
|
804
|
+
const bonusConfig = createConfig(tiles, {
|
|
805
|
+
bonuses: [
|
|
806
|
+
{ multiplier: 2, type: BONUS_CHARACTER, x: 1, y: 2 },
|
|
807
|
+
{ multiplier: 2, score: 3, type: BONUS_CHARACTER, x: 2, y: 1 },
|
|
808
|
+
{ multiplier: 3, type: BONUS_WORD, x: 3, y: 3 },
|
|
809
|
+
{ multiplier: 2, type: BONUS_WORD, x: 0, y: 0 },
|
|
810
|
+
],
|
|
811
|
+
});
|
|
812
|
+
const results = expectMatchesReference(gaddag, bonusConfig, createBoard(5, 5, cluster()), [
|
|
813
|
+
'a',
|
|
814
|
+
'e',
|
|
815
|
+
BLANK_CHARACTER,
|
|
816
|
+
]);
|
|
817
|
+
expect(results.length).toBeGreaterThan(0);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('matches with a score bingo', () => {
|
|
821
|
+
const bingoConfig = createConfig(tiles, { rackSize: 3 });
|
|
822
|
+
const results = expectMatchesReference(gaddag, bingoConfig, createBoard(5, 5, cluster()), ['a', 'd', 'e']);
|
|
823
|
+
expect(results.length).toBeGreaterThan(0);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it('matches with a multiplier bingo', () => {
|
|
827
|
+
const bingoConfig = createConfig(tiles, { bingo: { multiplier: 2 }, rackSize: 3 });
|
|
828
|
+
const results = expectMatchesReference(gaddag, bingoConfig, createBoard(5, 5, cluster()), ['a', 'd', 'e']);
|
|
829
|
+
expect(results.length).toBeGreaterThan(0);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('matches with digraph tiles', () => {
|
|
833
|
+
const spanishWords = ['cal', 'callo', 'chal', 'chico', 'cho', 'col', 'hilo', 'hola', 'loca', 'ola', 'olla'];
|
|
834
|
+
const spanishGaddag = Gaddag.fromArray(spanishWords);
|
|
835
|
+
const spanishConfig = createConfig(SPANISH_MINI_TILES);
|
|
836
|
+
const board = createBoard(6, 6, [
|
|
837
|
+
[1, 2, 'ch'],
|
|
838
|
+
[2, 2, 'i'],
|
|
839
|
+
[3, 2, 'c'],
|
|
840
|
+
[1, 3, 'o'],
|
|
841
|
+
[4, 1, 'll'],
|
|
842
|
+
[4, 2, 'o'],
|
|
843
|
+
[3, 4, 'o'],
|
|
844
|
+
]);
|
|
845
|
+
const tileResults = expectMatchesReference(spanishGaddag, spanishConfig, board, ['ch', 'o', 'l', 'a']);
|
|
846
|
+
expect(tileResults.length).toBeGreaterThan(0);
|
|
847
|
+
const blankResults = expectMatchesReference(spanishGaddag, spanishConfig, board, ['o', 'l', BLANK_CHARACTER]);
|
|
848
|
+
expect(blankResults.length).toBeGreaterThan(0);
|
|
849
|
+
const singleHResults = expectMatchesReference(spanishGaddag, spanishConfig, board, ['h', 'o', 'a']);
|
|
850
|
+
expect(singleHResults).toEqual([]);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('matches on random boards', () => {
|
|
854
|
+
const scenarios: [number, string[]][] = [
|
|
855
|
+
[11, ['a', 'b', BLANK_CHARACTER]],
|
|
856
|
+
[22, ['d', 'e', 'a']],
|
|
857
|
+
[33, ['b', 'c', BLANK_CHARACTER]],
|
|
858
|
+
];
|
|
859
|
+
let totalResults = 0;
|
|
860
|
+
|
|
861
|
+
for (const [seed, rack] of scenarios) {
|
|
862
|
+
const board = createRandomBoard(5, 5, ['a', 'b', 'c', 'd', 'e'], createRandom(seed));
|
|
863
|
+
totalResults += generate(gaddag, config, board, rack).length;
|
|
864
|
+
expectMatchesReference(gaddag, config, board, rack);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
expect(totalResults).toBeGreaterThan(0);
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
function generate(gaddag: Gaddag, config: Config, board: Board, rack: string[]): Result[] {
|
|
872
|
+
return generateJson(gaddag, config, board, rack).map((json) => Result.fromJson(json, board));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function generateJson(gaddag: Gaddag, config: Config, board: Board, rack: string[]): ResultJson[] {
|
|
876
|
+
return new MoveGenerator(gaddag, config, board, createRack(rack)).run();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function expectMatchesReference(gaddag: Gaddag, config: Config, board: Board, rack: string[]): Result[] {
|
|
880
|
+
const results = generate(gaddag, config, board, rack);
|
|
881
|
+
const signatures = results.map(signatureOf);
|
|
882
|
+
expect(new Set(signatures).size).toBe(signatures.length);
|
|
883
|
+
expect(results.map(({ id }) => id)).toEqual(results.map((_, index) => index));
|
|
884
|
+
expectEnumerationOrder(results);
|
|
885
|
+
expect([...signatures].sort()).toEqual(new ReferenceSolver(gaddag, config, board, createRack(rack)).solve());
|
|
886
|
+
return results;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function summarizeAll(results: Result[]): MoveSummary[] {
|
|
890
|
+
return results.map(summarize);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function summarize(result: Result): MoveSummary {
|
|
894
|
+
return {
|
|
895
|
+
placed: result.cells.filter((cell) => cell.isEmpty).map(toPlacedTile),
|
|
896
|
+
points: result.points,
|
|
897
|
+
words: result.words,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function toPlacedTile(cell: Cell): PlacedTile {
|
|
902
|
+
return [cell.x, cell.y, cell.tile.character, cell.tile.isBlank];
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function toWords(results: Result[]): string[] {
|
|
906
|
+
return results.map((result) => result.word);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function toSortedPoints(results: Result[]): number[] {
|
|
910
|
+
return results.map((result) => result.points).sort((a, b) => a - b);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function coversCell(result: Result, x: number, y: number): boolean {
|
|
914
|
+
return result.cells.some((cell) => cell.x === x && cell.y === y);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function signatureOf(result: Result): string {
|
|
918
|
+
const placed = result.cells
|
|
919
|
+
.filter((cell) => cell.isEmpty)
|
|
920
|
+
.map((cell) => `${cell.x},${cell.y},${cell.tile.character},${cell.tile.isBlank ? 1 : 0}`)
|
|
921
|
+
.sort()
|
|
922
|
+
.join(' ');
|
|
923
|
+
const words = [...result.words].sort().join(' ');
|
|
924
|
+
return `${placed} | ${words} | ${result.points}`;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function expectEnumerationOrder(results: Result[]): void {
|
|
928
|
+
const keys = results.map(enumerationKey);
|
|
929
|
+
expect(keys).toEqual([...keys].sort());
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function enumerationKey(result: Result): string {
|
|
933
|
+
const [firstCell] = result.cells;
|
|
934
|
+
const lastCell = result.cells[result.cells.length - 1];
|
|
935
|
+
const isHorizontal = firstCell.y === lastCell.y;
|
|
936
|
+
const key = isHorizontal ? [0, firstCell.y, firstCell.x, lastCell.x] : [1, firstCell.x, firstCell.y, lastCell.y];
|
|
937
|
+
return key.map((value) => String(value).padStart(2, '0')).join('|');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function createConfig(tileScores: TileScores, overrides: Partial<ConfigJson> = {}): Config {
|
|
941
|
+
return new Config({
|
|
942
|
+
bingo: { score: 50 },
|
|
943
|
+
blankScore: 0,
|
|
944
|
+
blanksCount: 2,
|
|
945
|
+
boardHeight: 15,
|
|
946
|
+
boardWidth: 15,
|
|
947
|
+
bonuses: [],
|
|
948
|
+
game: Game.Scrabble,
|
|
949
|
+
locale: Locale.EN_GB,
|
|
950
|
+
name: 'MoveGenerator tests',
|
|
951
|
+
rackSize: 7,
|
|
952
|
+
tiles: Object.entries(tileScores).map(([character, score]) => ({ character, count: 1, score })),
|
|
953
|
+
...overrides,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function createBoard(width: number, height: number, placements: BoardTilePlacement[] = []): Board {
|
|
958
|
+
const board = Board.create(width, height);
|
|
959
|
+
|
|
960
|
+
for (const [x, y, character, isBlank = false] of placements) {
|
|
961
|
+
board.updateCell(x, y, () => new Cell({ isEmpty: false, tile: new Tile({ character, isBlank }), x, y }));
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return board;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function createRack(characters: string[]): Tile[] {
|
|
968
|
+
return characters.map((character) =>
|
|
969
|
+
character === BLANK_CHARACTER
|
|
970
|
+
? new Tile({ character: '', isBlank: true })
|
|
971
|
+
: new Tile({ character, isBlank: false }),
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function createRandomBoard(width: number, height: number, characters: string[], random: () => number): Board {
|
|
976
|
+
const placements: BoardTilePlacement[] = [];
|
|
977
|
+
|
|
978
|
+
for (let y = 0; y < height; ++y) {
|
|
979
|
+
for (let x = 0; x < width; ++x) {
|
|
980
|
+
if (random() < 0.2) {
|
|
981
|
+
placements.push([x, y, characters[Math.floor(random() * characters.length)]]);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (placements.length === 0) {
|
|
987
|
+
placements.push([Math.floor(width / 2), Math.floor(height / 2), characters[0]]);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return createBoard(width, height, placements);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function createRandom(seed: number): () => number {
|
|
994
|
+
let state = (seed % 2147483647) + 1;
|
|
995
|
+
return () => {
|
|
996
|
+
state = (state * 48271) % 2147483647;
|
|
997
|
+
return state / 2147483647;
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function createWideAlphabet(size: number): TileScores {
|
|
1002
|
+
const tiles: TileScores = { a: 1, b: 3 };
|
|
1003
|
+
|
|
1004
|
+
for (let index = 2; index < size; ++index) {
|
|
1005
|
+
tiles[String.fromCharCode(0x100 + index)] = 1;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return tiles;
|
|
1009
|
+
}
|