@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.
Files changed (92) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +10 -10
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/next-server.js.nft.json +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/server-production/0.pack +0 -0
  9. package/.next/cache/webpack/server-production/index.pack +0 -0
  10. package/.next/next-server.js.nft.json +1 -1
  11. package/.next/prerender-manifest.json +1 -1
  12. package/.next/routes-manifest.json +1 -1
  13. package/.next/server/chunks/413.js +257 -55
  14. package/.next/server/chunks/44.js +802 -0
  15. package/.next/server/chunks/515.js +452 -104
  16. package/.next/server/chunks/911.js +53 -23
  17. package/.next/server/middleware-build-manifest.js +1 -1
  18. package/.next/server/pages/404.html +2 -2
  19. package/.next/server/pages/404.js.nft.json +1 -1
  20. package/.next/server/pages/500.html +2 -2
  21. package/.next/server/pages/_app.js.nft.json +1 -1
  22. package/.next/server/pages/_document.js.nft.json +1 -1
  23. package/.next/server/pages/_error.js.nft.json +1 -1
  24. package/.next/server/pages/api/solve.js +193 -927
  25. package/.next/server/pages/api/solve.js.nft.json +1 -1
  26. package/.next/server/pages/api/verify.js +217 -0
  27. package/.next/server/pages/api/verify.js.nft.json +1 -0
  28. package/.next/server/pages/index.html +3 -3
  29. package/.next/server/pages/index.js +7 -1
  30. package/.next/server/pages/index.js.nft.json +1 -1
  31. package/.next/server/pages/index.json +1 -1
  32. package/.next/server/pages-manifest.json +1 -0
  33. package/.next/static/chunks/317-a33dd38e9b9a17ed.js +1 -0
  34. package/.next/static/chunks/pages/{404-30c06e61d256c5b2.js → 404-90c624da3c83fd17.js} +1 -1
  35. package/.next/static/chunks/pages/_app-f8f360878e1c2aff.js +1 -0
  36. package/.next/static/chunks/pages/index-ecea697d3e5d8a6f.js +1 -0
  37. package/.next/static/css/64dc2ce1811912f1.css +1 -0
  38. package/.next/static/css/{cdbc9e0afcff5473.css → ad2a08918868cad8.css} +1 -1
  39. package/.next/static/yCxjzzYpw5JjJE53PO_s6/_buildManifest.js +1 -0
  40. package/.next/static/{z_0_lqfmiI_ISokr6NNRq → yCxjzzYpw5JjJE53PO_s6}/_ssgManifest.js +0 -0
  41. package/.next/trace +42 -40
  42. package/package.json +9 -9
  43. package/src/components/Badge/Badge.module.scss +13 -0
  44. package/src/components/Badge/Badge.tsx +15 -0
  45. package/src/components/Badge/index.ts +1 -0
  46. package/src/components/Board/components/Cell/Cell.module.scss +34 -9
  47. package/src/components/Board/components/Cell/Cell.tsx +23 -4
  48. package/src/components/Board/components/Cell/CellPure.tsx +29 -1
  49. package/src/components/Board/hooks/useGrid.ts +1 -0
  50. package/src/components/NavButtons/NavButtons.tsx +33 -14
  51. package/src/components/RemainingTiles/RemainingTiles.module.scss +8 -7
  52. package/src/components/RemainingTiles/RemainingTiles.tsx +13 -4
  53. package/src/components/Sidebar/Sidebar.tsx +2 -2
  54. package/src/components/Sidebar/components/Section/Section.module.scss +0 -1
  55. package/src/components/Sidebar/components/Section/Section.tsx +1 -1
  56. package/src/components/SquareButton/SquareButton.module.scss +5 -0
  57. package/src/components/Words/Words.module.scss +35 -0
  58. package/src/components/Words/Words.tsx +57 -0
  59. package/src/components/Words/index.ts +1 -0
  60. package/src/components/index.ts +2 -0
  61. package/src/i18n/de.json +5 -1
  62. package/src/i18n/en.json +5 -1
  63. package/src/i18n/es.json +5 -1
  64. package/src/i18n/fr.json +5 -1
  65. package/src/i18n/pl.json +5 -1
  66. package/src/icons/BookHalf.svg +4 -0
  67. package/src/icons/Check.svg +4 -0
  68. package/src/icons/Cross.svg +2 -2
  69. package/src/icons/CrossFill.svg +4 -0
  70. package/src/icons/Flag.svg +4 -0
  71. package/src/icons/Star.svg +4 -0
  72. package/src/icons/index.ts +5 -0
  73. package/src/pages/api/solve.ts +2 -3
  74. package/src/pages/api/verify.ts +71 -0
  75. package/src/pages/index.tsx +5 -0
  76. package/src/sdk/index.ts +1 -0
  77. package/src/sdk/verify.ts +24 -0
  78. package/src/state/rootReducer.ts +12 -1
  79. package/src/state/sagas.ts +54 -7
  80. package/src/state/selectors.ts +49 -11
  81. package/src/state/slices/cellFilterInitialState.ts +7 -0
  82. package/src/state/slices/cellFilterSlice.ts +24 -0
  83. package/src/state/slices/index.ts +4 -0
  84. package/src/state/slices/verifyInitialState.ts +12 -0
  85. package/src/state/slices/verifySlice.ts +31 -0
  86. package/src/styles/variables.scss +2 -1
  87. package/src/types/index.ts +5 -1
  88. package/.next/static/chunks/56-cf37c430261bbea5.js +0 -1
  89. package/.next/static/chunks/pages/_app-0b12a65bea70a0df.js +0 -1
  90. package/.next/static/chunks/pages/index-fcb69802550afb81.js +0 -1
  91. package/.next/static/css/1f39b55d50f5b30b.css +0 -1
  92. 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>
@@ -1,4 +1,4 @@
1
- <!-- https://icons.getbootstrap.com/icons/x-square-fill/ -->
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="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" />
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>
@@ -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';
@@ -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 Solver from '@scrabble-solver/solver';
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 solver = new Solver(config, trie);
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;
@@ -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
@@ -1,3 +1,4 @@
1
1
  export { default as findWordDefinitions } from './findWordDefinitions';
2
2
  export { default as solve } from './solve';
3
+ export { default as verify } from './verify';
3
4
  export { default as visit } from './visit';
@@ -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;
@@ -1,14 +1,25 @@
1
1
  import { combineReducers } from 'redux';
2
2
 
3
- import { boardSlice, dictionarySlice, rackSlice, resultsSlice, settingsSlice, solveSlice } from './slices';
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;
@@ -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 { boardSlice, dictionarySlice, rackSlice, resultsSlice, settingsSlice, solveSlice } from './slices';
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, onSubmit);
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* onSubmit(): AnyGenerator {
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);
@@ -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 || query.trim().length === 0) {
86
+ [selectSortedResults, selectResultsQuery, selectCellFilter],
87
+ (results, query, cellFilter) => {
88
+ if (!results) {
58
89
  return results;
59
90
  }
60
91
 
61
- return results.filter((result) => {
62
- try {
63
- const regExp = new RegExp(query, 'gi');
64
- return regExp.test(result.word);
65
- } catch {
66
- return false;
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,7 @@
1
+ import { Cell } from '@scrabble-solver/types';
2
+
3
+ export type Point = Pick<Cell, 'x' | 'y'>;
4
+
5
+ const cellFilterInitialState: Point[] = [];
6
+
7
+ export default cellFilterInitialState;
@@ -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: rgba(255, 0, 0, 0.75);
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;
@@ -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