@scrabble-solver/scrabble-solver 2.8.6 → 2.8.7
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 +9 -9
- 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 +183 -94
- package/.next/server/chunks/{206.js → 429.js} +2 -4137
- package/.next/server/chunks/515.js +92 -70
- package/.next/server/chunks/{907.js → 911.js} +134 -367
- package/.next/server/chunks/939.js +218 -0
- 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/dictionary/[locale]/[word].js +33 -17
- package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
- package/.next/server/pages/api/solve.js +399 -56
- package/.next/server/pages/api/solve.js.nft.json +1 -1
- package/.next/server/pages/api/visit.js +3 -2
- package/.next/server/pages/api/visit.js.nft.json +1 -1
- package/.next/server/pages/index.html +3 -7
- package/.next/server/pages/index.js +12 -14
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/static/chunks/56-cf37c430261bbea5.js +1 -0
- package/.next/static/chunks/pages/_app-0b12a65bea70a0df.js +1 -0
- package/.next/static/chunks/pages/index-fcb69802550afb81.js +1 -0
- package/.next/static/css/1f39b55d50f5b30b.css +1 -0
- package/.next/static/css/751e8a14776d05d8.css +1 -0
- package/.next/static/z_0_lqfmiI_ISokr6NNRq/_buildManifest.js +1 -0
- package/.next/static/{VjSpyGDWyVaO0muz54q_j → z_0_lqfmiI_ISokr6NNRq}/_ssgManifest.js +0 -0
- package/.next/trace +40 -42
- package/package.json +9 -9
- package/src/api/index.ts +3 -9
- package/src/api/isBoardValid.ts +43 -0
- package/src/api/isCellValid.ts +26 -0
- package/src/api/isRowValid.ts +19 -0
- package/src/components/Dictionary/Dictionary.module.scss +20 -0
- package/src/components/Dictionary/Dictionary.tsx +40 -29
- package/src/components/Results/Cell.tsx +3 -2
- package/src/components/Results/Result.tsx +16 -6
- package/src/components/ResultsInput/ResultsInput.tsx +11 -3
- package/src/hooks/useIsTablet.ts +2 -2
- package/src/lib/getRemainingTiles.ts +1 -1
- package/src/lib/index.ts +2 -1
- package/src/lib/isRegExp.ts +11 -0
- package/src/lib/isStringArray.ts +5 -0
- package/src/lib/sortResults.ts +5 -5
- package/src/pages/api/dictionary/[locale]/[word].ts +35 -11
- package/src/pages/api/solve.ts +39 -19
- package/src/pages/api/visit.ts +1 -0
- package/src/pages/index.module.scss +5 -11
- package/src/pages/index.tsx +5 -5
- package/src/sdk/{findWordDefinition.ts → findWordDefinitions.ts} +3 -3
- package/src/sdk/index.ts +1 -1
- package/src/state/sagas.ts +11 -11
- package/src/state/selectors.ts +6 -2
- package/src/state/slices/dictionaryInitialState.ts +3 -3
- package/src/state/slices/dictionarySlice.ts +4 -10
- package/.next/static/VjSpyGDWyVaO0muz54q_j/_buildManifest.js +0 -1
- package/.next/static/chunks/56-e2797384ae4b0fc0.js +0 -1
- package/.next/static/chunks/pages/_app-5136d33b9b007fd7.js +0 -1
- package/.next/static/chunks/pages/index-13ea7770a65c69ee.js +0 -1
- package/.next/static/css/3159cfe62ff742a3.css +0 -1
- package/.next/static/css/729bb37fe8f9bee6.css +0 -1
- package/src/api/validateBoard.ts +0 -45
- package/src/api/validateCell.ts +0 -40
- package/src/api/validateCharacter.ts +0 -14
- package/src/api/validateCharacters.ts +0 -24
- package/src/api/validateConfigId.ts +0 -9
- package/src/api/validateLocale.ts +0 -15
- package/src/api/validateRow.ts +0 -17
- package/src/api/validateTile.ts +0 -21
- package/src/api/validateWord.ts +0 -11
- package/src/lib/isLocale.ts +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scrabble-solver/scrabble-solver",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.7",
|
|
4
4
|
"description": "Scrabble Solver 2 - App",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=16"
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@popperjs/core": "^2.11.6",
|
|
32
32
|
"@reduxjs/toolkit": "^1.8.5",
|
|
33
|
-
"@scrabble-solver/configs": "^2.8.
|
|
34
|
-
"@scrabble-solver/constants": "^2.8.
|
|
35
|
-
"@scrabble-solver/dictionaries": "^2.8.
|
|
36
|
-
"@scrabble-solver/logger": "^2.8.
|
|
37
|
-
"@scrabble-solver/solver": "^2.8.
|
|
38
|
-
"@scrabble-solver/types": "^2.8.
|
|
39
|
-
"@scrabble-solver/word-definitions": "^2.8.
|
|
33
|
+
"@scrabble-solver/configs": "^2.8.7",
|
|
34
|
+
"@scrabble-solver/constants": "^2.8.7",
|
|
35
|
+
"@scrabble-solver/dictionaries": "^2.8.7",
|
|
36
|
+
"@scrabble-solver/logger": "^2.8.7",
|
|
37
|
+
"@scrabble-solver/solver": "^2.8.7",
|
|
38
|
+
"@scrabble-solver/types": "^2.8.7",
|
|
39
|
+
"@scrabble-solver/word-definitions": "^2.8.7",
|
|
40
40
|
"classnames": "^2.3.2",
|
|
41
41
|
"next": "^12.3.1",
|
|
42
42
|
"normalize.css": "^8.0.1",
|
|
@@ -68,5 +68,5 @@
|
|
|
68
68
|
"env-cmd": "^10.1.0",
|
|
69
69
|
"sass": "^1.55.0"
|
|
70
70
|
},
|
|
71
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "879bf9b68f27fa3d530d0582fd8fbbcd5c8b150b"
|
|
72
72
|
}
|
package/src/api/index.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
export { default as getServerLoggingData } from './getServerLoggingData';
|
|
2
|
-
export { default as
|
|
3
|
-
export { default as
|
|
4
|
-
export { default as
|
|
5
|
-
export { default as validateCharacters } from './validateCharacters';
|
|
6
|
-
export { default as validateConfigId } from './validateConfigId';
|
|
7
|
-
export { default as validateLocale } from './validateLocale';
|
|
8
|
-
export { default as validateRow } from './validateRow';
|
|
9
|
-
export { default as validateTile } from './validateTile';
|
|
10
|
-
export { default as validateWord } from './validateWord';
|
|
2
|
+
export { default as isBoardValid } from './isBoardValid';
|
|
3
|
+
export { default as isCellValid } from './isCellValid';
|
|
4
|
+
export { default as isRowValid } from './isRowValid';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BoardJson, CellJson, Config } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import isRowValid from './isRowValid';
|
|
4
|
+
|
|
5
|
+
const isBoardValid = (board: BoardJson, config: Config): boolean => {
|
|
6
|
+
if (board.length !== config.boardHeight) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
for (const row of board) {
|
|
11
|
+
if (!isRowValid(row, config)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return areTwoCharacterTilesValid(board, config);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const areTwoCharacterTilesValid = (board: CellJson[][], config: Config): boolean => {
|
|
20
|
+
const cells: CellJson[] = board
|
|
21
|
+
.flat()
|
|
22
|
+
.filter((cell) => cell && cell.tile && config.isTwoCharacterTilePrefix(cell.tile.character));
|
|
23
|
+
|
|
24
|
+
for (const cell of cells) {
|
|
25
|
+
for (const characters of config.twoCharacterTiles) {
|
|
26
|
+
const canCheckDown = cell.y + 1 < board.length;
|
|
27
|
+
const canCheckRight = cell.x + 1 < board[0].length;
|
|
28
|
+
const cellDown = board[cell.y + 1][cell.x];
|
|
29
|
+
const cellRight = board[cell.y][cell.x + 1];
|
|
30
|
+
const collidesDown = canCheckDown && cellDown.tile && cellDown.tile.character === characters[1];
|
|
31
|
+
const collidesRight = canCheckRight && cellRight.tile && cellRight.tile.character === characters[1];
|
|
32
|
+
const collides = collidesDown || collidesRight;
|
|
33
|
+
|
|
34
|
+
if (cell.tile && characters.startsWith(cell.tile.character) && collides) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default isBoardValid;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BLANK } from '@scrabble-solver/constants';
|
|
2
|
+
import { CellJson, Config } from '@scrabble-solver/types';
|
|
3
|
+
|
|
4
|
+
const isCellValid = (cell: CellJson, config: Config): boolean => {
|
|
5
|
+
const { isEmpty, tile, x, y } = cell;
|
|
6
|
+
|
|
7
|
+
if (x < 0 || x >= config.boardWidth) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (y < 0 || y >= config.boardHeight) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (isEmpty && tile !== null) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (tile !== null && !config.hasCharacter(tile.character) && tile.character !== BLANK) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default isCellValid;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CellJson, Config } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import isCellValid from './isCellValid';
|
|
4
|
+
|
|
5
|
+
const isRowValid = (row: CellJson[], config: Config): boolean => {
|
|
6
|
+
if (row.length !== config.boardWidth) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
for (const cell of row) {
|
|
11
|
+
if (!isCellValid(cell, config)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return true;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default isRowValid;
|
|
@@ -7,6 +7,26 @@
|
|
|
7
7
|
font-size: var(--font--size--s);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
.result {
|
|
11
|
+
transition: var(--transition);
|
|
12
|
+
|
|
13
|
+
&.isAllowed {
|
|
14
|
+
& + & {
|
|
15
|
+
.content {
|
|
16
|
+
padding-top: 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&.isNotAllowed {
|
|
22
|
+
& + & {
|
|
23
|
+
.content {
|
|
24
|
+
padding-top: 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
10
30
|
.content {
|
|
11
31
|
padding: var(--spacing--l);
|
|
12
32
|
}
|
|
@@ -14,44 +14,55 @@ interface Props {
|
|
|
14
14
|
|
|
15
15
|
const Dictionary: FunctionComponent<Props> = ({ className }) => {
|
|
16
16
|
const translate = useTranslate();
|
|
17
|
-
const {
|
|
17
|
+
const { results, isLoading } = useTypedSelector(selectDictionary);
|
|
18
|
+
const isFirstAllowed = results.length > 0 ? results[0].isAllowed : undefined;
|
|
18
19
|
|
|
19
20
|
return (
|
|
20
21
|
<div
|
|
21
22
|
className={classNames(styles.dictionary, className, {
|
|
22
|
-
[styles.isAllowed]:
|
|
23
|
-
[styles.isNotAllowed]:
|
|
23
|
+
[styles.isAllowed]: isFirstAllowed === true,
|
|
24
|
+
[styles.isNotAllowed]: isFirstAllowed === false,
|
|
24
25
|
})}
|
|
25
26
|
>
|
|
26
|
-
{
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
{isAllowed === true && (
|
|
37
|
-
<>
|
|
38
|
-
{definitions.length === 0 && <div>{translate('dictionary.empty-state.no-definitions')}</div>}
|
|
39
|
-
|
|
40
|
-
{definitions.length > 0 && (
|
|
41
|
-
<ul className={styles.definitions}>
|
|
42
|
-
{definitions.map((result, index) => (
|
|
43
|
-
<li key={index} className={styles.definition}>
|
|
44
|
-
{result}
|
|
45
|
-
</li>
|
|
46
|
-
))}
|
|
47
|
-
</ul>
|
|
48
|
-
)}
|
|
49
|
-
</>
|
|
27
|
+
{results.map(({ definitions, isAllowed, word }) => (
|
|
28
|
+
<div
|
|
29
|
+
className={classNames(styles.result, {
|
|
30
|
+
[styles.isAllowed]: isAllowed === true,
|
|
31
|
+
[styles.isNotAllowed]: isAllowed === false,
|
|
32
|
+
})}
|
|
33
|
+
key={word}
|
|
34
|
+
>
|
|
35
|
+
{typeof word === 'undefined' && (
|
|
36
|
+
<EmptyState type="info">{translate('dictionary.empty-state.uninitialized')}</EmptyState>
|
|
50
37
|
)}
|
|
51
38
|
|
|
52
|
-
{
|
|
39
|
+
{typeof word !== 'undefined' && (
|
|
40
|
+
<div className={styles.content}>
|
|
41
|
+
{word && <h2 className={styles.word}>{word}</h2>}
|
|
42
|
+
|
|
43
|
+
{isAllowed === false && <div>{translate('dictionary.empty-state.not-allowed')}</div>}
|
|
44
|
+
|
|
45
|
+
{isAllowed === true && (
|
|
46
|
+
<>
|
|
47
|
+
{definitions.length === 0 && <div>{translate('dictionary.empty-state.no-definitions')}</div>}
|
|
48
|
+
|
|
49
|
+
{definitions.length > 0 && (
|
|
50
|
+
<ul className={styles.definitions}>
|
|
51
|
+
{definitions.map((result, index) => (
|
|
52
|
+
<li key={index} className={styles.definition}>
|
|
53
|
+
{result}
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
</ul>
|
|
57
|
+
)}
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{!isLoading && isAllowed === null && <div>{translate('dictionary.empty-state.no-results')}</div>}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
53
64
|
</div>
|
|
54
|
-
)}
|
|
65
|
+
))}
|
|
55
66
|
|
|
56
67
|
{isLoading && <Loading />}
|
|
57
68
|
</div>
|
|
@@ -11,12 +11,13 @@ import styles from './Results.module.scss';
|
|
|
11
11
|
interface Props {
|
|
12
12
|
className?: string;
|
|
13
13
|
translationKey: TranslationKey;
|
|
14
|
+
tooltip?: string | number;
|
|
14
15
|
value: string | number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const Cell: FunctionComponent<Props> = ({ className, translationKey, value }) => {
|
|
18
|
+
const Cell: FunctionComponent<Props> = ({ className, translationKey, tooltip, value }) => {
|
|
18
19
|
const translate = useTranslate();
|
|
19
|
-
const triggerProps = useTooltip(`${translate(translationKey)}: ${value}`);
|
|
20
|
+
const triggerProps = useTooltip(`${translate(translationKey)}: ${tooltip || value}`);
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
23
|
<span className={classNames(styles.cell, className)} {...triggerProps}>
|
|
@@ -16,6 +16,7 @@ const Result = ({ index, style }: Props): ReactElement => {
|
|
|
16
16
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
17
17
|
const results = useTypedSelector(selectSortedFilteredResults)!;
|
|
18
18
|
const result = results[index];
|
|
19
|
+
const otherWords = result.words.slice(1).join(' / ').toLocaleUpperCase();
|
|
19
20
|
|
|
20
21
|
const handleClick = () => {
|
|
21
22
|
dispatch(resultsSlice.actions.applyResult(result));
|
|
@@ -49,12 +50,21 @@ const Result = ({ index, style }: Props): ReactElement => {
|
|
|
49
50
|
onMouseLeave={handleMouseLeave}
|
|
50
51
|
>
|
|
51
52
|
<span className={styles.resultContent}>
|
|
52
|
-
<Cell
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<Cell className={styles.stat} translationKey="common.
|
|
53
|
+
<Cell
|
|
54
|
+
className={styles.word}
|
|
55
|
+
translationKey="common.word"
|
|
56
|
+
value={`${result.word.toLocaleUpperCase()}${otherWords.length > 0 ? ` (${otherWords})` : ''}`}
|
|
57
|
+
/>
|
|
58
|
+
<Cell className={styles.stat} translationKey="common.tiles" value={result.tilesCount} />
|
|
59
|
+
<Cell className={styles.stat} translationKey="common.consonants" value={result.consonantsCount} />
|
|
60
|
+
<Cell className={styles.stat} translationKey="common.vowels" value={result.vowelsCount} />
|
|
61
|
+
<Cell className={styles.stat} translationKey="common.blanks" value={result.blanksCount} />
|
|
62
|
+
<Cell
|
|
63
|
+
className={styles.stat}
|
|
64
|
+
translationKey="common.words"
|
|
65
|
+
tooltip={`${result.wordsCount} (${result.words.join(' / ').toLocaleUpperCase()})`}
|
|
66
|
+
value={result.wordsCount}
|
|
67
|
+
/>
|
|
58
68
|
<Cell className={styles.points} translationKey="common.points" value={result.points} />
|
|
59
69
|
</span>
|
|
60
70
|
</button>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
import { ChangeEvent, FunctionComponent } from 'react';
|
|
2
|
+
import { ChangeEvent, FunctionComponent, useState } from 'react';
|
|
3
3
|
import { useDispatch } from 'react-redux';
|
|
4
4
|
|
|
5
|
+
import { isRegExp } from 'lib';
|
|
5
6
|
import { resultsSlice, selectResultsQuery, useTranslate, useTypedSelector } from 'state';
|
|
6
7
|
|
|
7
8
|
import styles from './ResultsInput.module.scss';
|
|
@@ -14,9 +15,16 @@ const ResultsInput: FunctionComponent<Props> = ({ className }) => {
|
|
|
14
15
|
const dispatch = useDispatch();
|
|
15
16
|
const translate = useTranslate();
|
|
16
17
|
const value = useTypedSelector(selectResultsQuery);
|
|
18
|
+
const [localValue, setLocalValue] = useState(value);
|
|
17
19
|
|
|
18
20
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
19
|
-
|
|
21
|
+
const newValue = event.target.value;
|
|
22
|
+
|
|
23
|
+
setLocalValue(newValue);
|
|
24
|
+
|
|
25
|
+
if (isRegExp(newValue)) {
|
|
26
|
+
dispatch(resultsSlice.actions.changeQuery(newValue));
|
|
27
|
+
}
|
|
20
28
|
};
|
|
21
29
|
|
|
22
30
|
return (
|
|
@@ -25,7 +33,7 @@ const ResultsInput: FunctionComponent<Props> = ({ className }) => {
|
|
|
25
33
|
className={styles.input}
|
|
26
34
|
placeholder={translate('results.input.placeholder')}
|
|
27
35
|
type="text"
|
|
28
|
-
value={
|
|
36
|
+
value={localValue}
|
|
29
37
|
onChange={handleChange}
|
|
30
38
|
/>
|
|
31
39
|
</form>
|
package/src/hooks/useIsTablet.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useMedia } from 'react-use';
|
|
2
2
|
|
|
3
3
|
const useIsTablet = (): boolean => {
|
|
4
|
-
const isTabletHeight = useMedia('(max-height: 800px)');
|
|
5
|
-
const isTabletWidth = useMedia('(max-width: 1024px)');
|
|
4
|
+
const isTabletHeight = useMedia('(max-height: 800px)', false);
|
|
5
|
+
const isTabletWidth = useMedia('(max-width: 1024px)', false);
|
|
6
6
|
const isTablet = isTabletHeight || isTabletWidth;
|
|
7
7
|
return isTablet;
|
|
8
8
|
};
|
|
@@ -11,7 +11,7 @@ const getRemainingTiles = (config: Config, board: Board, characters: string[]):
|
|
|
11
11
|
const remainingTiles = Object.fromEntries(config.tiles.map((tile) => [tile.character, { ...tile, usedCount: 0 }]));
|
|
12
12
|
const blank: RemainingTile = {
|
|
13
13
|
character: BLANK,
|
|
14
|
-
count: config.
|
|
14
|
+
count: config.blanksCount,
|
|
15
15
|
score: config.blankScore,
|
|
16
16
|
usedCount:
|
|
17
17
|
nonEmptyCells.filter((cell) => cell.tile.isBlank).length +
|
package/src/lib/index.ts
CHANGED
|
@@ -15,8 +15,9 @@ export { default as getTotalRemainingTilesCount } from './getTotalRemainingTiles
|
|
|
15
15
|
export { default as getTileSizes } from './getTileSizes';
|
|
16
16
|
export { default as inverseDirection } from './inverseDirection';
|
|
17
17
|
export { default as isCtrl } from './isCtrl';
|
|
18
|
-
export { default as isLocale } from './isLocale';
|
|
19
18
|
export { default as isMac } from './isMac';
|
|
19
|
+
export { default as isRegExp } from './isRegExp';
|
|
20
|
+
export { default as isStringArray } from './isStringArray';
|
|
20
21
|
export { default as memoize } from './memoize';
|
|
21
22
|
export { default as noop } from './noop';
|
|
22
23
|
export { default as numberComparator } from './numberComparator';
|
package/src/lib/sortResults.ts
CHANGED
|
@@ -6,13 +6,13 @@ import createKeyComparator from './createKeyComparator';
|
|
|
6
6
|
import reverseComparator from './reverseComparator';
|
|
7
7
|
|
|
8
8
|
const comparators: Record<ResultColumn, Comparator<Result>> = {
|
|
9
|
-
[ResultColumn.BlanksCount]: createKeyComparator('
|
|
10
|
-
[ResultColumn.ConsonantsCount]: createKeyComparator('
|
|
9
|
+
[ResultColumn.BlanksCount]: createKeyComparator('blanksCount'),
|
|
10
|
+
[ResultColumn.ConsonantsCount]: createKeyComparator('consonantsCount'),
|
|
11
11
|
[ResultColumn.Points]: createKeyComparator('points'),
|
|
12
|
-
[ResultColumn.TilesCount]: createKeyComparator('
|
|
13
|
-
[ResultColumn.VowelsCount]: createKeyComparator('
|
|
12
|
+
[ResultColumn.TilesCount]: createKeyComparator('tilesCount'),
|
|
13
|
+
[ResultColumn.VowelsCount]: createKeyComparator('vowelsCount'),
|
|
14
14
|
[ResultColumn.Word]: createKeyComparator('word'),
|
|
15
|
-
[ResultColumn.WordsCount]: createKeyComparator('
|
|
15
|
+
[ResultColumn.WordsCount]: createKeyComparator('wordsCount'),
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const sortResults = (
|
|
@@ -1,45 +1,69 @@
|
|
|
1
|
+
import { scrabble } from '@scrabble-solver/configs';
|
|
1
2
|
import logger from '@scrabble-solver/logger';
|
|
2
|
-
import { Locale } from '@scrabble-solver/types';
|
|
3
|
+
import { isLocale, Locale } from '@scrabble-solver/types';
|
|
3
4
|
import { getWordDefinition } from '@scrabble-solver/word-definitions';
|
|
4
5
|
import { NextApiRequest, NextApiResponse } from 'next';
|
|
5
6
|
|
|
6
|
-
import { getServerLoggingData
|
|
7
|
+
import { getServerLoggingData } from 'api';
|
|
7
8
|
|
|
8
9
|
interface RequestData {
|
|
9
10
|
locale: Locale;
|
|
10
|
-
|
|
11
|
+
words: string[];
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
const MAXIMUM_WORDS_COUNT = scrabble['en-US'].maximumCharactersCount;
|
|
15
|
+
|
|
13
16
|
const dictionary = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
|
|
14
17
|
const meta = getServerLoggingData(request);
|
|
15
18
|
|
|
16
19
|
try {
|
|
17
|
-
const { locale,
|
|
20
|
+
const { locale, words } = parseRequest(request);
|
|
21
|
+
|
|
18
22
|
logger.info('dictionary - request', {
|
|
19
23
|
meta,
|
|
20
24
|
payload: {
|
|
21
25
|
locale,
|
|
22
|
-
|
|
26
|
+
words,
|
|
23
27
|
},
|
|
24
28
|
});
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
|
|
30
|
+
const results = await Promise.all(words.map((word) => getWordDefinition(locale, word)));
|
|
31
|
+
response.status(200).send(results.map((result) => result.toJson()));
|
|
27
32
|
} catch (error) {
|
|
28
33
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
29
34
|
logger.error('dictionary - error', { error, meta });
|
|
30
35
|
response.status(500).send({ error: 'Server error', message });
|
|
36
|
+
throw error;
|
|
31
37
|
}
|
|
32
38
|
};
|
|
33
39
|
|
|
34
40
|
const parseRequest = (request: NextApiRequest): RequestData => {
|
|
35
41
|
const { locale, word } = request.query;
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
if (!isLocale(locale)) {
|
|
44
|
+
throw new Error('Invalid "locale" parameter');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof word !== 'string' || word.length === 0) {
|
|
48
|
+
throw new Error('Invalid "word" parameter');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const words = Array.from(
|
|
52
|
+
new Set(
|
|
53
|
+
word
|
|
54
|
+
.split(',')
|
|
55
|
+
.map((part) => part.trim())
|
|
56
|
+
.filter(Boolean),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (words.length > MAXIMUM_WORDS_COUNT) {
|
|
61
|
+
throw new Error('Invalid "word" parameter');
|
|
62
|
+
}
|
|
39
63
|
|
|
40
64
|
return {
|
|
41
|
-
locale
|
|
42
|
-
|
|
65
|
+
locale,
|
|
66
|
+
words,
|
|
43
67
|
};
|
|
44
68
|
};
|
|
45
69
|
|
package/src/pages/api/solve.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { getLocaleConfig } from '@scrabble-solver/configs';
|
|
1
|
+
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
5
|
import Solver from '@scrabble-solver/solver';
|
|
6
|
-
import { Board, Config, Locale, Tile } from '@scrabble-solver/types';
|
|
6
|
+
import { Board, Config, isBoardJson, isLocale, Locale, Tile } from '@scrabble-solver/types';
|
|
7
7
|
import { NextApiRequest, NextApiResponse } from 'next';
|
|
8
8
|
|
|
9
|
-
import { getServerLoggingData,
|
|
9
|
+
import { getServerLoggingData, isBoardValid } from 'api';
|
|
10
|
+
import { isStringArray } from 'lib';
|
|
10
11
|
|
|
11
12
|
interface RequestData {
|
|
12
13
|
board: Board;
|
|
@@ -20,6 +21,7 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
|
|
|
20
21
|
|
|
21
22
|
try {
|
|
22
23
|
const { board, characters, config, locale } = parseRequest(request);
|
|
24
|
+
|
|
23
25
|
logger.info('solve - request', {
|
|
24
26
|
meta,
|
|
25
27
|
payload: {
|
|
@@ -31,7 +33,7 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
|
|
|
31
33
|
locale,
|
|
32
34
|
},
|
|
33
35
|
});
|
|
34
|
-
|
|
36
|
+
|
|
35
37
|
const trie = await dictionaries.get(locale);
|
|
36
38
|
const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
|
|
37
39
|
const solver = new Solver(config, trie);
|
|
@@ -41,33 +43,51 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
|
|
|
41
43
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
42
44
|
logger.error('solve - error', { error, meta });
|
|
43
45
|
response.status(500).send({ error: 'Server error', message });
|
|
46
|
+
throw error;
|
|
44
47
|
}
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
const parseRequest = (request: NextApiRequest): RequestData => {
|
|
48
|
-
const { board, characters, configId, locale } = request.body;
|
|
51
|
+
const { board: boardJson, characters, configId, locale } = request.body;
|
|
52
|
+
|
|
53
|
+
if (!isLocale(locale)) {
|
|
54
|
+
throw new Error('Invalid "locale" parameter');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isConfigId(configId)) {
|
|
58
|
+
throw new Error('Invalid "configId" parameter');
|
|
59
|
+
}
|
|
49
60
|
|
|
50
|
-
validateConfigId(configId);
|
|
51
|
-
validateLocale(locale);
|
|
52
61
|
const config = getLocaleConfig(configId, locale);
|
|
53
|
-
validateBoard(board, config);
|
|
54
|
-
validateCharacters(characters, config);
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
+
if (!isBoardJson(boardJson) || !isBoardValid(boardJson, config)) {
|
|
64
|
+
throw new Error('Invalid "board" parameter');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!isStringArray(characters) || characters.length === 0) {
|
|
68
|
+
throw new Error('Invalid "characters" parameter');
|
|
69
|
+
}
|
|
63
70
|
|
|
64
|
-
const
|
|
71
|
+
for (const character of characters) {
|
|
72
|
+
if (!config.hasCharacter(character) && character !== BLANK) {
|
|
73
|
+
throw new Error('Invalid "characters" parameter');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const board = Board.fromJson(boardJson);
|
|
65
78
|
const blankTilesCount = characters.filter((character) => character === BLANK).length;
|
|
66
79
|
const blanksCount = board.getBlanksCount() + blankTilesCount;
|
|
67
80
|
|
|
68
|
-
if (blanksCount > config.
|
|
69
|
-
throw new Error(
|
|
81
|
+
if (blanksCount > config.blanksCount) {
|
|
82
|
+
throw new Error('Too many blank tiles passed');
|
|
70
83
|
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
board,
|
|
87
|
+
characters,
|
|
88
|
+
config,
|
|
89
|
+
locale,
|
|
90
|
+
};
|
|
71
91
|
};
|
|
72
92
|
|
|
73
93
|
export default solve;
|
package/src/pages/api/visit.ts
CHANGED
|
@@ -13,6 +13,7 @@ const visit = async (request: NextApiRequest, response: NextApiResponse): Promis
|
|
|
13
13
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
14
14
|
logger.error('visit - error', { error, meta });
|
|
15
15
|
response.status(500).send({ error: 'Server error', message });
|
|
16
|
+
throw error;
|
|
16
17
|
}
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -25,11 +25,15 @@
|
|
|
25
25
|
padding: var(--spacing--l);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
.
|
|
28
|
+
.navLogo {
|
|
29
29
|
display: flex;
|
|
30
30
|
flex: 1;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
.logoContainer {
|
|
34
|
+
display: flex;
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
.logo {
|
|
34
38
|
height: 60px;
|
|
35
39
|
user-select: none;
|
|
@@ -127,13 +131,3 @@
|
|
|
127
131
|
.submitInput {
|
|
128
132
|
display: none;
|
|
129
133
|
}
|
|
130
|
-
|
|
131
|
-
.version {
|
|
132
|
-
position: fixed;
|
|
133
|
-
bottom: var(--spacing--m);
|
|
134
|
-
left: 0;
|
|
135
|
-
right: 0;
|
|
136
|
-
font-size: var(--font--size--xs);
|
|
137
|
-
color: var(--color--background);
|
|
138
|
-
text-align: center;
|
|
139
|
-
}
|