@scrabble-solver/scrabble-solver 2.18.1 → 2.18.3
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/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +14 -14
- package/.next/cache/.previewinfo +1 -1
- package/.next/cache/.rscinfo +1 -1
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/.next/diagnostics/framework.json +1 -1
- package/.next/dynamic-css-manifest.json +2 -2
- package/.next/prerender-manifest.json +4 -4
- package/.next/react-loadable-manifest.json +37 -37
- package/.next/routes-manifest.json +1 -1
- package/.next/server/chunks/309.js +1 -1
- package/.next/server/chunks/344.js +1 -1
- package/.next/server/chunks/371.js +1 -1
- package/.next/server/chunks/392.js +1 -1
- package/.next/server/chunks/465.js +1 -1
- package/.next/server/chunks/546.js +1 -1
- package/.next/server/chunks/641.js +1 -1
- package/.next/server/chunks/67.js +1 -0
- package/.next/server/chunks/712.js +1 -1
- package/.next/server/chunks/812.js +1 -1
- package/.next/server/chunks/846.js +1 -1
- package/.next/server/chunks/886.js +1 -1
- package/.next/server/chunks/942.js +1 -0
- package/.next/server/chunks/953.js +1 -1
- package/.next/server/chunks/961.js +1 -1
- package/.next/server/dynamic-css-manifest.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +3 -3
- package/.next/server/pages/500.html +3 -3
- package/.next/server/pages/_app.js +1 -1
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/_document.js +2 -2
- package/.next/server/pages/_document.js.nft.json +1 -1
- package/.next/server/pages/_error.js +1 -1
- package/.next/server/pages/_error.js.nft.json +1 -1
- package/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
- package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
- package/.next/server/pages/api/dictionary/[locale].js +1 -1
- package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
- package/.next/server/pages/api/solve.js +1 -1
- package/.next/server/pages/api/solve.js.nft.json +1 -1
- package/.next/server/pages/api/verify.js +1 -1
- package/.next/server/pages/api/verify.js.nft.json +1 -1
- package/.next/server/pages/index.html +3 -3
- package/.next/server/pages/index.js +4 -4
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/server/pages/not-found.html +3 -3
- package/.next/server/pages/not-found.js.nft.json +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/static/{5KOWzYqfY1Z2rjrzptr8D → 8WnIUTe-yJvPD6stltvYI}/_buildManifest.js +1 -1
- package/.next/static/chunks/100.305bdf9c75b1758f.js +1 -0
- package/.next/static/chunks/256.6a20b23a766cfebd.js +1 -0
- package/.next/static/chunks/{307.67f52f13e3d60d4e.js → 307.89e6100f58443679.js} +1 -1
- package/.next/static/chunks/{363.01eed3233f92b6a4.js → 363.cec65b1f9668a75e.js} +1 -1
- package/.next/static/chunks/{422.1559ae6756912ca5.js → 422.3e55b4c94e328701.js} +1 -1
- package/.next/static/chunks/{663.d2c9ca36b26695b8.js → 663.bd2c45413fbca518.js} +1 -1
- package/.next/static/chunks/693.677c88e0eb1340b5.js +1 -0
- package/.next/static/chunks/{820.97b82f751ca49579.js → 820.823e9e885ebb66af.js} +1 -1
- package/.next/static/chunks/827.c81b92b383a748c9.js +1 -0
- package/.next/static/chunks/{865.a9b563a45848a83c.js → 865.59baba8296c9b116.js} +1 -1
- package/.next/static/chunks/912.077603e4f0bc6cca.js +1 -0
- package/.next/static/chunks/{939.19b33a3d3c351d73.js → 939.2ad86b50405c7897.js} +1 -1
- package/.next/static/chunks/980.7ce17464fc56648a.js +1 -0
- package/.next/static/chunks/{main-b92e08f3fc96dd8d.js → main-7e52da35a4beea0b.js} +1 -1
- package/.next/static/chunks/pages/_app-e3fe880e24c024ab.js +1 -0
- package/.next/static/chunks/pages/index-b073a1686818dd26.js +1 -0
- package/.next/static/chunks/webpack-d7138f1ef6b8c9d6.js +1 -0
- package/.next/static/css/65bcc9440e067113.css +1 -0
- package/.next/static/css/{4dbcacc1122ac6b9.css → 92f85ce358a0a468.css} +1 -1
- package/.next/static/css/a691348b788f8bc0.css +1 -0
- package/.next/static/css/cd4d7f2458487399.css +1 -0
- package/.next/trace +22 -21
- package/.next/trace-build +1 -1
- package/AGENTS.md +9 -0
- package/CLAUDE.md +1 -0
- package/package.json +14 -12
- package/public/llms.txt +1 -1
- package/src/components/Board/Board.tsx +3 -1
- package/src/components/Board/BoardPure.tsx +26 -20
- package/src/components/Board/components/Cell/Cell.tsx +4 -4
- package/src/components/Board/lib.test.ts +106 -0
- package/src/components/Board/lib.ts +34 -3
- package/src/components/Board/selectors.ts +17 -5
- package/src/components/Button/Button.module.scss +1 -1
- package/src/components/Modal/Modal.module.scss +0 -4
- package/src/components/Results/Header.tsx +30 -5
- package/src/components/Results/Result.tsx +45 -55
- package/src/components/Results/Results.module.scss +0 -126
- package/src/components/Results/Results.module.scss.d.ts +0 -10
- package/src/components/Results/Results.tsx +25 -5
- package/src/components/Solver/Solver.module.scss +4 -4
- package/src/components/Solver/Solver.tsx +5 -3
- package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +5 -1
- package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss.d.ts +1 -0
- package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +5 -2
- package/src/components/Table/Cell.tsx +42 -0
- package/src/components/Table/Header.tsx +11 -0
- package/src/components/{Results → Table}/HeaderButton.tsx +22 -14
- package/src/components/Table/Row.tsx +59 -0
- package/src/components/{ResultsInput/ResultsInput.tsx → Table/Search.tsx} +9 -11
- package/src/components/Table/Table.module.scss +147 -0
- package/src/components/Table/Table.module.scss.d.ts +19 -0
- package/src/components/Table/index.ts +5 -0
- package/src/components/Tile/Tile.module.scss +2 -1
- package/src/hooks/useColumns.ts +7 -5
- package/src/hooks/useIsCompactLayout.ts +14 -0
- package/src/i18n/languages/english.json +6 -1
- package/src/i18n/languages/french.json +6 -1
- package/src/i18n/languages/german.json +6 -1
- package/src/i18n/languages/persian.json +6 -1
- package/src/i18n/languages/polish.json +6 -1
- package/src/i18n/languages/romanian.json +6 -1
- package/src/i18n/languages/spanish.json +6 -1
- package/src/i18n/languages/turkish.json +6 -1
- package/src/icons/QuestionSquare.svg +5 -0
- package/src/lib/createCoordinatesComparator.ts +23 -0
- package/src/lib/createKeyComparator.ts +1 -4
- package/src/lib/createSortComparator.ts +22 -0
- package/src/lib/getCoordinates.test.ts +21 -0
- package/src/lib/getCoordinates.ts +16 -0
- package/src/lib/getNextSort.ts +12 -0
- package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +3 -1
- package/src/modals/RemainingTilesModal/components/Character/Character.tsx +8 -9
- package/src/modals/ResultsModal/ResultsModal.tsx +14 -10
- package/src/modals/WordsModal/WordsModal.module.scss +16 -27
- package/src/modals/WordsModal/WordsModal.module.scss.d.ts +3 -6
- package/src/modals/WordsModal/WordsModal.tsx +56 -42
- package/src/modals/WordsModal/components/WordsTable/WordRow.tsx +92 -0
- package/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss +69 -0
- package/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss.d.ts +16 -0
- package/src/modals/WordsModal/components/WordsTable/WordsTable.tsx +137 -0
- package/src/modals/WordsModal/components/WordsTable/index.ts +1 -0
- package/src/modals/WordsModal/components/index.ts +1 -0
- package/src/pages/api/verify.ts +7 -6
- package/src/pages/index.tsx +29 -4
- package/src/parameters/index.ts +2 -0
- package/src/sdk/verify.ts +3 -2
- package/src/solver-worker/client.test.ts +10 -3
- package/src/solver-worker/index.worker.ts +7 -7
- package/src/solver-worker/messages.ts +3 -3
- package/src/state/hoveredWord/index.ts +4 -0
- package/src/state/hoveredWord/initialState.ts +5 -0
- package/src/state/hoveredWord/selectors.ts +5 -0
- package/src/state/hoveredWord/slice.ts +18 -0
- package/src/state/hoveredWord/types.ts +5 -0
- package/src/state/index.ts +1 -0
- package/src/state/localStorage.ts +13 -2
- package/src/state/results/lib.ts +9 -26
- package/src/state/results/selectors.ts +9 -1
- package/src/state/results/slice.ts +4 -16
- package/src/state/sagas.ts +26 -1
- package/src/state/selectors.ts +18 -1
- package/src/state/store.ts +2 -0
- package/src/state/types.ts +2 -0
- package/src/state/verify/initialState.ts +7 -0
- package/src/state/verify/lib.test.ts +55 -0
- package/src/state/verify/lib.ts +49 -0
- package/src/state/verify/selectors.ts +37 -0
- package/src/state/verify/slice.ts +13 -2
- package/src/state/verify/types.ts +6 -2
- package/src/styles/_tokens.scss +2 -0
- package/src/styles/mixins.scss +5 -0
- package/src/styles/variables.module.scss +1 -0
- package/src/styles/variables.module.scss.d.ts +1 -0
- package/src/styles/variables.scss +1 -1
- package/src/types/index.ts +24 -11
- package/tsconfig.tsbuildinfo +1 -1
- package/.next/server/chunks/316.js +0 -1
- package/.next/server/chunks/916.js +0 -1
- package/.next/static/chunks/106.e414e88694add27f.js +0 -1
- package/.next/static/chunks/127.125d896a19061d71.js +0 -1
- package/.next/static/chunks/18.8b91e137d239e15c.js +0 -1
- package/.next/static/chunks/583.7a46807be14c377e.js +0 -1
- package/.next/static/chunks/711.ba7b8f900dfd40d0.js +0 -1
- package/.next/static/chunks/912.ff8037ba9d4db5c8.js +0 -1
- package/.next/static/chunks/pages/_app-2e607dcd4135f340.js +0 -1
- package/.next/static/chunks/pages/index-e0087247c995328d.js +0 -1
- package/.next/static/chunks/webpack-cfb34d175e081981.js +0 -1
- package/.next/static/css/b74cd7de82dabcdd.css +0 -1
- package/.next/static/css/c403ce739a9c0d83.css +0 -1
- package/.next/static/css/fbfdce70731d0d2b.css +0 -1
- package/src/components/Results/Cell.tsx +0 -32
- package/src/components/ResultsInput/ResultsInput.module.scss +0 -12
- package/src/components/ResultsInput/ResultsInput.module.scss.d.ts +0 -7
- package/src/components/ResultsInput/index.ts +0 -1
- /package/.next/static/{5KOWzYqfY1Z2rjrzptr8D → 8WnIUTe-yJvPD6stltvYI}/_ssgManifest.js +0 -0
package/.next/trace-build
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"name":"run-typescript","duration":
|
|
1
|
+
[{"name":"run-typescript","duration":160243,"timestamp":6454978890,"id":13,"parentId":1,"tags":{},"startTime":1787605800486,"traceId":"f92ac34b43b8b857"},{"name":"run-webpack","duration":5760453,"timestamp":6455141586,"id":15,"parentId":1,"tags":{},"startTime":1787605800649,"traceId":"f92ac34b43b8b857"},{"name":"static-check","duration":171530,"timestamp":6460973542,"id":2106,"parentId":1,"tags":{},"startTime":1787605806481,"traceId":"f92ac34b43b8b857"},{"name":"static-generation","duration":249530,"timestamp":6461155014,"id":2124,"parentId":1,"tags":{},"startTime":1787605806662,"traceId":"f92ac34b43b8b857"},{"name":"collect-build-traces","duration":4270405,"timestamp":6461145430,"id":2121,"parentId":1,"tags":{},"startTime":1787605806652,"traceId":"f92ac34b43b8b857"},{"name":"telemetry-flush","duration":19,"timestamp":6465417121,"id":2131,"parentId":1,"tags":{},"startTime":1787605810924,"traceId":"f92ac34b43b8b857"},{"name":"next-build","duration":10674125,"timestamp":6454743019,"id":1,"tags":{"buildMode":"default","version":"16.3.2","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1787605800250,"traceId":"f92ac34b43b8b857"}]
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- BEGIN:nextjs-agent-rules -->
|
|
2
|
+
|
|
3
|
+
# This is NOT the Next.js you know
|
|
4
|
+
|
|
5
|
+
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
|
6
|
+
|
|
7
|
+
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
|
8
|
+
|
|
9
|
+
<!-- END:nextjs-agent-rules -->
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scrabble-solver/scrabble-solver",
|
|
3
|
-
"version": "2.18.
|
|
3
|
+
"version": "2.18.3",
|
|
4
4
|
"description": "Scrabble Solver web app: free word finder and board solver for Scrabble, Super Scrabble, Literaki, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"scrabble",
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"build": "bun run generate-scss-types && env-cmd next build --webpack",
|
|
46
46
|
"clean": "rimraf --glob .next/ 'src/**/*.module.scss.d.ts'",
|
|
47
47
|
"debug": "NODE_OPTIONS='--inspect' next dev --webpack",
|
|
48
|
-
"dev": "
|
|
48
|
+
"dev": "bun run --parallel --no-orphans 'dev:*'",
|
|
49
|
+
"dev:next": "env-cmd next dev --webpack",
|
|
50
|
+
"dev:scss": "bun run generate-scss-types --watch",
|
|
49
51
|
"generate-scss-types": "bun scripts/generate-scss-types.ts",
|
|
50
52
|
"start": "env-cmd next start -p 3333",
|
|
51
53
|
"test": "bun test --timeout 180000 src",
|
|
@@ -55,18 +57,18 @@
|
|
|
55
57
|
"@floating-ui/react": "^0.27.20",
|
|
56
58
|
"@kamilmielnik/gaddag": "^2.0.0",
|
|
57
59
|
"@reduxjs/toolkit": "^2.12.0",
|
|
58
|
-
"@scrabble-solver/configs": "^2.18.
|
|
59
|
-
"@scrabble-solver/constants": "^2.18.
|
|
60
|
-
"@scrabble-solver/dictionaries": "^2.18.
|
|
61
|
-
"@scrabble-solver/logger": "^2.18.
|
|
62
|
-
"@scrabble-solver/solver": "^2.18.
|
|
63
|
-
"@scrabble-solver/types": "^2.18.
|
|
64
|
-
"@scrabble-solver/word-definitions": "^2.18.
|
|
60
|
+
"@scrabble-solver/configs": "^2.18.3",
|
|
61
|
+
"@scrabble-solver/constants": "^2.18.3",
|
|
62
|
+
"@scrabble-solver/dictionaries": "^2.18.3",
|
|
63
|
+
"@scrabble-solver/logger": "^2.18.3",
|
|
64
|
+
"@scrabble-solver/solver": "^2.18.3",
|
|
65
|
+
"@scrabble-solver/types": "^2.18.3",
|
|
66
|
+
"@scrabble-solver/word-definitions": "^2.18.3",
|
|
65
67
|
"classnames": "^2.5.1",
|
|
66
68
|
"env-cmd": "^11.0.0",
|
|
67
69
|
"include-media": "^2.0.0",
|
|
68
70
|
"include-media-query-builder": "^1.1.0",
|
|
69
|
-
"next": "^16.3.
|
|
71
|
+
"next": "^16.3.2",
|
|
70
72
|
"normalize.css": "^8.0.1",
|
|
71
73
|
"react": "^19.2.8",
|
|
72
74
|
"react-cool-onclickoutside": "^1.7.0",
|
|
@@ -92,7 +94,7 @@
|
|
|
92
94
|
"@types/react-modal": "^3.16.3",
|
|
93
95
|
"@types/react-portal": "^4.0.7",
|
|
94
96
|
"postcss": "^8.5.26",
|
|
95
|
-
"sass": "^1.
|
|
97
|
+
"sass": "^1.103.1"
|
|
96
98
|
},
|
|
97
|
-
"gitHead": "
|
|
99
|
+
"gitHead": "a1b4a5735a1a48fbb5dfe7f214a7df2158309202"
|
|
98
100
|
}
|
package/public/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Scrabble Solver
|
|
2
2
|
|
|
3
|
-
> Free and open-source analysis tool for Scrabble, Super Scrabble, Scrabble Duel, Letter League, Crossplay, Literaki, and
|
|
3
|
+
> Free and open-source analysis tool for Scrabble, Super Scrabble, Scrabble Duel, Letter League, Crossplay, Literaki, Kelimelik, and Wyrazy. Quickly finds top-scoring words for a given board and rack in English, French, German, Persian, Polish, Romanian, Spanish, and Turkish.
|
|
4
4
|
|
|
5
5
|
Scrabble Solver is a single-page web app. Enter the board state and your tiles to get every legal move ranked by score, check whether a word is allowed in the selected language's dictionary, and look up word definitions. Once a dictionary is cached, solving also works offline.
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ import styles from './Board.module.scss';
|
|
|
26
26
|
import { BoardPure } from './BoardPure';
|
|
27
27
|
import { Actions, InputPrompt } from './components';
|
|
28
28
|
import { useBoardStyle, useFloatingActions, useFloatingFocus, useFloatingInputPrompt, useGrid } from './hooks';
|
|
29
|
-
import { selectReachableCells, selectRowsWithCandidate } from './selectors';
|
|
29
|
+
import { selectHoveredWordCells, selectReachableCells, selectRowsWithCandidate } from './selectors';
|
|
30
30
|
|
|
31
31
|
interface Props {
|
|
32
32
|
className?: string;
|
|
@@ -39,6 +39,7 @@ export const Board: FunctionComponent<Props> = ({ className }) => {
|
|
|
39
39
|
const config = useTypedSelector(selectConfig);
|
|
40
40
|
const rows = useTypedSelector(selectRowsWithCandidate);
|
|
41
41
|
const reachableCells = useTypedSelector(selectReachableCells);
|
|
42
|
+
const hoveredWordCells = useTypedSelector(selectHoveredWordCells);
|
|
42
43
|
const inputMode = useTypedSelector(selectInputMode);
|
|
43
44
|
const cellFilters = useTypedSelector(selectCellFilters);
|
|
44
45
|
const showCoordinates = useTypedSelector(selectShowCoordinates);
|
|
@@ -148,6 +149,7 @@ export const Board: FunctionComponent<Props> = ({ className }) => {
|
|
|
148
149
|
config={config}
|
|
149
150
|
direction={LOCALE_FEATURES[locale].direction}
|
|
150
151
|
hoveredCharacter={hoveredCharacter}
|
|
152
|
+
hoveredWordCells={hoveredWordCells}
|
|
151
153
|
inputRefs={inputRefs}
|
|
152
154
|
locale={locale}
|
|
153
155
|
reachableCells={reachableCells}
|
|
@@ -33,6 +33,7 @@ interface Props {
|
|
|
33
33
|
config: Config;
|
|
34
34
|
direction: TextDirection;
|
|
35
35
|
hoveredCharacter: string | null;
|
|
36
|
+
hoveredWordCells: boolean[][] | null;
|
|
36
37
|
inputRefs: RefObject<HTMLInputElement | null>[][];
|
|
37
38
|
locale: Locale;
|
|
38
39
|
reachableCells: boolean[][] | null;
|
|
@@ -55,6 +56,7 @@ const BoardPureBase = forwardRef<HTMLDivElement, Props>(
|
|
|
55
56
|
direction,
|
|
56
57
|
cellFilters,
|
|
57
58
|
hoveredCharacter,
|
|
59
|
+
hoveredWordCells,
|
|
58
60
|
inputRefs,
|
|
59
61
|
locale,
|
|
60
62
|
reachableCells,
|
|
@@ -118,25 +120,29 @@ const BoardPureBase = forwardRef<HTMLDivElement, Props>(
|
|
|
118
120
|
{getCoordinate(y, showCoordinates === 'original' ? 'number' : 'letter')}
|
|
119
121
|
</div>
|
|
120
122
|
|
|
121
|
-
{cells.map((cell, x) =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
123
|
+
{cells.map((cell, x) => {
|
|
124
|
+
const isHoveredWord = hoveredWordCells ? hoveredWordCells[y][x] : false;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Cell
|
|
128
|
+
cell={cell}
|
|
129
|
+
cellBottom={y < rows.length - 1 ? rows[y + 1][x] : undefined}
|
|
130
|
+
cellLeft={x > 0 ? rows[y][x - 1] : undefined}
|
|
131
|
+
cellRight={x < rows[y].length - 1 ? rows[y][x + 1] : undefined}
|
|
132
|
+
cellTop={y > 0 ? rows[y - 1][x] : undefined}
|
|
133
|
+
config={config}
|
|
134
|
+
highlighted={cell.isCandidate() || isHoverCharacter(cell, hoveredCharacter) || isHoveredWord}
|
|
135
|
+
inputRef={inputRefs[y][x]}
|
|
136
|
+
isReachable={reachableCells ? reachableCells[y][x] : true}
|
|
137
|
+
key={x}
|
|
138
|
+
locale={locale}
|
|
139
|
+
showCoordinates={showCoordinates}
|
|
140
|
+
translate={translate}
|
|
141
|
+
onChange={onChange}
|
|
142
|
+
onFocus={onFocus}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
})}
|
|
140
146
|
</Fragment>
|
|
141
147
|
))}
|
|
142
148
|
</div>
|
|
@@ -145,7 +151,7 @@ const BoardPureBase = forwardRef<HTMLDivElement, Props>(
|
|
|
145
151
|
|
|
146
152
|
export const BoardPure = memo(BoardPureBase);
|
|
147
153
|
|
|
148
|
-
function
|
|
154
|
+
function isHoverCharacter(cell: CellModel, hoveredCharacter: string | null): boolean {
|
|
149
155
|
if (cell.tile.character === EMPTY_CELL || hoveredCharacter === null) {
|
|
150
156
|
return false;
|
|
151
157
|
}
|
|
@@ -28,8 +28,8 @@ interface Props {
|
|
|
28
28
|
cellTop?: CellModel;
|
|
29
29
|
className?: string;
|
|
30
30
|
config: Config;
|
|
31
|
+
highlighted?: boolean;
|
|
31
32
|
inputRef: RefObject<HTMLInputElement | null>;
|
|
32
|
-
isHoverMatch: boolean;
|
|
33
33
|
isReachable?: boolean;
|
|
34
34
|
locale: Locale;
|
|
35
35
|
showCoordinates: ShowCoordinates;
|
|
@@ -39,7 +39,7 @@ interface Props {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Must not subscribe to the store - it can read the
|
|
42
|
+
* Must not subscribe to the store - it can read the store only in event handlers
|
|
43
43
|
*/
|
|
44
44
|
const CellBase: FunctionComponent<Props> = ({
|
|
45
45
|
cell,
|
|
@@ -49,8 +49,8 @@ const CellBase: FunctionComponent<Props> = ({
|
|
|
49
49
|
cellTop,
|
|
50
50
|
className,
|
|
51
51
|
config,
|
|
52
|
+
highlighted,
|
|
52
53
|
inputRef,
|
|
53
|
-
isHoverMatch,
|
|
54
54
|
isReachable = true,
|
|
55
55
|
locale,
|
|
56
56
|
showCoordinates,
|
|
@@ -116,7 +116,7 @@ const CellBase: FunctionComponent<Props> = ({
|
|
|
116
116
|
[styles.unreachable]: !isReachable,
|
|
117
117
|
})}
|
|
118
118
|
character={isEmpty ? undefined : tile.character}
|
|
119
|
-
highlighted={
|
|
119
|
+
highlighted={highlighted}
|
|
120
120
|
inputRef={inputRef}
|
|
121
121
|
isBlank={tile.isBlank}
|
|
122
122
|
isValid={isValid}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Board, Cell, Result, Tile } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import { getReachableCellsFromResults, getWordCells } from './lib';
|
|
4
|
+
|
|
5
|
+
describe('getReachableCellsFromResults', () => {
|
|
6
|
+
const board = Board.fromStringArray([' ', ' ', ' i ', ' ', ' ']);
|
|
7
|
+
|
|
8
|
+
it('marks only cells with tiles as reachable when there are no results', () => {
|
|
9
|
+
expect(getReachableCellsFromResults(board, [])).toEqual([
|
|
10
|
+
[false, false, false, false, false],
|
|
11
|
+
[false, false, false, false, false],
|
|
12
|
+
[false, false, true, false, false],
|
|
13
|
+
[false, false, false, false, false],
|
|
14
|
+
[false, false, false, false, false],
|
|
15
|
+
]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('marks cells covered by any result as reachable and all other empty cells as unreachable', () => {
|
|
19
|
+
const results = [
|
|
20
|
+
createResult([createPlacedCell('q', 1, 2), board.rows[2][2]]),
|
|
21
|
+
createResult([createPlacedCell('q', 2, 1), board.rows[2][2]]),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
expect(getReachableCellsFromResults(board, results)).toEqual([
|
|
25
|
+
[false, false, false, false, false],
|
|
26
|
+
[false, false, true, false, false],
|
|
27
|
+
[false, true, true, false, false],
|
|
28
|
+
[false, false, false, false, false],
|
|
29
|
+
[false, false, false, false, false],
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('getWordCells', () => {
|
|
35
|
+
const board = Board.fromStringArray([' ', ' cat ', ' o ', ' d x', ' ']);
|
|
36
|
+
|
|
37
|
+
it('marks the cells of a horizontal word', () => {
|
|
38
|
+
expect(getWordCells(board, { direction: 'horizontal', word: 'cat', x: 1, y: 1 })).toEqual([
|
|
39
|
+
[false, false, false, false, false],
|
|
40
|
+
[false, true, true, true, false],
|
|
41
|
+
[false, false, false, false, false],
|
|
42
|
+
[false, false, false, false, false],
|
|
43
|
+
[false, false, false, false, false],
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('marks the cells of a vertical word', () => {
|
|
48
|
+
expect(getWordCells(board, { direction: 'vertical', word: 'cod', x: 1, y: 1 })).toEqual([
|
|
49
|
+
[false, false, false, false, false],
|
|
50
|
+
[false, true, false, false, false],
|
|
51
|
+
[false, true, false, false, false],
|
|
52
|
+
[false, true, false, false, false],
|
|
53
|
+
[false, false, false, false, false],
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('stops at the board edge', () => {
|
|
58
|
+
expect(getWordCells(board, { direction: 'horizontal', word: 'x', x: 4, y: 3 })).toEqual([
|
|
59
|
+
[false, false, false, false, false],
|
|
60
|
+
[false, false, false, false, false],
|
|
61
|
+
[false, false, false, false, false],
|
|
62
|
+
[false, false, false, false, true],
|
|
63
|
+
[false, false, false, false, false],
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('marks the run of cells regardless of the word length in characters', () => {
|
|
68
|
+
const digraphBoard = new Board({
|
|
69
|
+
rows: [
|
|
70
|
+
[createFilledCell('ch', 0, 0), createFilledCell('e', 1, 0), createEmptyCell(2, 0)],
|
|
71
|
+
[createEmptyCell(0, 1), createEmptyCell(1, 1), createEmptyCell(2, 1)],
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(getWordCells(digraphBoard, { direction: 'horizontal', word: 'che', x: 0, y: 0 })).toEqual([
|
|
76
|
+
[true, true, false],
|
|
77
|
+
[false, false, false],
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('marks nothing when the starting cell is empty', () => {
|
|
82
|
+
expect(getWordCells(board, { direction: 'horizontal', word: 'gone', x: 0, y: 4 })).toEqual([
|
|
83
|
+
[false, false, false, false, false],
|
|
84
|
+
[false, false, false, false, false],
|
|
85
|
+
[false, false, false, false, false],
|
|
86
|
+
[false, false, false, false, false],
|
|
87
|
+
[false, false, false, false, false],
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function createResult(cells: Cell[]): Result {
|
|
93
|
+
return new Result({ cells, collisions: [], id: 0, points: 0 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createPlacedCell(character: string, x: number, y: number): Cell {
|
|
97
|
+
return new Cell({ isEmpty: true, tile: new Tile({ character }), x, y });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createFilledCell(character: string, x: number, y: number): Cell {
|
|
101
|
+
return new Cell({ isEmpty: false, tile: new Tile({ character }), x, y });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createEmptyCell(x: number, y: number): Cell {
|
|
105
|
+
return new Cell({ isEmpty: true, tile: Tile.Null, x, y });
|
|
106
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BONUS_WORD } from '@scrabble-solver/constants';
|
|
2
|
-
import { type Board, type Bonus, type
|
|
2
|
+
import { type Board, type BoardWord, type Bonus, type Result } from '@scrabble-solver/types';
|
|
3
3
|
|
|
4
4
|
import { COLOR_BONUS_CHARACTER, COLOR_BONUS_CHARACTER_MULTIPLIER, COLOR_BONUS_WORD } from '@/parameters';
|
|
5
5
|
import { type Direction, type Point } from '@/types';
|
|
@@ -36,8 +36,9 @@ export const getPositionInGrid = <T>(grid: T[][], constraint: (value: T) => bool
|
|
|
36
36
|
return undefined;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
export const getReachableCells = (
|
|
40
|
-
const
|
|
39
|
+
export const getReachableCells = (board: Board, charactersCount: number): boolean[][] => {
|
|
40
|
+
const boardHeight = board.rowsCount;
|
|
41
|
+
const boardWidth = board.columnsCount;
|
|
41
42
|
const reachable = createGrid(boardWidth, boardHeight, () => false);
|
|
42
43
|
|
|
43
44
|
if (charactersCount === 0) {
|
|
@@ -116,3 +117,33 @@ export const getReachableCells = (config: Config, board: Board, charactersCount:
|
|
|
116
117
|
|
|
117
118
|
return reachable;
|
|
118
119
|
};
|
|
120
|
+
|
|
121
|
+
export const getReachableCellsFromResults = (board: Board, results: Result[]): boolean[][] => {
|
|
122
|
+
const reachable = createGrid(board.columnsCount, board.rowsCount, (x, y) => board.rows[y][x].hasTile());
|
|
123
|
+
|
|
124
|
+
for (const result of results) {
|
|
125
|
+
for (const cell of result.cells) {
|
|
126
|
+
reachable[cell.y][cell.x] = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return reachable;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const getWordCells = (board: Board, word: BoardWord): boolean[][] => {
|
|
134
|
+
const wordCells = createGrid(board.columnsCount, board.rowsCount, () => false);
|
|
135
|
+
const isHorizontal = word.direction === 'horizontal';
|
|
136
|
+
let { x, y } = word;
|
|
137
|
+
|
|
138
|
+
while (x < board.columnsCount && y < board.rowsCount && board.rows[y][x].hasTile()) {
|
|
139
|
+
wordCells[y][x] = true;
|
|
140
|
+
|
|
141
|
+
if (isHorizontal) {
|
|
142
|
+
x += 1;
|
|
143
|
+
} else {
|
|
144
|
+
y += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return wordCells;
|
|
149
|
+
};
|
|
@@ -4,24 +4,36 @@ import { findCell } from '@/lib/findCell';
|
|
|
4
4
|
import {
|
|
5
5
|
selectBoard,
|
|
6
6
|
selectCharacters,
|
|
7
|
-
selectConfig,
|
|
8
7
|
selectHighlightUnreachableCells,
|
|
9
8
|
selectResultCandidateCells,
|
|
9
|
+
selectUpToDateHoveredWord,
|
|
10
|
+
selectUpToDateResults,
|
|
10
11
|
} from '@/state';
|
|
11
12
|
|
|
12
|
-
import { getReachableCells } from './lib';
|
|
13
|
+
import { getReachableCells, getReachableCellsFromResults, getWordCells } from './lib';
|
|
13
14
|
|
|
14
15
|
export const selectRowsWithCandidate = createSelector([selectBoard, selectResultCandidateCells], (board, cells) => {
|
|
15
16
|
return board.rows.map((row, y) => row.map((cell, x) => findCell(cells, x, y) || cell));
|
|
16
17
|
});
|
|
17
18
|
|
|
19
|
+
export const selectHoveredWordCells = createSelector(
|
|
20
|
+
[selectBoard, selectUpToDateHoveredWord],
|
|
21
|
+
(board, hoveredWord): boolean[][] | null => {
|
|
22
|
+
return hoveredWord ? getWordCells(board, hoveredWord) : null;
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
18
26
|
export const selectReachableCells = createSelector(
|
|
19
|
-
[
|
|
20
|
-
(
|
|
27
|
+
[selectBoard, selectCharacters, selectHighlightUnreachableCells, selectUpToDateResults],
|
|
28
|
+
(board, characters, highlightUnreachableCells, upToDateResults): boolean[][] | null => {
|
|
21
29
|
if (!highlightUnreachableCells || characters.length === 0) {
|
|
22
30
|
return null;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
if (upToDateResults) {
|
|
34
|
+
return getReachableCellsFromResults(board, upToDateResults);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return getReachableCells(board, characters.length);
|
|
26
38
|
},
|
|
27
39
|
);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type FunctionComponent } from 'react';
|
|
1
|
+
import { type FunctionComponent, useCallback } from 'react';
|
|
2
|
+
import { useDispatch } from 'react-redux';
|
|
2
3
|
|
|
4
|
+
import { Header as TableHeader, HeaderButton } from '@/components/Table';
|
|
3
5
|
import { useColumns } from '@/hooks/useColumns';
|
|
4
6
|
import GeoAlt from '@/icons/GeoAlt.svg';
|
|
5
7
|
import OneTwoThree from '@/icons/OneTwoThree.svg';
|
|
@@ -8,27 +10,38 @@ import SquareA from '@/icons/SquareA.svg';
|
|
|
8
10
|
import SquareB from '@/icons/SquareB.svg';
|
|
9
11
|
import Squares from '@/icons/Squares.svg';
|
|
10
12
|
import Words from '@/icons/Words.svg';
|
|
13
|
+
import { resultsSlice, selectResultsSort, useTypedSelector } from '@/state';
|
|
11
14
|
import { ResultColumnId } from '@/types';
|
|
12
15
|
|
|
13
|
-
import { HeaderButton } from './HeaderButton';
|
|
14
16
|
import styles from './Results.module.scss';
|
|
15
17
|
|
|
16
18
|
export const Header: FunctionComponent = () => {
|
|
19
|
+
const dispatch = useDispatch();
|
|
17
20
|
const columns = useColumns();
|
|
21
|
+
const sort = useTypedSelector(selectResultsSort);
|
|
22
|
+
|
|
23
|
+
const handleSort = useCallback(
|
|
24
|
+
(columnId: ResultColumnId) => {
|
|
25
|
+
dispatch(resultsSlice.actions.sort(columnId));
|
|
26
|
+
},
|
|
27
|
+
[dispatch],
|
|
28
|
+
);
|
|
18
29
|
|
|
19
30
|
return (
|
|
20
|
-
<
|
|
31
|
+
<TableHeader>
|
|
21
32
|
{columns[ResultColumnId.Coordinates] && (
|
|
22
33
|
<HeaderButton
|
|
23
34
|
className={styles.coordinates}
|
|
24
35
|
Icon={GeoAlt}
|
|
25
36
|
id={ResultColumnId.Coordinates}
|
|
37
|
+
sort={sort}
|
|
26
38
|
translationKey="settings.showCoordinates"
|
|
39
|
+
onSort={handleSort}
|
|
27
40
|
/>
|
|
28
41
|
)}
|
|
29
42
|
|
|
30
43
|
{columns[ResultColumnId.Word] && (
|
|
31
|
-
<HeaderButton
|
|
44
|
+
<HeaderButton id={ResultColumnId.Word} primary sort={sort} translationKey="common.word" onSort={handleSort} />
|
|
32
45
|
)}
|
|
33
46
|
|
|
34
47
|
{columns[ResultColumnId.TilesCount] && (
|
|
@@ -36,7 +49,9 @@ export const Header: FunctionComponent = () => {
|
|
|
36
49
|
className={styles.stat}
|
|
37
50
|
Icon={Squares}
|
|
38
51
|
id={ResultColumnId.TilesCount}
|
|
52
|
+
sort={sort}
|
|
39
53
|
translationKey="common.tiles"
|
|
54
|
+
onSort={handleSort}
|
|
40
55
|
/>
|
|
41
56
|
)}
|
|
42
57
|
|
|
@@ -45,7 +60,9 @@ export const Header: FunctionComponent = () => {
|
|
|
45
60
|
className={styles.stat}
|
|
46
61
|
Icon={SquareA}
|
|
47
62
|
id={ResultColumnId.VowelsCount}
|
|
63
|
+
sort={sort}
|
|
48
64
|
translationKey="common.vowels"
|
|
65
|
+
onSort={handleSort}
|
|
49
66
|
/>
|
|
50
67
|
)}
|
|
51
68
|
|
|
@@ -54,7 +71,9 @@ export const Header: FunctionComponent = () => {
|
|
|
54
71
|
className={styles.stat}
|
|
55
72
|
Icon={SquareB}
|
|
56
73
|
id={ResultColumnId.ConsonantsCount}
|
|
74
|
+
sort={sort}
|
|
57
75
|
translationKey="common.consonants"
|
|
76
|
+
onSort={handleSort}
|
|
58
77
|
/>
|
|
59
78
|
)}
|
|
60
79
|
|
|
@@ -63,7 +82,9 @@ export const Header: FunctionComponent = () => {
|
|
|
63
82
|
className={styles.stat}
|
|
64
83
|
Icon={Square}
|
|
65
84
|
id={ResultColumnId.BlanksCount}
|
|
85
|
+
sort={sort}
|
|
66
86
|
translationKey="common.blanks"
|
|
87
|
+
onSort={handleSort}
|
|
67
88
|
/>
|
|
68
89
|
)}
|
|
69
90
|
|
|
@@ -72,7 +93,9 @@ export const Header: FunctionComponent = () => {
|
|
|
72
93
|
className={styles.stat}
|
|
73
94
|
Icon={Words}
|
|
74
95
|
id={ResultColumnId.WordsCount}
|
|
96
|
+
sort={sort}
|
|
75
97
|
translationKey="common.words"
|
|
98
|
+
onSort={handleSort}
|
|
76
99
|
/>
|
|
77
100
|
)}
|
|
78
101
|
|
|
@@ -81,9 +104,11 @@ export const Header: FunctionComponent = () => {
|
|
|
81
104
|
className={styles.points}
|
|
82
105
|
Icon={OneTwoThree}
|
|
83
106
|
id={ResultColumnId.Points}
|
|
107
|
+
sort={sort}
|
|
84
108
|
translationKey="common.points"
|
|
109
|
+
onSort={handleSort}
|
|
85
110
|
/>
|
|
86
111
|
)}
|
|
87
|
-
</
|
|
112
|
+
</TableHeader>
|
|
88
113
|
);
|
|
89
114
|
};
|