@scrabble-solver/scrabble-solver 2.8.8 → 2.8.10
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 +12 -12
- 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 +350 -73
- package/.next/server/chunks/429.js +2 -13
- package/.next/server/chunks/44.js +802 -0
- package/.next/server/chunks/515.js +767 -322
- package/.next/server/chunks/911.js +77 -25
- 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 +226 -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 +8 -2
- 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/A8A_Lmg8cM-Bkf-Jo1CLh/_buildManifest.js +1 -0
- package/.next/static/{z3J3qmq1nazbDv_ENIkCo → A8A_Lmg8cM-Bkf-Jo1CLh}/_ssgManifest.js +0 -0
- package/.next/static/chunks/317-95ab9051449362fa.js +1 -0
- package/.next/static/chunks/758-eff80059a1365d5d.js +1 -0
- package/.next/static/chunks/pages/{404-8eb3ba4f0ba17e08.js → 404-90c624da3c83fd17.js} +1 -1
- package/.next/static/chunks/pages/_app-0e358b5622cf9e66.js +1 -0
- package/.next/static/chunks/pages/index-0cc5e6eda5adac73.js +1 -0
- package/.next/static/css/9ac903004135f4b1.css +1 -0
- package/.next/static/css/{cdbc9e0afcff5473.css → ad2a08918868cad8.css} +1 -1
- package/.next/trace +42 -41
- package/package.json +12 -12
- 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/Board.tsx +4 -2
- package/src/components/Board/BoardPure.tsx +25 -5
- package/src/components/Board/hooks/useGrid.ts +212 -91
- package/src/components/Dictionary/Dictionary.tsx +8 -1
- package/src/components/NavButtons/NavButtons.tsx +33 -14
- package/src/components/Rack/Rack.tsx +51 -11
- package/src/components/Rack/RackTile.tsx +33 -16
- package/src/components/RemainingTiles/RemainingTiles.module.scss +8 -7
- package/src/components/RemainingTiles/RemainingTiles.tsx +13 -4
- package/src/components/Results/Results.tsx +19 -3
- 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/Link.tsx +1 -1
- package/src/components/SquareButton/SquareButton.module.scss +5 -0
- package/src/components/Tile/Tile.module.scss +4 -0
- package/src/components/Tile/Tile.tsx +13 -4
- package/src/components/Tile/TilePure.tsx +3 -4
- 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 +4 -1
- package/src/i18n/en.json +4 -1
- package/src/i18n/es.json +4 -1
- package/src/i18n/fr.json +4 -1
- package/src/i18n/pl.json +4 -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/index.ts +3 -0
- package/src/lib/extractCharacters.ts +26 -0
- package/src/lib/extractInputValue.ts +17 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/isCtrl.ts +1 -1
- package/src/lib/memoize.ts +15 -1
- package/src/pages/api/solve.ts +3 -4
- package/src/pages/api/verify.ts +71 -0
- package/src/pages/index.tsx +5 -0
- package/src/sdk/fetchJson.ts +36 -0
- package/src/sdk/findWordDefinitions.ts +4 -3
- package/src/sdk/index.ts +1 -0
- package/src/sdk/solve.ts +8 -7
- package/src/sdk/verify.ts +23 -0
- package/src/state/rootReducer.ts +2 -0
- package/src/state/sagas.ts +35 -8
- package/src/state/selectors.ts +17 -1
- package/src/state/slices/dictionaryInitialState.ts +10 -2
- package/src/state/slices/dictionarySlice.ts +10 -16
- package/src/state/slices/index.ts +2 -0
- package/src/state/slices/rackSlice.ts +7 -0
- package/src/state/slices/solveInitialState.ts +14 -2
- package/src/state/slices/solveSlice.ts +7 -4
- 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 +6 -1
- package/.next/static/chunks/615-d258f6c528c18622.js +0 -1
- package/.next/static/chunks/758-f333b1dcdb941547.js +0 -1
- package/.next/static/chunks/pages/_app-4a663fd3d5ca4524.js +0 -1
- package/.next/static/chunks/pages/index-1a9826d740cc8830.js +0 -1
- package/.next/static/css/180c6c26317ac90f.css +0 -1
- package/.next/static/z3J3qmq1nazbDv_ENIkCo/_buildManifest.js +0 -1
package/src/i18n/de.json
CHANGED
|
@@ -49,5 +49,8 @@
|
|
|
49
49
|
"settings.autoGroupTiles.right": "Rechte Seite",
|
|
50
50
|
"settings.autoGroupTiles.null": "Nicht gruppieren",
|
|
51
51
|
"settings.game": "Spiel",
|
|
52
|
-
"settings.language": "Sprache"
|
|
52
|
+
"settings.language": "Sprache",
|
|
53
|
+
"words": "Gebildete Wörter",
|
|
54
|
+
"words.invalid": "Falsch",
|
|
55
|
+
"words.valid": "Korrekt"
|
|
53
56
|
}
|
package/src/i18n/en.json
CHANGED
|
@@ -49,5 +49,8 @@
|
|
|
49
49
|
"settings.autoGroupTiles.right": "On the right",
|
|
50
50
|
"settings.autoGroupTiles.null": "Do not group",
|
|
51
51
|
"settings.game": "Game",
|
|
52
|
-
"settings.language": "Language"
|
|
52
|
+
"settings.language": "Language",
|
|
53
|
+
"words": "Created words",
|
|
54
|
+
"words.invalid": "Invalid",
|
|
55
|
+
"words.valid": "Valid"
|
|
53
56
|
}
|
package/src/i18n/es.json
CHANGED
|
@@ -49,5 +49,8 @@
|
|
|
49
49
|
"settings.autoGroupTiles.right": "A la derecha",
|
|
50
50
|
"settings.autoGroupTiles.null": "No agrupar",
|
|
51
51
|
"settings.game": "Juego",
|
|
52
|
-
"settings.language": "Idioma"
|
|
52
|
+
"settings.language": "Idioma",
|
|
53
|
+
"words": "Palabras creadas",
|
|
54
|
+
"words.invalid": "Incorrecto",
|
|
55
|
+
"words.valid": "Correcto"
|
|
53
56
|
}
|
package/src/i18n/fr.json
CHANGED
|
@@ -49,5 +49,8 @@
|
|
|
49
49
|
"settings.autoGroupTiles.right": "Vers la gauche",
|
|
50
50
|
"settings.autoGroupTiles.null": "Ne pas grouper",
|
|
51
51
|
"settings.game": "Jeu",
|
|
52
|
-
"settings.language": "Langue"
|
|
52
|
+
"settings.language": "Langue",
|
|
53
|
+
"words": "Mots créés",
|
|
54
|
+
"words.invalid": "Incorrect",
|
|
55
|
+
"words.valid": "Corriger"
|
|
53
56
|
}
|
package/src/i18n/pl.json
CHANGED
|
@@ -49,5 +49,8 @@
|
|
|
49
49
|
"settings.autoGroupTiles.right": "Po prawej",
|
|
50
50
|
"settings.autoGroupTiles.null": "Nie grupuj",
|
|
51
51
|
"settings.game": "Gra",
|
|
52
|
-
"settings.language": "Język"
|
|
52
|
+
"settings.language": "Język",
|
|
53
|
+
"words": "Utworzone słowa",
|
|
54
|
+
"words.invalid": "Niepoprawne",
|
|
55
|
+
"words.valid": "Poprawne"
|
|
53
56
|
}
|
|
@@ -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>
|
package/src/icons/index.ts
CHANGED
|
@@ -2,10 +2,13 @@ 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';
|
|
11
14
|
export { default as Flag } from './Flag.svg';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BLANK } from '@scrabble-solver/constants';
|
|
2
|
+
import { Config } from '@scrabble-solver/types';
|
|
3
|
+
|
|
4
|
+
const extractCharacters = (config: Config, value: string): string[] => {
|
|
5
|
+
let index = 0;
|
|
6
|
+
const characters: string[] = [];
|
|
7
|
+
|
|
8
|
+
while (index < value.length) {
|
|
9
|
+
const character = value[index];
|
|
10
|
+
const nextCharacter = value[index + 1];
|
|
11
|
+
const twoCharacterTileCandidate = `${character}${nextCharacter}`;
|
|
12
|
+
|
|
13
|
+
if (config.twoCharacterTiles.includes(twoCharacterTileCandidate)) {
|
|
14
|
+
characters.push(twoCharacterTileCandidate);
|
|
15
|
+
++index;
|
|
16
|
+
} else if (config.hasCharacter(character) || character === BLANK) {
|
|
17
|
+
characters.push(character);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
++index;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return characters;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default extractCharacters;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const extractInputValue = (input: HTMLInputElement) => {
|
|
2
|
+
const value = input.value.toLocaleLowerCase();
|
|
3
|
+
|
|
4
|
+
if (input.selectionStart === null || input.selectionEnd === null) {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const index = Math.min(input.selectionStart, input.selectionEnd);
|
|
9
|
+
|
|
10
|
+
if (index > 0) {
|
|
11
|
+
return value.substring(index - 1, index);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default extractInputValue;
|
package/src/lib/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { default as createKeyboardNavigation } from './createKeyboardNavigation'
|
|
|
6
6
|
export { default as createKeyComparator } from './createKeyComparator';
|
|
7
7
|
export { default as createNullMovingComparator } from './createNullMovingComparator';
|
|
8
8
|
export { default as detectLocale } from './detectLocale';
|
|
9
|
+
export { default as extractCharacters } from './extractCharacters';
|
|
10
|
+
export { default as extractInputValue } from './extractInputValue';
|
|
9
11
|
export { default as findCell } from './findCell';
|
|
10
12
|
export { default as getCellSize } from './getCellSize';
|
|
11
13
|
export { default as getRemainingTiles } from './getRemainingTiles';
|
package/src/lib/isCtrl.ts
CHANGED
package/src/lib/memoize.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
interface AnyFunction {
|
|
2
|
-
(...parameters: any[]): any
|
|
2
|
+
(...parameters: any[]): any | Promise<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
interface AnyCachedFunction<T extends AnyFunction> extends AnyFunction {
|
|
@@ -20,6 +20,14 @@ const memoize = <T extends AnyFunction>(fn: T): AnyCachedFunction<T> => {
|
|
|
20
20
|
return cache.find((entry) => parametersEqual(entry.parameters, parameters))?.result;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
const removeCache = (parameters: Parameters<T>): void => {
|
|
24
|
+
const index = cache.findIndex((entry) => parametersEqual(entry.parameters, parameters));
|
|
25
|
+
|
|
26
|
+
if (index >= 0) {
|
|
27
|
+
cache.splice(index, 1);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
23
31
|
const writeCache = (parameters: Parameters<T>, result: ReturnType<T>): void => {
|
|
24
32
|
cache.push({ parameters, result });
|
|
25
33
|
};
|
|
@@ -33,6 +41,12 @@ const memoize = <T extends AnyFunction>(fn: T): AnyCachedFunction<T> => {
|
|
|
33
41
|
|
|
34
42
|
const result = fn(...parameters);
|
|
35
43
|
|
|
44
|
+
if (result instanceof Promise) {
|
|
45
|
+
result.catch(() => {
|
|
46
|
+
removeCache(parameters);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
writeCache(parameters, result);
|
|
37
51
|
|
|
38
52
|
return result;
|
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,9 +36,8 @@ 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
|
-
|
|
41
|
-
response.status(200).send(results.map((result) => result.toJson()));
|
|
39
|
+
const results = solveScrabble(trie, config, board, tiles);
|
|
40
|
+
response.status(200).send(results);
|
|
42
41
|
} catch (error) {
|
|
43
42
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
44
43
|
logger.error('solve - error', { error, meta });
|
|
@@ -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} />
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isError } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
const fetchJson = async <T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> => {
|
|
4
|
+
let response: Response;
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
response = await fetch(input, {
|
|
8
|
+
...init,
|
|
9
|
+
headers: {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
...init?.headers,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
} catch (error) {
|
|
15
|
+
const message = isError(error) ? error.message : 'Unknown error';
|
|
16
|
+
throw new Error(`Network error: ${message}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (response.ok) {
|
|
20
|
+
return response.json();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const json = await response.json();
|
|
25
|
+
|
|
26
|
+
if (isError(json)) {
|
|
27
|
+
throw new Error(json.message);
|
|
28
|
+
}
|
|
29
|
+
} finally {
|
|
30
|
+
// do nothing
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default fetchJson;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Locale, WordDefinition } from '@scrabble-solver/types';
|
|
1
|
+
import { Locale, WordDefinition, WordDefinitionJson } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import fetchJson from './fetchJson';
|
|
2
4
|
|
|
3
5
|
const findWordDefinitions = async (locale: Locale, word: string): Promise<WordDefinition[]> => {
|
|
4
|
-
const
|
|
5
|
-
const json = await fetch(url).then((response) => response.json());
|
|
6
|
+
const json = await fetchJson<WordDefinitionJson[]>(`/api/dictionary/${locale}/${word}`);
|
|
6
7
|
return json.map(WordDefinition.fromJson);
|
|
7
8
|
};
|
|
8
9
|
|
package/src/sdk/index.ts
CHANGED
package/src/sdk/solve.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { BoardJson, Locale, ResultJson } from '@scrabble-solver/types';
|
|
1
|
+
import { BoardJson, Locale, Result, ResultJson } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import fetchJson from './fetchJson';
|
|
2
4
|
|
|
3
5
|
interface Payload {
|
|
4
6
|
board: BoardJson;
|
|
@@ -7,14 +9,13 @@ interface Payload {
|
|
|
7
9
|
locale: Locale;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
const solve = ({ board, characters, configId, locale }: Payload): Promise<
|
|
11
|
-
|
|
12
|
+
const solve = async ({ board, characters, configId, locale }: Payload): Promise<Result[]> => {
|
|
13
|
+
const json = await fetchJson<ResultJson[]>('/api/solve', {
|
|
12
14
|
method: 'POST',
|
|
13
|
-
headers: {
|
|
14
|
-
'Content-Type': 'application/json',
|
|
15
|
-
},
|
|
16
15
|
body: JSON.stringify({ board, characters, configId, locale }),
|
|
17
|
-
})
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return json.map(Result.fromJson);
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export default solve;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BoardJson, Locale } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import fetchJson from './fetchJson';
|
|
4
|
+
|
|
5
|
+
interface Payload {
|
|
6
|
+
board: BoardJson;
|
|
7
|
+
configId: string;
|
|
8
|
+
locale: Locale;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Response {
|
|
12
|
+
invalidWords: string[];
|
|
13
|
+
validWords: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const verify = async ({ board, configId, locale }: Payload): Promise<Response> => {
|
|
17
|
+
return fetchJson<Response>('/api/verify', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
body: JSON.stringify({ board, configId, locale }),
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default verify;
|
package/src/state/rootReducer.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
resultsSlice,
|
|
9
9
|
settingsSlice,
|
|
10
10
|
solveSlice,
|
|
11
|
+
verifySlice,
|
|
11
12
|
} from './slices';
|
|
12
13
|
|
|
13
14
|
const rootReducer = combineReducers({
|
|
@@ -18,6 +19,7 @@ const rootReducer = combineReducers({
|
|
|
18
19
|
results: resultsSlice.reducer,
|
|
19
20
|
settings: settingsSlice.reducer,
|
|
20
21
|
solve: solveSlice.reducer,
|
|
22
|
+
verify: verifySlice.reducer,
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
export default rootReducer;
|
package/src/state/sagas.ts
CHANGED
|
@@ -3,7 +3,7 @@ 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 {
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
resultsSlice,
|
|
24
24
|
settingsSlice,
|
|
25
25
|
solveSlice,
|
|
26
|
+
verifySlice,
|
|
26
27
|
} from './slices';
|
|
27
28
|
|
|
28
29
|
const SUBMIT_DELAY = 150;
|
|
@@ -43,7 +44,8 @@ export function* rootSaga(): AnyGenerator {
|
|
|
43
44
|
yield takeLatest(dictionarySlice.actions.submit.type, onDictionarySubmit);
|
|
44
45
|
yield takeLatest(initialize.type, onInitialize);
|
|
45
46
|
yield takeLatest(reset.type, onReset);
|
|
46
|
-
yield takeLatest(solveSlice.actions.submit.type,
|
|
47
|
+
yield takeLatest(solveSlice.actions.submit.type, onSolve);
|
|
48
|
+
yield takeLatest(verifySlice.actions.submit.type, onVerify);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
function* onCellValueChange({ payload }: PayloadAction<{ value: string; x: number; y: number }>): AnyGenerator {
|
|
@@ -52,6 +54,8 @@ function* onCellValueChange({ payload }: PayloadAction<{ value: string; x: numbe
|
|
|
52
54
|
if (isFiltered) {
|
|
53
55
|
yield put(cellFilterSlice.actions.toggle(payload));
|
|
54
56
|
}
|
|
57
|
+
|
|
58
|
+
yield put(verifySlice.actions.submit());
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
function* onApplyResult({ payload: result }: PayloadAction<Result>): AnyGenerator {
|
|
@@ -60,11 +64,13 @@ function* onApplyResult({ payload: result }: PayloadAction<Result>): AnyGenerato
|
|
|
60
64
|
yield put(cellFilterSlice.actions.reset());
|
|
61
65
|
yield put(rackSlice.actions.removeTiles(result.tiles));
|
|
62
66
|
yield put(rackSlice.actions.groupTiles(autoGroupTiles));
|
|
67
|
+
yield put(verifySlice.actions.submit());
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
function* onConfigIdChange(): AnyGenerator {
|
|
66
71
|
yield put(resultsSlice.actions.reset());
|
|
67
72
|
yield put(solveSlice.actions.submit());
|
|
73
|
+
yield put(verifySlice.actions.submit());
|
|
68
74
|
yield* ensureProperTilesCount();
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -80,7 +86,7 @@ function* onDictionarySubmit(): AnyGenerator {
|
|
|
80
86
|
const wordDefinitions = yield call(memoizedFindWordDefinitions, locale, word);
|
|
81
87
|
yield put(dictionarySlice.actions.submitSuccess(wordDefinitions));
|
|
82
88
|
} catch (error) {
|
|
83
|
-
yield put(dictionarySlice.actions.submitFailure());
|
|
89
|
+
yield put(dictionarySlice.actions.submitFailure(error));
|
|
84
90
|
}
|
|
85
91
|
}
|
|
86
92
|
|
|
@@ -95,12 +101,14 @@ function* onReset(): AnyGenerator {
|
|
|
95
101
|
yield put(dictionarySlice.actions.reset());
|
|
96
102
|
yield put(rackSlice.actions.reset());
|
|
97
103
|
yield put(resultsSlice.actions.reset());
|
|
104
|
+
yield put(verifySlice.actions.submit());
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
function* onLocaleChange(): AnyGenerator {
|
|
101
|
-
yield put(solveSlice.actions.submit());
|
|
102
|
-
yield put(resultsSlice.actions.changeResultCandidate(null));
|
|
103
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());
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
function* onResultCandidateChange({ payload: result }: PayloadAction<Result | null>): AnyGenerator {
|
|
@@ -110,7 +118,7 @@ function* onResultCandidateChange({ payload: result }: PayloadAction<Result | nu
|
|
|
110
118
|
}
|
|
111
119
|
}
|
|
112
120
|
|
|
113
|
-
function*
|
|
121
|
+
function* onSolve(): AnyGenerator {
|
|
114
122
|
const board = yield select(selectBoard);
|
|
115
123
|
const { config } = yield select(selectConfig);
|
|
116
124
|
const locale = yield select(selectLocale);
|
|
@@ -129,11 +137,30 @@ function* onSubmit(): AnyGenerator {
|
|
|
129
137
|
configId: config.id,
|
|
130
138
|
locale,
|
|
131
139
|
});
|
|
140
|
+
yield put(resultsSlice.actions.changeResults(results));
|
|
132
141
|
yield put(solveSlice.actions.submitSuccess({ board, characters }));
|
|
133
|
-
yield put(resultsSlice.actions.changeResults(results.map(Result.fromJson)));
|
|
134
142
|
} catch (error) {
|
|
135
143
|
yield put(resultsSlice.actions.changeResults([]));
|
|
136
|
-
yield put(solveSlice.actions.submitFailure());
|
|
144
|
+
yield put(solveSlice.actions.submitFailure(error));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
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());
|
|
137
164
|
}
|
|
138
165
|
}
|
|
139
166
|
|
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, Result, Tile } from '@scrabble-solver/types';
|
|
3
|
+
import { Cell, Config, isError, Result, Tile } from '@scrabble-solver/types';
|
|
4
4
|
|
|
5
5
|
import i18n from 'i18n';
|
|
6
6
|
import { findCell, getRemainingTiles, getRemainingTilesGroups, sortResults, unorderedArraysEqual } from 'lib';
|
|
@@ -30,8 +30,14 @@ const selectSettingsRoot = (state: RootState): RootState['settings'] => state.se
|
|
|
30
30
|
|
|
31
31
|
const selectSolveRoot = (state: RootState): RootState['solve'] => state.solve;
|
|
32
32
|
|
|
33
|
+
const selectVerifyRoot = (state: RootState): RootState['verify'] => state.verify;
|
|
34
|
+
|
|
33
35
|
export const selectDictionary = selectDictionaryRoot;
|
|
34
36
|
|
|
37
|
+
export const selectDictionaryError = createSelector([selectDictionaryRoot], (dictionary) => {
|
|
38
|
+
return isError(dictionary.error) ? dictionary.error : undefined;
|
|
39
|
+
});
|
|
40
|
+
|
|
35
41
|
export const selectAutoGroupTiles = createSelector([selectSettingsRoot], (settings) => settings.autoGroupTiles);
|
|
36
42
|
|
|
37
43
|
export const selectLocale = createSelector([selectSettingsRoot], (settings) => settings.locale);
|
|
@@ -156,6 +162,10 @@ export const selectLastSolvedParameters = createSelector([selectSolveRoot], (sol
|
|
|
156
162
|
|
|
157
163
|
export const selectIsLoading = createSelector([selectSolveRoot], (solve) => solve.isLoading);
|
|
158
164
|
|
|
165
|
+
export const selectSolveError = createSelector([selectSolveRoot], (solve) => {
|
|
166
|
+
return isError(solve.error) ? solve.error : undefined;
|
|
167
|
+
});
|
|
168
|
+
|
|
159
169
|
export const selectHaveCharactersChanged = createSelector(
|
|
160
170
|
[selectLastSolvedParameters, selectCharacters],
|
|
161
171
|
(lastSolvedParameters, characters) => {
|
|
@@ -183,3 +193,9 @@ export const selectHasOverusedTiles = createSelector([selectRemainingTiles], (re
|
|
|
183
193
|
});
|
|
184
194
|
|
|
185
195
|
export const selectRemainingTilesGroups = createSelector([selectRemainingTiles], getRemainingTilesGroups);
|
|
196
|
+
|
|
197
|
+
export const selectVerify = selectVerifyRoot;
|
|
198
|
+
|
|
199
|
+
export const selectHasInvalidWords = createSelector([selectVerify], ({ invalidWords }) => {
|
|
200
|
+
return invalidWords.length > 0;
|
|
201
|
+
});
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { WordDefinition } from '@scrabble-solver/types';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface DictionaryInitialState {
|
|
4
|
+
error: unknown | undefined;
|
|
5
|
+
input: string;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
results: WordDefinition[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const dictionaryInitialState: DictionaryInitialState = {
|
|
11
|
+
error: undefined,
|
|
4
12
|
input: '',
|
|
5
13
|
isLoading: false,
|
|
6
|
-
results: []
|
|
14
|
+
results: [],
|
|
7
15
|
};
|
|
8
16
|
|
|
9
17
|
export default dictionaryInitialState;
|