@scrabble-solver/scrabble-solver 2.8.7 → 2.8.9
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 +10 -10
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/eslint/.cache_8dgz12 +1 -1
- package/.next/cache/next-server.js.nft.json +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/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/chunks/413.js +257 -55
- package/.next/server/chunks/44.js +802 -0
- package/.next/server/chunks/515.js +452 -104
- package/.next/server/chunks/911.js +53 -23
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/404.js.nft.json +1 -1
- package/.next/server/pages/500.html +2 -2
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/_document.js.nft.json +1 -1
- package/.next/server/pages/_error.js.nft.json +1 -1
- package/.next/server/pages/api/solve.js +193 -927
- package/.next/server/pages/api/solve.js.nft.json +1 -1
- package/.next/server/pages/api/verify.js +217 -0
- package/.next/server/pages/api/verify.js.nft.json +1 -0
- package/.next/server/pages/index.html +3 -3
- package/.next/server/pages/index.js +7 -1
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/server/pages-manifest.json +1 -0
- package/.next/static/chunks/317-a33dd38e9b9a17ed.js +1 -0
- package/.next/static/chunks/pages/{404-30c06e61d256c5b2.js → 404-90c624da3c83fd17.js} +1 -1
- package/.next/static/chunks/pages/_app-f8f360878e1c2aff.js +1 -0
- package/.next/static/chunks/pages/index-ecea697d3e5d8a6f.js +1 -0
- package/.next/static/css/64dc2ce1811912f1.css +1 -0
- package/.next/static/css/{cdbc9e0afcff5473.css → ad2a08918868cad8.css} +1 -1
- package/.next/static/yCxjzzYpw5JjJE53PO_s6/_buildManifest.js +1 -0
- package/.next/static/{z_0_lqfmiI_ISokr6NNRq → yCxjzzYpw5JjJE53PO_s6}/_ssgManifest.js +0 -0
- package/.next/trace +42 -40
- package/package.json +9 -9
- package/src/components/Badge/Badge.module.scss +13 -0
- package/src/components/Badge/Badge.tsx +15 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/Board/components/Cell/Cell.module.scss +34 -9
- package/src/components/Board/components/Cell/Cell.tsx +23 -4
- package/src/components/Board/components/Cell/CellPure.tsx +29 -1
- package/src/components/Board/hooks/useGrid.ts +1 -0
- package/src/components/NavButtons/NavButtons.tsx +33 -14
- package/src/components/RemainingTiles/RemainingTiles.module.scss +8 -7
- package/src/components/RemainingTiles/RemainingTiles.tsx +13 -4
- package/src/components/Sidebar/Sidebar.tsx +2 -2
- package/src/components/Sidebar/components/Section/Section.module.scss +0 -1
- package/src/components/Sidebar/components/Section/Section.tsx +1 -1
- package/src/components/SquareButton/SquareButton.module.scss +5 -0
- package/src/components/Words/Words.module.scss +35 -0
- package/src/components/Words/Words.tsx +57 -0
- package/src/components/Words/index.ts +1 -0
- package/src/components/index.ts +2 -0
- package/src/i18n/de.json +5 -1
- package/src/i18n/en.json +5 -1
- package/src/i18n/es.json +5 -1
- package/src/i18n/fr.json +5 -1
- package/src/i18n/pl.json +5 -1
- package/src/icons/BookHalf.svg +4 -0
- package/src/icons/Check.svg +4 -0
- package/src/icons/Cross.svg +2 -2
- package/src/icons/CrossFill.svg +4 -0
- package/src/icons/Flag.svg +4 -0
- package/src/icons/Star.svg +4 -0
- package/src/icons/index.ts +5 -0
- package/src/pages/api/solve.ts +2 -3
- package/src/pages/api/verify.ts +71 -0
- package/src/pages/index.tsx +5 -0
- package/src/sdk/index.ts +1 -0
- package/src/sdk/verify.ts +24 -0
- package/src/state/rootReducer.ts +12 -1
- package/src/state/sagas.ts +54 -7
- package/src/state/selectors.ts +49 -11
- package/src/state/slices/cellFilterInitialState.ts +7 -0
- package/src/state/slices/cellFilterSlice.ts +24 -0
- package/src/state/slices/index.ts +4 -0
- package/src/state/slices/verifyInitialState.ts +12 -0
- package/src/state/slices/verifySlice.ts +31 -0
- package/src/styles/variables.scss +2 -1
- package/src/types/index.ts +5 -1
- package/.next/static/chunks/56-cf37c430261bbea5.js +0 -1
- package/.next/static/chunks/pages/_app-0b12a65bea70a0df.js +0 -1
- package/.next/static/chunks/pages/index-fcb69802550afb81.js +0 -1
- package/.next/static/css/1f39b55d50f5b30b.css +0 -1
- package/.next/static/z_0_lqfmiI_ISokr6NNRq/_buildManifest.js +0 -1
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/book-half/ -->
|
|
2
|
+
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M8.5 2.687c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z" fill="currentColor" />
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/check/ -->
|
|
2
|
+
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z" fill="currentColor" />
|
|
4
|
+
</svg>
|
package/src/icons/Cross.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- https://icons.getbootstrap.com/icons/x
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/x/ -->
|
|
2
2
|
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
-
<path d="
|
|
3
|
+
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" fill="currentColor" />
|
|
4
4
|
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/x-square-fill/ -->
|
|
2
|
+
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm3.354 4.646L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 1 1 .708-.708z" fill="currentColor" />
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/flag-fill/ -->
|
|
2
|
+
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M14.778.085A.5.5 0 0 1 15 .5V8a.5.5 0 0 1-.314.464L14.5 8l.186.464-.003.001-.006.003-.023.009a12.435 12.435 0 0 1-.397.15c-.264.095-.631.223-1.047.35-.816.252-1.879.523-2.71.523-.847 0-1.548-.28-2.158-.525l-.028-.01C7.68 8.71 7.14 8.5 6.5 8.5c-.7 0-1.638.23-2.437.477A19.626 19.626 0 0 0 3 9.342V15.5a.5.5 0 0 1-1 0V.5a.5.5 0 0 1 1 0v.282c.226-.079.496-.17.79-.26C4.606.272 5.67 0 6.5 0c.84 0 1.524.277 2.121.519l.043.018C9.286.788 9.828 1 10.5 1c.7 0 1.638-.23 2.437-.477a19.587 19.587 0 0 0 1.349-.476l.019-.007.004-.002h.001" fill="currentColor" />
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/star-fill/ -->
|
|
2
|
+
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z" fill="currentColor" />
|
|
4
|
+
</svg>
|
package/src/icons/index.ts
CHANGED
|
@@ -2,12 +2,16 @@ export { default as ArrowDown } from './ArrowDown.svg';
|
|
|
2
2
|
export { default as ArrowLeft } from './ArrowLeft.svg';
|
|
3
3
|
export { default as ArrowRight } from './ArrowRight.svg';
|
|
4
4
|
export { default as ArrowUp } from './ArrowUp.svg';
|
|
5
|
+
export { default as BookHalf } from './BookHalf.svg';
|
|
6
|
+
export { default as Check } from './Check.svg';
|
|
5
7
|
export { default as CheckboxChecked } from './CheckboxChecked.svg';
|
|
6
8
|
export { default as CheckboxEmpty } from './CheckboxEmpty.svg';
|
|
7
9
|
export { default as Cog } from './Cog.svg';
|
|
8
10
|
export { default as Cross } from './Cross.svg';
|
|
11
|
+
export { default as CrossFill } from './CrossFill.svg';
|
|
9
12
|
export { default as DashCircleFill } from './DashCircleFill.svg';
|
|
10
13
|
export { default as Eraser } from './Eraser.svg';
|
|
14
|
+
export { default as Flag } from './Flag.svg';
|
|
11
15
|
export { default as FlagEs } from './FlagEs.svg';
|
|
12
16
|
export { default as FlagFr } from './FlagFr.svg';
|
|
13
17
|
export { default as FlagGb } from './FlagGb.svg';
|
|
@@ -20,3 +24,4 @@ export { default as Play } from './Play.svg';
|
|
|
20
24
|
export { default as Sack } from './Sack.svg';
|
|
21
25
|
export { default as SortDown } from './SortDown.svg';
|
|
22
26
|
export { default as SortUp } from './SortUp.svg';
|
|
27
|
+
export { default as Star } from './Star.svg';
|
package/src/pages/api/solve.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { getLocaleConfig, isConfigId } from '@scrabble-solver/configs';
|
|
|
2
2
|
import { BLANK } from '@scrabble-solver/constants';
|
|
3
3
|
import { dictionaries } from '@scrabble-solver/dictionaries';
|
|
4
4
|
import logger from '@scrabble-solver/logger';
|
|
5
|
-
import
|
|
5
|
+
import { solve as solveScrabble } from '@scrabble-solver/solver';
|
|
6
6
|
import { Board, Config, isBoardJson, isLocale, Locale, Tile } from '@scrabble-solver/types';
|
|
7
7
|
import { NextApiRequest, NextApiResponse } from 'next';
|
|
8
8
|
|
|
@@ -36,8 +36,7 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
|
|
|
36
36
|
|
|
37
37
|
const trie = await dictionaries.get(locale);
|
|
38
38
|
const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
|
|
39
|
-
const
|
|
40
|
-
const results = solver.solve(board, tiles);
|
|
39
|
+
const results = solveScrabble(trie, config, board, tiles);
|
|
41
40
|
response.status(200).send(results.map((result) => result.toJson()));
|
|
42
41
|
} catch (error) {
|
|
43
42
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getLocaleConfig, isConfigId } from '@scrabble-solver/configs';
|
|
2
|
+
import { dictionaries } from '@scrabble-solver/dictionaries';
|
|
3
|
+
import logger from '@scrabble-solver/logger';
|
|
4
|
+
import { Board, Config, isBoardJson, isLocale, Locale } from '@scrabble-solver/types';
|
|
5
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
6
|
+
|
|
7
|
+
import { getServerLoggingData, isBoardValid } from 'api';
|
|
8
|
+
|
|
9
|
+
interface RequestData {
|
|
10
|
+
board: Board;
|
|
11
|
+
config: Config;
|
|
12
|
+
locale: Locale;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const verify = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
|
|
16
|
+
const meta = getServerLoggingData(request);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const { board, locale } = parseRequest(request);
|
|
20
|
+
|
|
21
|
+
logger.info('verify - request', {
|
|
22
|
+
meta,
|
|
23
|
+
payload: {
|
|
24
|
+
board: board.toString(),
|
|
25
|
+
boardBlanksCount: board.getBlanksCount(),
|
|
26
|
+
boardTilesCount: board.getTilesCount(),
|
|
27
|
+
configId: request.body.configId,
|
|
28
|
+
locale,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const trie = await dictionaries.get(locale);
|
|
33
|
+
const words = board.getWords().sort((a, b) => a.localeCompare(b));
|
|
34
|
+
const invalidWords = words.filter((word) => !trie.has(word));
|
|
35
|
+
const validWords = words.filter((word) => trie.has(word));
|
|
36
|
+
response.status(200).send({ invalidWords, validWords });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
39
|
+
logger.error('verify - error', { error, meta });
|
|
40
|
+
response.status(500).send({ error: 'Server error', message });
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const parseRequest = (request: NextApiRequest): RequestData => {
|
|
46
|
+
const { board: boardJson, configId, locale } = request.body;
|
|
47
|
+
|
|
48
|
+
if (!isLocale(locale)) {
|
|
49
|
+
throw new Error('Invalid "locale" parameter');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!isConfigId(configId)) {
|
|
53
|
+
throw new Error('Invalid "configId" parameter');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const config = getLocaleConfig(configId, locale);
|
|
57
|
+
|
|
58
|
+
if (!isBoardJson(boardJson) || !isBoardValid(boardJson, config)) {
|
|
59
|
+
throw new Error('Invalid "board" parameter');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const board = Board.fromJson(boardJson);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
board,
|
|
66
|
+
config,
|
|
67
|
+
locale,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default verify;
|
package/src/pages/index.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
Settings,
|
|
20
20
|
Splash,
|
|
21
21
|
Well,
|
|
22
|
+
Words,
|
|
22
23
|
} from 'components';
|
|
23
24
|
import { useIsTablet, useLocalStorage } from 'hooks';
|
|
24
25
|
import { getCellSize } from 'lib';
|
|
@@ -39,6 +40,7 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
39
40
|
const [showKeyMap, setShowKeyMap] = useState(false);
|
|
40
41
|
const [showRemainingTiles, setShowRemainingTiles] = useState(false);
|
|
41
42
|
const [showSettings, setShowSettings] = useState(false);
|
|
43
|
+
const [showWords, setShowWords] = useState(false);
|
|
42
44
|
const [boardRef, { height: boardHeight }] = useMeasure<HTMLDivElement>();
|
|
43
45
|
const [contentRef, { height: contentHeight, width: contentWidth }] = useMeasure<HTMLDivElement>();
|
|
44
46
|
const [resultsContainerRef, { height: resultsContainerHeight, width: resultsContainerWidth }] =
|
|
@@ -85,6 +87,7 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
85
87
|
onShowKeyMap={() => setShowKeyMap(true)}
|
|
86
88
|
onShowRemainingTiles={() => setShowRemainingTiles(true)}
|
|
87
89
|
onShowSettings={() => setShowSettings(true)}
|
|
90
|
+
onShowWords={() => setShowWords(true)}
|
|
88
91
|
/>
|
|
89
92
|
</div>
|
|
90
93
|
|
|
@@ -122,6 +125,8 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
122
125
|
|
|
123
126
|
<KeyMap isOpen={showKeyMap} onClose={() => setShowKeyMap(false)} />
|
|
124
127
|
|
|
128
|
+
<Words isOpen={showWords} onClose={() => setShowWords(false)} />
|
|
129
|
+
|
|
125
130
|
<RemainingTiles isOpen={showRemainingTiles} onClose={() => setShowRemainingTiles(false)} />
|
|
126
131
|
|
|
127
132
|
<Splash forceShow={!isInitialized} onAnimationEnd={handleSplashAnimationEnd} />
|
package/src/sdk/index.ts
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BoardJson, Locale } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
interface Payload {
|
|
4
|
+
board: BoardJson;
|
|
5
|
+
configId: string;
|
|
6
|
+
locale: Locale;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Response {
|
|
10
|
+
invalidWords: string[];
|
|
11
|
+
validWords: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const verify = ({ board, configId, locale }: Payload): Promise<Response> => {
|
|
15
|
+
return fetch('/api/verify', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({ board, configId, locale }),
|
|
21
|
+
}).then((response) => response.json());
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default verify;
|
package/src/state/rootReducer.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { combineReducers } from 'redux';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
boardSlice,
|
|
5
|
+
dictionarySlice,
|
|
6
|
+
cellFilterSlice,
|
|
7
|
+
rackSlice,
|
|
8
|
+
resultsSlice,
|
|
9
|
+
settingsSlice,
|
|
10
|
+
solveSlice,
|
|
11
|
+
verifySlice,
|
|
12
|
+
} from './slices';
|
|
4
13
|
|
|
5
14
|
const rootReducer = combineReducers({
|
|
6
15
|
board: boardSlice.reducer,
|
|
16
|
+
cellFilter: cellFilterSlice.reducer,
|
|
7
17
|
dictionary: dictionarySlice.reducer,
|
|
8
18
|
rack: rackSlice.reducer,
|
|
9
19
|
results: resultsSlice.reducer,
|
|
10
20
|
settings: settingsSlice.reducer,
|
|
11
21
|
solve: solveSlice.reducer,
|
|
22
|
+
verify: verifySlice.reducer,
|
|
12
23
|
});
|
|
13
24
|
|
|
14
25
|
export default rootReducer;
|
package/src/state/sagas.ts
CHANGED
|
@@ -3,18 +3,28 @@ import { Result } from '@scrabble-solver/types';
|
|
|
3
3
|
import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
|
|
4
4
|
|
|
5
5
|
import { memoize } from 'lib';
|
|
6
|
-
import { findWordDefinitions, solve, visit } from 'sdk';
|
|
6
|
+
import { findWordDefinitions, solve, verify, visit } from 'sdk';
|
|
7
7
|
|
|
8
8
|
import { initialize, reset } from './actions';
|
|
9
9
|
import {
|
|
10
10
|
selectAutoGroupTiles,
|
|
11
11
|
selectBoard,
|
|
12
|
+
selectCellIsFiltered,
|
|
12
13
|
selectCharacters,
|
|
13
14
|
selectConfig,
|
|
14
15
|
selectDictionary,
|
|
15
16
|
selectLocale,
|
|
16
17
|
} from './selectors';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
boardSlice,
|
|
20
|
+
cellFilterSlice,
|
|
21
|
+
dictionarySlice,
|
|
22
|
+
rackSlice,
|
|
23
|
+
resultsSlice,
|
|
24
|
+
settingsSlice,
|
|
25
|
+
solveSlice,
|
|
26
|
+
verifySlice,
|
|
27
|
+
} from './slices';
|
|
18
28
|
|
|
19
29
|
const SUBMIT_DELAY = 150;
|
|
20
30
|
|
|
@@ -26,6 +36,7 @@ const memoizedFindWordDefinitions = memoize(findWordDefinitions);
|
|
|
26
36
|
type AnyGenerator = Generator<any, any, any>;
|
|
27
37
|
|
|
28
38
|
export function* rootSaga(): AnyGenerator {
|
|
39
|
+
yield takeEvery(boardSlice.actions.changeCellValue.type, onCellValueChange);
|
|
29
40
|
yield takeEvery(resultsSlice.actions.applyResult.type, onApplyResult);
|
|
30
41
|
yield takeEvery(resultsSlice.actions.changeResultCandidate.type, onResultCandidateChange);
|
|
31
42
|
yield takeEvery(settingsSlice.actions.changeConfigId.type, onConfigIdChange);
|
|
@@ -33,19 +44,33 @@ export function* rootSaga(): AnyGenerator {
|
|
|
33
44
|
yield takeLatest(dictionarySlice.actions.submit.type, onDictionarySubmit);
|
|
34
45
|
yield takeLatest(initialize.type, onInitialize);
|
|
35
46
|
yield takeLatest(reset.type, onReset);
|
|
36
|
-
yield takeLatest(solveSlice.actions.submit.type,
|
|
47
|
+
yield takeLatest(solveSlice.actions.submit.type, onSolve);
|
|
48
|
+
yield takeLatest(verifySlice.actions.submit.type, onVerify);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function* onCellValueChange({ payload }: PayloadAction<{ value: string; x: number; y: number }>): AnyGenerator {
|
|
52
|
+
const isFiltered = yield select((state) => selectCellIsFiltered(state, payload));
|
|
53
|
+
|
|
54
|
+
if (isFiltered) {
|
|
55
|
+
yield put(cellFilterSlice.actions.toggle(payload));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
yield put(verifySlice.actions.submit());
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
function* onApplyResult({ payload: result }: PayloadAction<Result>): AnyGenerator {
|
|
40
62
|
const autoGroupTiles = yield select(selectAutoGroupTiles);
|
|
41
63
|
yield put(boardSlice.actions.applyResult(result));
|
|
64
|
+
yield put(cellFilterSlice.actions.reset());
|
|
42
65
|
yield put(rackSlice.actions.removeTiles(result.tiles));
|
|
43
66
|
yield put(rackSlice.actions.groupTiles(autoGroupTiles));
|
|
67
|
+
yield put(verifySlice.actions.submit());
|
|
44
68
|
}
|
|
45
69
|
|
|
46
70
|
function* onConfigIdChange(): AnyGenerator {
|
|
47
71
|
yield put(resultsSlice.actions.reset());
|
|
48
72
|
yield put(solveSlice.actions.submit());
|
|
73
|
+
yield put(verifySlice.actions.submit());
|
|
49
74
|
yield* ensureProperTilesCount();
|
|
50
75
|
}
|
|
51
76
|
|
|
@@ -72,15 +97,18 @@ function* onInitialize(): AnyGenerator {
|
|
|
72
97
|
|
|
73
98
|
function* onReset(): AnyGenerator {
|
|
74
99
|
yield put(boardSlice.actions.reset());
|
|
100
|
+
yield put(cellFilterSlice.actions.reset());
|
|
75
101
|
yield put(dictionarySlice.actions.reset());
|
|
76
102
|
yield put(rackSlice.actions.reset());
|
|
77
103
|
yield put(resultsSlice.actions.reset());
|
|
104
|
+
yield put(verifySlice.actions.submit());
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
function* onLocaleChange(): AnyGenerator {
|
|
81
|
-
yield put(solveSlice.actions.submit());
|
|
82
|
-
yield put(resultsSlice.actions.changeResultCandidate(null));
|
|
83
108
|
yield put(dictionarySlice.actions.reset());
|
|
109
|
+
yield put(resultsSlice.actions.changeResultCandidate(null));
|
|
110
|
+
yield put(solveSlice.actions.submit());
|
|
111
|
+
yield put(verifySlice.actions.submit());
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
function* onResultCandidateChange({ payload: result }: PayloadAction<Result | null>): AnyGenerator {
|
|
@@ -90,7 +118,7 @@ function* onResultCandidateChange({ payload: result }: PayloadAction<Result | nu
|
|
|
90
118
|
}
|
|
91
119
|
}
|
|
92
120
|
|
|
93
|
-
function*
|
|
121
|
+
function* onSolve(): AnyGenerator {
|
|
94
122
|
const board = yield select(selectBoard);
|
|
95
123
|
const { config } = yield select(selectConfig);
|
|
96
124
|
const locale = yield select(selectLocale);
|
|
@@ -109,14 +137,33 @@ function* onSubmit(): AnyGenerator {
|
|
|
109
137
|
configId: config.id,
|
|
110
138
|
locale,
|
|
111
139
|
});
|
|
112
|
-
yield put(solveSlice.actions.submitSuccess({ board, characters }));
|
|
113
140
|
yield put(resultsSlice.actions.changeResults(results.map(Result.fromJson)));
|
|
141
|
+
yield put(solveSlice.actions.submitSuccess({ board, characters }));
|
|
114
142
|
} catch (error) {
|
|
115
143
|
yield put(resultsSlice.actions.changeResults([]));
|
|
116
144
|
yield put(solveSlice.actions.submitFailure());
|
|
117
145
|
}
|
|
118
146
|
}
|
|
119
147
|
|
|
148
|
+
function* onVerify(): AnyGenerator {
|
|
149
|
+
yield delay(SUBMIT_DELAY);
|
|
150
|
+
|
|
151
|
+
const board = yield select(selectBoard);
|
|
152
|
+
const { config } = yield select(selectConfig);
|
|
153
|
+
const locale = yield select(selectLocale);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const { invalidWords, validWords } = yield call(verify, {
|
|
157
|
+
board: board.toJson(),
|
|
158
|
+
configId: config.id,
|
|
159
|
+
locale,
|
|
160
|
+
});
|
|
161
|
+
yield put(verifySlice.actions.submitSuccess({ board, invalidWords, validWords }));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
yield put(verifySlice.actions.submitFailure());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
120
167
|
function* ensureProperTilesCount(): AnyGenerator {
|
|
121
168
|
const { config } = yield select(selectConfig);
|
|
122
169
|
const characters = yield select(selectCharacters);
|
package/src/state/selectors.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSelector } from '@reduxjs/toolkit';
|
|
2
2
|
import { getLocaleConfig } from '@scrabble-solver/configs';
|
|
3
|
-
import { Cell, Config, Tile } from '@scrabble-solver/types';
|
|
3
|
+
import { Cell, Config, Result, Tile } from '@scrabble-solver/types';
|
|
4
4
|
|
|
5
5
|
import i18n from 'i18n';
|
|
6
6
|
import { findCell, getRemainingTiles, getRemainingTilesGroups, sortResults, unorderedArraysEqual } from 'lib';
|
|
@@ -10,6 +10,8 @@ import { RootState } from './types';
|
|
|
10
10
|
|
|
11
11
|
const selectCell = (_: unknown, cell: Cell): Cell => cell;
|
|
12
12
|
|
|
13
|
+
const selectPoint = (_: unknown, point: { x: number; y: number }): { x: number; y: number } => point;
|
|
14
|
+
|
|
13
15
|
const selectCharacter = (_: unknown, character: string | null): string | null => character;
|
|
14
16
|
|
|
15
17
|
const selectTile = (_: unknown, tile: Tile | null): Tile | null => tile;
|
|
@@ -18,6 +20,8 @@ const selectBoardRoot = (state: RootState): RootState['board'] => state.board;
|
|
|
18
20
|
|
|
19
21
|
const selectDictionaryRoot = (state: RootState): RootState['dictionary'] => state.dictionary;
|
|
20
22
|
|
|
23
|
+
const selectCellFilterRoot = (state: RootState): RootState['cellFilter'] => state.cellFilter;
|
|
24
|
+
|
|
21
25
|
const selectRackRoot = (state: RootState): RootState['rack'] => state.rack;
|
|
22
26
|
|
|
23
27
|
const selectResultsRoot = (state: RootState): RootState['results'] => state.results;
|
|
@@ -26,6 +30,8 @@ const selectSettingsRoot = (state: RootState): RootState['settings'] => state.se
|
|
|
26
30
|
|
|
27
31
|
const selectSolveRoot = (state: RootState): RootState['solve'] => state.solve;
|
|
28
32
|
|
|
33
|
+
const selectVerifyRoot = (state: RootState): RootState['verify'] => state.verify;
|
|
34
|
+
|
|
29
35
|
export const selectDictionary = selectDictionaryRoot;
|
|
30
36
|
|
|
31
37
|
export const selectAutoGroupTiles = createSelector([selectSettingsRoot], (settings) => settings.autoGroupTiles);
|
|
@@ -38,6 +44,12 @@ export const selectConfigId = createSelector([selectSettingsRoot], (settings) =>
|
|
|
38
44
|
|
|
39
45
|
export const selectConfig = createSelector([selectConfigId, selectLocale], getLocaleConfig);
|
|
40
46
|
|
|
47
|
+
export const selectCellFilter = selectCellFilterRoot;
|
|
48
|
+
|
|
49
|
+
export const selectCellIsFiltered = createSelector([selectCellFilter, selectPoint], (cellFilter, { x, y }) => {
|
|
50
|
+
return cellFilter.some((cell) => cell.x === x && cell.y === y);
|
|
51
|
+
});
|
|
52
|
+
|
|
41
53
|
export const selectResults = createSelector([selectResultsRoot], (results) => results.results);
|
|
42
54
|
|
|
43
55
|
export const selectResultsQuery = createSelector([selectResultsRoot], (results) => results.query);
|
|
@@ -51,20 +63,40 @@ export const selectSortedResults = createSelector(
|
|
|
51
63
|
sortResults,
|
|
52
64
|
);
|
|
53
65
|
|
|
66
|
+
const filterResultsByQuery = (results: Result[], query: string): Result[] => {
|
|
67
|
+
if (query.trim().length === 0) {
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let regExp: RegExp | undefined;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
regExp = new RegExp(query, 'gi');
|
|
75
|
+
} catch {
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return results.filter((result) => {
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
81
|
+
return regExp!.test(result.word);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
54
85
|
export const selectSortedFilteredResults = createSelector(
|
|
55
|
-
[selectSortedResults, selectResultsQuery],
|
|
56
|
-
(results, query) => {
|
|
57
|
-
if (!results
|
|
86
|
+
[selectSortedResults, selectResultsQuery, selectCellFilter],
|
|
87
|
+
(results, query, cellFilter) => {
|
|
88
|
+
if (!results) {
|
|
58
89
|
return results;
|
|
59
90
|
}
|
|
60
91
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
const filteredByQuery = filterResultsByQuery(results, query);
|
|
93
|
+
|
|
94
|
+
if (!cellFilter) {
|
|
95
|
+
return filteredByQuery;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return filteredByQuery.filter((result) => {
|
|
99
|
+
return cellFilter.every(({ x, y }) => result.cells.some((cell) => cell.x === x && cell.y === y));
|
|
68
100
|
});
|
|
69
101
|
},
|
|
70
102
|
);
|
|
@@ -153,3 +185,9 @@ export const selectHasOverusedTiles = createSelector([selectRemainingTiles], (re
|
|
|
153
185
|
});
|
|
154
186
|
|
|
155
187
|
export const selectRemainingTilesGroups = createSelector([selectRemainingTiles], getRemainingTilesGroups);
|
|
188
|
+
|
|
189
|
+
export const selectVerify = selectVerifyRoot;
|
|
190
|
+
|
|
191
|
+
export const selectHasInvalidWords = createSelector([selectVerify], ({ invalidWords }) => {
|
|
192
|
+
return invalidWords.length > 0;
|
|
193
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
2
|
+
|
|
3
|
+
import cellFilterInitialState, { Point } from './cellFilterInitialState';
|
|
4
|
+
|
|
5
|
+
const cellFilterSlice = createSlice({
|
|
6
|
+
initialState: cellFilterInitialState,
|
|
7
|
+
name: 'cellFilter',
|
|
8
|
+
reducers: {
|
|
9
|
+
toggle: (state, action: PayloadAction<Point>) => {
|
|
10
|
+
const { x, y } = action.payload;
|
|
11
|
+
const has = state.some((point) => point.x === x && point.y === y);
|
|
12
|
+
|
|
13
|
+
if (has) {
|
|
14
|
+
return state.filter((point) => point.x !== x || point.y !== y);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return [...state, action.payload];
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
reset: () => cellFilterInitialState,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default cellFilterSlice;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { default as boardInitialState } from './boardInitialState';
|
|
2
2
|
export { default as boardSlice } from './boardSlice';
|
|
3
|
+
export { default as cellFilterInitialState } from './cellFilterInitialState';
|
|
4
|
+
export { default as cellFilterSlice } from './cellFilterSlice';
|
|
3
5
|
export { default as dictionaryInitialState } from './dictionaryInitialState';
|
|
4
6
|
export { default as dictionarySlice } from './dictionarySlice';
|
|
5
7
|
export { default as rackInitialState } from './rackInitialState';
|
|
@@ -10,3 +12,5 @@ export { default as settingsInitialState } from './settingsInitialState';
|
|
|
10
12
|
export { default as settingsSlice } from './settingsSlice';
|
|
11
13
|
export { default as solveInitialState } from './solveInitialState';
|
|
12
14
|
export { default as solveSlice } from './solveSlice';
|
|
15
|
+
export { default as verifyInitialState } from './verifyInitialState';
|
|
16
|
+
export { default as verifySlice } from './verifySlice';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import boardInitialState from './boardInitialState';
|
|
2
|
+
|
|
3
|
+
const verifyInitialState = {
|
|
4
|
+
isLoading: false,
|
|
5
|
+
lastSolvedParameters: {
|
|
6
|
+
board: boardInitialState,
|
|
7
|
+
},
|
|
8
|
+
invalidWords: [] as string[],
|
|
9
|
+
validWords: [] as string[],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default verifyInitialState;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
2
|
+
import { Board } from '@scrabble-solver/types';
|
|
3
|
+
|
|
4
|
+
import verifyInitialState from './verifyInitialState';
|
|
5
|
+
|
|
6
|
+
interface VerifyParameters {
|
|
7
|
+
board: Board;
|
|
8
|
+
invalidWords: string[];
|
|
9
|
+
validWords: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const verifySlice = createSlice({
|
|
13
|
+
initialState: verifyInitialState,
|
|
14
|
+
name: 'verify',
|
|
15
|
+
reducers: {
|
|
16
|
+
submit: (state) => {
|
|
17
|
+
return { ...state, isLoading: true };
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
submitFailure: (state) => {
|
|
21
|
+
return { ...state, isLoading: false };
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
submitSuccess: (state, action: PayloadAction<VerifyParameters>) => {
|
|
25
|
+
const { board, invalidWords, validWords } = action.payload;
|
|
26
|
+
return { ...state, isLoading: false, lastSolvedParameters: { board }, invalidWords, validWords };
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export default verifySlice;
|
|
@@ -26,7 +26,8 @@ $easeOutSine: cubic-bezier(0.61, 1, 0.88, 1);
|
|
|
26
26
|
--color--background: #f4f4f4;
|
|
27
27
|
--color--background--secondary: #111;
|
|
28
28
|
--color--background--overlay: rgba(255, 255, 255, 0.65);
|
|
29
|
-
--color--error:
|
|
29
|
+
--color--error: #fc3d3d;
|
|
30
|
+
--color--success: #00a900;
|
|
30
31
|
--color--focus: #{rgba(#268fff, 0.5)};
|
|
31
32
|
--color--foreground: black;
|
|
32
33
|
--color--foreground--secondary: #444;
|
package/src/types/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export enum ResultColumn {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export type TranslationKey =
|
|
33
|
+
| 'cell.filter-cell'
|
|
33
34
|
| 'cell.set-blank'
|
|
34
35
|
| 'cell.set-not-blank'
|
|
35
36
|
| 'cell.toggle-direction'
|
|
@@ -79,7 +80,10 @@ export type TranslationKey =
|
|
|
79
80
|
| 'settings.autoGroupTiles.right'
|
|
80
81
|
| 'settings.autoGroupTiles.null'
|
|
81
82
|
| 'settings.game'
|
|
82
|
-
| 'settings.language'
|
|
83
|
+
| 'settings.language'
|
|
84
|
+
| 'words'
|
|
85
|
+
| 'words.invalid'
|
|
86
|
+
| 'words.valid';
|
|
83
87
|
|
|
84
88
|
export type Translate = (key: TranslationKey) => string;
|
|
85
89
|
|