@scrabble-solver/scrabble-solver 2.12.1 → 2.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +14 -14
- package/.next/cache/.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/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-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/required-server-files.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/chunks/131.js +478 -106
- package/.next/server/chunks/277.js +1202 -1013
- package/.next/server/chunks/44.js +36 -6
- package/.next/server/chunks/675.js +35 -13
- package/.next/server/chunks/859.js +21 -4
- package/.next/server/chunks/865.js +478 -106
- package/.next/server/chunks/911.js +894 -773
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/404.js.nft.json +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages/_app.js +3 -1
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/api/dictionary/[locale]/[word].js +4 -2
- package/.next/server/pages/api/solve.js +51 -20
- package/.next/server/pages/api/verify.js +8 -5
- package/.next/server/pages/index.html +1 -1
- package/.next/server/pages/index.js +55 -65
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/server/pages-manifest.json +2 -2
- package/.next/static/chunks/{framework-2c5cac93e8c637b5.js → framework-2c29dc3cd933590b.js} +2 -2
- package/.next/static/chunks/main-4dcb7f9b52833aba.js +1 -0
- package/.next/static/chunks/pages/_app-e7f3d1c9c09c8f91.js +32 -0
- package/.next/static/chunks/pages/index-82b2939158c7729f.js +1 -0
- package/.next/static/css/4e8b47fe382a8a8f.css +2 -0
- package/.next/static/css/cfae5256f1689f57.css +1 -0
- package/.next/static/{_yG9K-PD5kWT_p4sMdCSV → wNIKOJJzkMSJYb2nOL21o}/_buildManifest.js +1 -1
- package/.next/trace +51 -52
- package/package.json +14 -14
- package/src/components/Board/Board.module.scss +33 -0
- package/src/components/Board/Board.tsx +3 -0
- package/src/components/Board/BoardPure.tsx +25 -1
- package/src/components/Board/components/Cell/Cell.module.scss +4 -12
- package/src/components/Board/components/Cell/Cell.tsx +4 -0
- package/src/components/Board/components/InputPrompt/InputPrompt.tsx +1 -0
- package/src/components/Board/hooks/useBackgroundImage.tsx +24 -6
- package/src/components/PlainTiles/lib/createPlainTile.ts +4 -4
- package/src/components/Rack/components/InputPrompt/InputPrompt.tsx +1 -0
- package/src/components/Radio/Radio.module.scss +4 -0
- package/src/components/Radio/Radio.tsx +1 -0
- package/src/components/SeoMessage/SeoMessage.tsx +3 -3
- package/src/hooks/useAppLayout.ts +1 -2
- package/src/hooks/useLocalStorage.ts +5 -5
- package/src/i18n/constants.ts +31 -61
- package/src/lib/extractCharacters.test.ts +3 -3
- package/src/lib/extractCharactersByCase.test.ts +3 -3
- package/src/lib/getCellSize.ts +2 -2
- package/src/modals/MenuModal/MenuModal.module.scss +0 -1
- package/src/modals/MenuModal/MenuModal.tsx +3 -3
- package/src/modals/SettingsModal/SettingsModal.tsx +7 -3
- package/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.tsx +16 -7
- package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss +1 -4
- package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +8 -8
- package/src/pages/_app.tsx +3 -1
- package/src/pages/api/dictionary/[locale]/[word].ts +6 -3
- package/src/pages/api/solve.ts +11 -7
- package/src/pages/api/verify.ts +11 -7
- package/src/pages/index.module.scss +0 -1
- package/src/parameters/index.ts +4 -6
- package/src/sdk/solve.ts +3 -3
- package/src/sdk/verify.ts +3 -3
- package/src/service-worker/routeSolveRequests.ts +3 -3
- package/src/state/localStorage.ts +6 -6
- package/src/state/sagas.ts +18 -7
- package/src/state/selectors.ts +10 -10
- package/src/state/slices/boardInitialState.ts +3 -3
- package/src/state/slices/boardSlice.ts +20 -6
- package/src/state/slices/settingsInitialState.ts +3 -4
- package/src/state/slices/settingsSlice.ts +5 -5
- package/src/styles/variables.scss +0 -9
- package/.next/static/chunks/main-0ecb9ccfcb6c9b24.js +0 -1
- package/.next/static/chunks/pages/_app-bea4539a6b8042de.js +0 -32
- package/.next/static/chunks/pages/index-4e8566409753e1c3.js +0 -1
- package/.next/static/css/58053f9594647860.css +0 -2
- package/.next/static/css/60e8258da7362a1a.css +0 -1
- package/src/i18n/i18n.module.scss +0 -27
- package/src/modals/SettingsModal/components/ConfigSetting/options.ts +0 -19
- package/src/modals/SettingsModal/components/ConfigSetting/types.ts +0 -9
- package/src/modals/SettingsModal/components/InputModeSetting/types.ts +0 -7
- package/src/modals/SettingsModal/components/LocaleSetting/types.ts +0 -9
- /package/.next/static/{_yG9K-PD5kWT_p4sMdCSV → wNIKOJJzkMSJYb2nOL21o}/_ssgManifest.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FunctionComponent, memo } from 'react';
|
|
2
2
|
|
|
3
3
|
import { Button, Modal } from 'components';
|
|
4
|
-
import {
|
|
4
|
+
import { LOCALE_FEATURES } from 'i18n';
|
|
5
5
|
import { BookHalf, CardChecklist, Cog, Github, Sack } from 'icons';
|
|
6
6
|
import { GITHUB_PROJECT_URL } from 'parameters';
|
|
7
7
|
import { selectLocale, useTranslate, useTypedSelector } from 'state';
|
|
@@ -29,7 +29,7 @@ const MenuModal: FunctionComponent<Props> = ({
|
|
|
29
29
|
}) => {
|
|
30
30
|
const translate = useTranslate();
|
|
31
31
|
const locale = useTypedSelector(selectLocale);
|
|
32
|
-
const
|
|
32
|
+
const { Icon } = LOCALE_FEATURES[locale];
|
|
33
33
|
|
|
34
34
|
return (
|
|
35
35
|
<Modal className={className} isOpen={isOpen} title={translate('menu')} onClose={onClose}>
|
|
@@ -64,7 +64,7 @@ const MenuModal: FunctionComponent<Props> = ({
|
|
|
64
64
|
<Button aria-label={translate('settings')} className={styles.button} Icon={Cog} onClick={onShowSettings}>
|
|
65
65
|
<div className={styles.settings}>
|
|
66
66
|
<div className={styles.settingsLabel}>{translate('settings')}</div>
|
|
67
|
-
<
|
|
67
|
+
<Icon className={styles.flag} />
|
|
68
68
|
</div>
|
|
69
69
|
</Button>
|
|
70
70
|
</Modal>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FunctionComponent, memo } from 'react';
|
|
2
2
|
|
|
3
3
|
import { Modal } from 'components';
|
|
4
|
+
import { useIsTouchDevice } from 'hooks';
|
|
4
5
|
import { useTranslate } from 'state';
|
|
5
6
|
|
|
6
7
|
import { AutoGroupTilesSetting, ConfigSetting, InputModeSetting, LocaleSetting } from './components';
|
|
@@ -13,6 +14,7 @@ interface Props {
|
|
|
13
14
|
|
|
14
15
|
const SettingsModal: FunctionComponent<Props> = ({ className, isOpen, onClose }) => {
|
|
15
16
|
const translate = useTranslate();
|
|
17
|
+
const isTouchDevice = useIsTouchDevice();
|
|
16
18
|
|
|
17
19
|
return (
|
|
18
20
|
<Modal className={className} isOpen={isOpen} title={translate('settings')} onClose={onClose}>
|
|
@@ -24,9 +26,11 @@ const SettingsModal: FunctionComponent<Props> = ({ className, isOpen, onClose })
|
|
|
24
26
|
<LocaleSetting disabled={!isOpen} />
|
|
25
27
|
</Modal.Section>
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
29
|
+
{!isTouchDevice && (
|
|
30
|
+
<Modal.Section title={translate('settings.inputMode')}>
|
|
31
|
+
<InputModeSetting disabled={!isOpen} />
|
|
32
|
+
</Modal.Section>
|
|
33
|
+
)}
|
|
30
34
|
|
|
31
35
|
<Modal.Section title={translate('settings.autoGroupTiles')}>
|
|
32
36
|
<AutoGroupTilesSetting disabled={!isOpen} />
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { games, hasConfig } from '@scrabble-solver/configs';
|
|
2
|
+
import { isGame } from '@scrabble-solver/types';
|
|
1
3
|
import { ChangeEvent, FunctionComponent } from 'react';
|
|
2
4
|
import { useDispatch } from 'react-redux';
|
|
3
5
|
|
|
4
6
|
import { Radio } from 'components';
|
|
5
|
-
import {
|
|
7
|
+
import { selectGame, selectLocale, settingsSlice, useTypedSelector } from 'state';
|
|
6
8
|
|
|
7
9
|
import styles from './ConfigSetting.module.scss';
|
|
8
|
-
import options from './options';
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
className?: string;
|
|
@@ -14,21 +15,29 @@ interface Props {
|
|
|
14
15
|
|
|
15
16
|
const ConfigSetting: FunctionComponent<Props> = ({ className, disabled }) => {
|
|
16
17
|
const dispatch = useDispatch();
|
|
17
|
-
const
|
|
18
|
+
const game = useTypedSelector(selectGame);
|
|
19
|
+
const locale = useTypedSelector(selectLocale);
|
|
20
|
+
const options = Object.values(games).map((gameConfig) => ({
|
|
21
|
+
disabled: !hasConfig(gameConfig.game, locale),
|
|
22
|
+
label: gameConfig.name,
|
|
23
|
+
value: gameConfig.game,
|
|
24
|
+
}));
|
|
18
25
|
|
|
19
26
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
20
|
-
|
|
27
|
+
if (isGame(event.target.value)) {
|
|
28
|
+
dispatch(settingsSlice.actions.changeGame(event.target.value));
|
|
29
|
+
}
|
|
21
30
|
};
|
|
22
31
|
|
|
23
32
|
return (
|
|
24
33
|
<div className={className}>
|
|
25
34
|
{options.map((option) => (
|
|
26
35
|
<Radio
|
|
27
|
-
checked={
|
|
36
|
+
checked={game === option.value}
|
|
28
37
|
className={styles.option}
|
|
29
|
-
disabled={disabled}
|
|
38
|
+
disabled={disabled || option.disabled}
|
|
30
39
|
key={option.value}
|
|
31
|
-
name="
|
|
40
|
+
name="game"
|
|
32
41
|
value={option.value}
|
|
33
42
|
onChange={handleChange}
|
|
34
43
|
>
|
|
@@ -4,7 +4,7 @@ import { ChangeEvent, FunctionComponent } from 'react';
|
|
|
4
4
|
import { useDispatch } from 'react-redux';
|
|
5
5
|
|
|
6
6
|
import { Radio } from 'components';
|
|
7
|
-
import {
|
|
7
|
+
import { LOCALE_FEATURES } from 'i18n';
|
|
8
8
|
import { selectLocale, settingsSlice, useTypedSelector } from 'state';
|
|
9
9
|
|
|
10
10
|
import styles from './LocaleSetting.module.scss';
|
|
@@ -14,7 +14,7 @@ interface Props {
|
|
|
14
14
|
disabled: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const OPTIONS = Object.values(
|
|
17
|
+
const OPTIONS = Object.values(LOCALE_FEATURES).sort((a, b) => a.name.localeCompare(b.name));
|
|
18
18
|
|
|
19
19
|
const LocaleSetting: FunctionComponent<Props> = ({ className, disabled }) => {
|
|
20
20
|
const dispatch = useDispatch();
|
|
@@ -29,18 +29,18 @@ const LocaleSetting: FunctionComponent<Props> = ({ className, disabled }) => {
|
|
|
29
29
|
<div className={className}>
|
|
30
30
|
{OPTIONS.map(({ Icon, ...option }) => (
|
|
31
31
|
<Radio
|
|
32
|
-
checked={locale === option.
|
|
32
|
+
checked={locale === option.locale}
|
|
33
33
|
className={classNames(styles.option, className, {
|
|
34
|
-
[styles.checked]: locale === option.
|
|
34
|
+
[styles.checked]: locale === option.locale,
|
|
35
35
|
})}
|
|
36
36
|
disabled={disabled}
|
|
37
|
-
key={option.
|
|
37
|
+
key={option.locale}
|
|
38
38
|
name="locale"
|
|
39
|
-
value={option.
|
|
39
|
+
value={option.locale}
|
|
40
40
|
onChange={handleChange}
|
|
41
41
|
>
|
|
42
|
-
<span className={
|
|
43
|
-
<Icon className={
|
|
42
|
+
<span className={styles.label}>
|
|
43
|
+
<Icon className={styles.flag} />
|
|
44
44
|
|
|
45
45
|
<span>{option.label}</span>
|
|
46
46
|
</span>
|
package/src/pages/_app.tsx
CHANGED
|
@@ -10,12 +10,14 @@ import 'styles/global.scss';
|
|
|
10
10
|
|
|
11
11
|
const DESCRIPTION =
|
|
12
12
|
// eslint-disable-next-line max-len
|
|
13
|
-
'Scrabble Solver 2 - Free and open-source analysis tool for Scrabble
|
|
13
|
+
'Scrabble Solver 2 - Free and open-source analysis tool for Scrabble, Super Scrabble & Literaki. Quickly find top scoring words using given letters and board state. Available in English, French, German, Polish & Spanish.';
|
|
14
14
|
|
|
15
15
|
const KEYWORDS = [
|
|
16
16
|
'Scrabble Solver',
|
|
17
17
|
'Scrabble',
|
|
18
18
|
'Solver',
|
|
19
|
+
'Super Scrabble',
|
|
20
|
+
'Super Scrabble Solver',
|
|
19
21
|
'Board',
|
|
20
22
|
'Open-source',
|
|
21
23
|
'Open',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { games } from '@scrabble-solver/configs';
|
|
2
2
|
import { COMMA_ARABIC, COMMA_LATIN } from '@scrabble-solver/constants';
|
|
3
3
|
import { dictionaries } from '@scrabble-solver/dictionaries';
|
|
4
4
|
import logger from '@scrabble-solver/logger';
|
|
5
|
-
import {
|
|
5
|
+
import { Locale, isLocale } from '@scrabble-solver/types';
|
|
6
6
|
import { getWordDefinition } from '@scrabble-solver/word-definitions';
|
|
7
7
|
import { NextApiRequest, NextApiResponse } from 'next';
|
|
8
8
|
|
|
@@ -13,7 +13,10 @@ interface RequestData {
|
|
|
13
13
|
words: string[];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const MAXIMUM_COLLISIONS_COUNT =
|
|
16
|
+
const MAXIMUM_COLLISIONS_COUNT = Object.values(games).reduce(
|
|
17
|
+
(result, game) => Math.max(result, game.maximumCharactersCount),
|
|
18
|
+
0,
|
|
19
|
+
);
|
|
17
20
|
const MAXIMUM_WORDS_COUNT = MAXIMUM_COLLISIONS_COUNT + 1;
|
|
18
21
|
|
|
19
22
|
const dictionary = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
|
package/src/pages/api/solve.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getConfig, hasConfig } 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 { solve as solveScrabble } from '@scrabble-solver/solver';
|
|
6
|
-
import { Board, Config,
|
|
6
|
+
import { Board, Config, Locale, Tile, isBoardJson, isGame, isLocale } from '@scrabble-solver/types';
|
|
7
7
|
import { NextApiRequest, NextApiResponse } from 'next';
|
|
8
8
|
|
|
9
9
|
import { getServerLoggingData, isBoardValid, isCharacterValid } from 'api';
|
|
@@ -29,7 +29,7 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
|
|
|
29
29
|
boardBlanksCount: board.getBlanksCount(),
|
|
30
30
|
boardTilesCount: board.getTilesCount(),
|
|
31
31
|
characters: characters.join(''),
|
|
32
|
-
|
|
32
|
+
game: request.body.game,
|
|
33
33
|
locale,
|
|
34
34
|
},
|
|
35
35
|
});
|
|
@@ -46,21 +46,25 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
const parseRequest = (request: NextApiRequest): RequestData => {
|
|
49
|
-
const { board: boardJson, characters,
|
|
49
|
+
const { board: boardJson, characters, game, locale } = request.body;
|
|
50
50
|
|
|
51
51
|
if (!isLocale(locale)) {
|
|
52
52
|
throw new Error('Invalid "locale" parameter');
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
if (!
|
|
56
|
-
throw new Error('Invalid "
|
|
55
|
+
if (!isGame(game)) {
|
|
56
|
+
throw new Error('Invalid "game" parameter');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
if (!isStringArray(characters) || characters.length === 0) {
|
|
60
60
|
throw new Error('Invalid "characters" parameter');
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
if (!hasConfig(game, locale)) {
|
|
64
|
+
throw new Error(`No game "${game}" in "${locale}"`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const config = getConfig(game, locale);
|
|
64
68
|
|
|
65
69
|
for (const character of characters) {
|
|
66
70
|
if (!isCharacterValid(character)) {
|
package/src/pages/api/verify.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getConfig, hasConfig } from '@scrabble-solver/configs';
|
|
2
2
|
import { dictionaries } from '@scrabble-solver/dictionaries';
|
|
3
3
|
import logger from '@scrabble-solver/logger';
|
|
4
|
-
import { Board, Config, isBoardJson,
|
|
4
|
+
import { Board, Config, Locale, isBoardJson, isGame, isLocale } from '@scrabble-solver/types';
|
|
5
5
|
import { NextApiRequest, NextApiResponse } from 'next';
|
|
6
6
|
|
|
7
7
|
import { getServerLoggingData, isBoardValid } from 'api';
|
|
@@ -24,7 +24,7 @@ const verify = async (request: NextApiRequest, response: NextApiResponse): Promi
|
|
|
24
24
|
board: board.toString(),
|
|
25
25
|
boardBlanksCount: board.getBlanksCount(),
|
|
26
26
|
boardTilesCount: board.getTilesCount(),
|
|
27
|
-
|
|
27
|
+
game: request.body.game,
|
|
28
28
|
locale,
|
|
29
29
|
},
|
|
30
30
|
});
|
|
@@ -42,17 +42,21 @@ const verify = async (request: NextApiRequest, response: NextApiResponse): Promi
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
const parseRequest = (request: NextApiRequest): RequestData => {
|
|
45
|
-
const { board: boardJson,
|
|
45
|
+
const { board: boardJson, game, locale } = request.body;
|
|
46
46
|
|
|
47
47
|
if (!isLocale(locale)) {
|
|
48
48
|
throw new Error('Invalid "locale" parameter');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
if (!
|
|
52
|
-
throw new Error('Invalid "
|
|
51
|
+
if (!isGame(game)) {
|
|
52
|
+
throw new Error('Invalid "game" parameter');
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
if (!hasConfig(game, locale)) {
|
|
56
|
+
throw new Error(`No game "${game}" in "${locale}"`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const config = getConfig(game, locale);
|
|
56
60
|
|
|
57
61
|
if (!isBoardJson(boardJson) || !isBoardValid(boardJson, config)) {
|
|
58
62
|
throw new Error('Invalid "board" parameter');
|
package/src/parameters/index.ts
CHANGED
|
@@ -25,9 +25,11 @@ export const COLOR_BONUS_CHARACTER_3 = '#dde4f6';
|
|
|
25
25
|
export const COLOR_BONUS_CHARACTER_5 = '#fbe0d4';
|
|
26
26
|
export const COLOR_BONUS_CHARACTER_MULTIPLIER_2 = '#b8d5ed';
|
|
27
27
|
export const COLOR_BONUS_CHARACTER_MULTIPLIER_3 = '#86aed1';
|
|
28
|
+
export const COLOR_BONUS_CHARACTER_MULTIPLIER_4 = '#3477b2';
|
|
28
29
|
export const COLOR_BONUS_START = '#b284b8';
|
|
29
30
|
export const COLOR_BONUS_WORD_MULTIPLIER_2 = '#fbc997';
|
|
30
31
|
export const COLOR_BONUS_WORD_MULTIPLIER_3 = '#f19393';
|
|
32
|
+
export const COLOR_BONUS_WORD_MULTIPLIER_4 = '#ed5e5e';
|
|
31
33
|
export const COLOR_FILTERED = '#444';
|
|
32
34
|
|
|
33
35
|
export const COLOR_BONUS_CHARACTER: Record<number, string> = {
|
|
@@ -40,11 +42,13 @@ export const COLOR_BONUS_CHARACTER: Record<number, string> = {
|
|
|
40
42
|
export const COLOR_BONUS_CHARACTER_MULTIPLIER: Record<number, string> = {
|
|
41
43
|
2: COLOR_BONUS_CHARACTER_MULTIPLIER_2,
|
|
42
44
|
3: COLOR_BONUS_CHARACTER_MULTIPLIER_3,
|
|
45
|
+
4: COLOR_BONUS_CHARACTER_MULTIPLIER_4,
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
export const COLOR_BONUS_WORD: Record<number, string> = {
|
|
46
49
|
2: COLOR_BONUS_WORD_MULTIPLIER_2,
|
|
47
50
|
3: COLOR_BONUS_WORD_MULTIPLIER_3,
|
|
51
|
+
4: COLOR_BONUS_WORD_MULTIPLIER_4,
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
export const SPACING_XS = 2;
|
|
@@ -61,12 +65,6 @@ export const BOARD_CELL_BORDER_WIDTH = 1;
|
|
|
61
65
|
export const BOARD_TILE_FONT_SIZE_MIN = 14;
|
|
62
66
|
export const BOARD_TILE_FONT_SIZE_POINTS_MIN = 10;
|
|
63
67
|
export const BOARD_TILE_SIZE_MAX = 64;
|
|
64
|
-
/**
|
|
65
|
-
* 20 - fits all board tiles without horizontal scrollbar on 360px viewport width (font-size: 14px)
|
|
66
|
-
* 21 - fits all board tiles without horizontal scrollbar on 375px viewport width (font-size: 14px)
|
|
67
|
-
* 26 - tiles start to look good (font-size: 16px)
|
|
68
|
-
*/
|
|
69
|
-
export const BOARD_TILE_SIZE_MIN = 20;
|
|
70
68
|
|
|
71
69
|
export const BORDER_COLOR = '#cdcdcd';
|
|
72
70
|
export const BORDER_COLOR_LIGHT = '#d9d9d9';
|
package/src/sdk/solve.ts
CHANGED
|
@@ -5,14 +5,14 @@ import fetchJson from './fetchJson';
|
|
|
5
5
|
interface Payload {
|
|
6
6
|
board: BoardJson;
|
|
7
7
|
characters: string[];
|
|
8
|
-
|
|
8
|
+
game: string;
|
|
9
9
|
locale: Locale;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const solve = async ({ board, characters,
|
|
12
|
+
const solve = async ({ board, characters, game, locale }: Payload): Promise<Result[]> => {
|
|
13
13
|
const json = await fetchJson<ResultJson[]>('/api/solve', {
|
|
14
14
|
method: 'POST',
|
|
15
|
-
body: JSON.stringify({ board, characters,
|
|
15
|
+
body: JSON.stringify({ board, characters, game, locale }),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
return json.map(Result.fromJson);
|
package/src/sdk/verify.ts
CHANGED
|
@@ -4,7 +4,7 @@ import fetchJson from './fetchJson';
|
|
|
4
4
|
|
|
5
5
|
interface Payload {
|
|
6
6
|
board: BoardJson;
|
|
7
|
-
|
|
7
|
+
game: string;
|
|
8
8
|
locale: Locale;
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -13,10 +13,10 @@ interface Response {
|
|
|
13
13
|
validWords: string[];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const verify = async ({ board,
|
|
16
|
+
const verify = async ({ board, game, locale }: Payload): Promise<Response> => {
|
|
17
17
|
return fetchJson<Response>('/api/verify', {
|
|
18
18
|
method: 'POST',
|
|
19
|
-
body: JSON.stringify({ board,
|
|
19
|
+
body: JSON.stringify({ board, game, locale }),
|
|
20
20
|
});
|
|
21
21
|
};
|
|
22
22
|
|
|
@@ -2,7 +2,7 @@ import { Trie } from '@kamilmielnik/trie';
|
|
|
2
2
|
import { getConfig } from '@scrabble-solver/configs';
|
|
3
3
|
import { BLANK } from '@scrabble-solver/constants';
|
|
4
4
|
import { solve } from '@scrabble-solver/solver';
|
|
5
|
-
import { Board,
|
|
5
|
+
import { Board, Tile } from '@scrabble-solver/types';
|
|
6
6
|
import { registerRoute } from 'workbox-routing';
|
|
7
7
|
|
|
8
8
|
import average from './average';
|
|
@@ -33,10 +33,10 @@ const routeSolveRequests = () => {
|
|
|
33
33
|
registerRoute(
|
|
34
34
|
({ url }) => url.origin === location.origin && url.pathname === '/api/solve',
|
|
35
35
|
async ({ request }) => {
|
|
36
|
-
const { board, characters,
|
|
36
|
+
const { board, characters, game, locale } = await request.clone().json();
|
|
37
37
|
|
|
38
38
|
const solveLocal = async (trie: Trie) => {
|
|
39
|
-
const config = getConfig(
|
|
39
|
+
const config = getConfig(game, locale);
|
|
40
40
|
const tiles = characters.map((character: string) => new Tile({ character, isBlank: character === BLANK }));
|
|
41
41
|
const resultsJson = solve(trie, config, Board.fromJson(board), tiles);
|
|
42
42
|
const json = JSON.stringify(resultsJson);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Board, Locale } from '@scrabble-solver/types';
|
|
1
|
+
import { Board, Game, Locale } from '@scrabble-solver/types';
|
|
2
2
|
import store2 from 'store2';
|
|
3
3
|
|
|
4
4
|
import { AutoGroupTiles, InputMode, Rack } from 'types';
|
|
5
5
|
|
|
6
6
|
const AUTO_GROUP_TILES = 'auto-group-tiles';
|
|
7
7
|
const BOARD = 'board';
|
|
8
|
-
const
|
|
8
|
+
const GAME_ID = 'config-id';
|
|
9
9
|
const INPUT_MODE = 'input-mode';
|
|
10
10
|
const LOCALE = 'locale';
|
|
11
11
|
const RACK = 'rack';
|
|
@@ -31,12 +31,12 @@ const localStorage = {
|
|
|
31
31
|
store.set(BOARD, serialized, true);
|
|
32
32
|
},
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
return store.get(
|
|
34
|
+
getGame(): Game | undefined {
|
|
35
|
+
return store.get(GAME_ID);
|
|
36
36
|
},
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
store.set(
|
|
38
|
+
setGame(game: Game | undefined): void {
|
|
39
|
+
store.set(GAME_ID, game, true);
|
|
40
40
|
},
|
|
41
41
|
|
|
42
42
|
getInputMode(): InputMode | undefined {
|
package/src/state/sagas.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
|
|
3
3
|
import { PayloadAction } from '@reduxjs/toolkit';
|
|
4
|
-
import {
|
|
4
|
+
import { hasConfig, localesMap } from '@scrabble-solver/configs';
|
|
5
|
+
import { Board, Locale, Result } from '@scrabble-solver/types';
|
|
5
6
|
import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
|
|
6
7
|
|
|
7
8
|
import { LOCALE_FEATURES } from 'i18n';
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
selectCharacters,
|
|
16
17
|
selectConfig,
|
|
17
18
|
selectDictionary,
|
|
19
|
+
selectGame,
|
|
18
20
|
selectLocale,
|
|
19
21
|
selectLocaleAutoGroupTiles,
|
|
20
22
|
selectRack,
|
|
@@ -44,7 +46,7 @@ export function* rootSaga(): AnyGenerator {
|
|
|
44
46
|
yield takeEvery([rackSlice.actions.changeCharacter.type, rackSlice.actions.changeCharacters.type], onRackValueChange);
|
|
45
47
|
yield takeEvery(resultsSlice.actions.applyResult.type, onApplyResult);
|
|
46
48
|
yield takeEvery(resultsSlice.actions.changeResultCandidate.type, onResultCandidateChange);
|
|
47
|
-
yield takeEvery(settingsSlice.actions.
|
|
49
|
+
yield takeEvery(settingsSlice.actions.changeGame.type, onGameChange);
|
|
48
50
|
yield takeEvery(settingsSlice.actions.changeLocale.type, onLocaleChange);
|
|
49
51
|
yield takeLatest(dictionarySlice.actions.submit.type, onDictionarySubmit);
|
|
50
52
|
yield takeLatest(initialize.type, onInitialize);
|
|
@@ -77,7 +79,7 @@ function* onApplyResult({ payload: result }: PayloadAction<Result>): AnyGenerato
|
|
|
77
79
|
yield put(verifySlice.actions.submit());
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
function*
|
|
82
|
+
function* onGameChange(): AnyGenerator {
|
|
81
83
|
const characters = yield select(selectCharacters);
|
|
82
84
|
|
|
83
85
|
if (characters.length > 0) {
|
|
@@ -119,7 +121,9 @@ function* onInitialize(): AnyGenerator {
|
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
function* onReset(): AnyGenerator {
|
|
122
|
-
yield
|
|
124
|
+
const config = yield select(selectConfig);
|
|
125
|
+
|
|
126
|
+
yield put(boardSlice.actions.init(Board.create(config.boardWidth, config.boardHeight)));
|
|
123
127
|
yield put(cellFilterSlice.actions.reset());
|
|
124
128
|
yield put(dictionarySlice.actions.reset());
|
|
125
129
|
yield put(rackSlice.actions.reset());
|
|
@@ -128,7 +132,14 @@ function* onReset(): AnyGenerator {
|
|
|
128
132
|
yield put(verifySlice.actions.submit());
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
function* onLocaleChange(): AnyGenerator {
|
|
135
|
+
function* onLocaleChange({ payload: locale }: PayloadAction<Locale>): AnyGenerator {
|
|
136
|
+
const game = yield select(selectGame);
|
|
137
|
+
|
|
138
|
+
if (!hasConfig(game, locale)) {
|
|
139
|
+
const defaultConfig = localesMap[locale][0];
|
|
140
|
+
yield put(settingsSlice.actions.changeGame(defaultConfig.game));
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
const characters = yield select(selectCharacters);
|
|
133
144
|
|
|
134
145
|
if (characters.length > 0) {
|
|
@@ -166,7 +177,7 @@ function* onSolve(): AnyGenerator {
|
|
|
166
177
|
const results = yield call(solve, {
|
|
167
178
|
board: board.toJson(),
|
|
168
179
|
characters,
|
|
169
|
-
|
|
180
|
+
game: config.game,
|
|
170
181
|
locale,
|
|
171
182
|
});
|
|
172
183
|
yield put(resultsSlice.actions.changeResults(results));
|
|
@@ -187,7 +198,7 @@ function* onVerify(): AnyGenerator {
|
|
|
187
198
|
try {
|
|
188
199
|
const { invalidWords, validWords } = yield call(verify, {
|
|
189
200
|
board: board.toJson(),
|
|
190
|
-
|
|
201
|
+
game: config.game,
|
|
191
202
|
locale,
|
|
192
203
|
});
|
|
193
204
|
yield put(verifySlice.actions.submitSuccess({ board, invalidWords, validWords }));
|
package/src/state/selectors.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
|
|
3
3
|
import { createSelector } from '@reduxjs/toolkit';
|
|
4
|
-
import {
|
|
4
|
+
import { getConfig } from '@scrabble-solver/configs';
|
|
5
5
|
import { BLANK } from '@scrabble-solver/constants';
|
|
6
6
|
import { Cell, Config, isError, Tile } from '@scrabble-solver/types';
|
|
7
7
|
|
|
@@ -67,13 +67,13 @@ export const selectBoard = selectBoardRoot;
|
|
|
67
67
|
|
|
68
68
|
export const selectInputMode = createSelector([selectSettingsRoot], (settings) => settings.inputMode);
|
|
69
69
|
|
|
70
|
-
export const
|
|
70
|
+
export const selectGame = createSelector([selectSettingsRoot], (settings) => settings.game);
|
|
71
71
|
|
|
72
|
-
export const selectConfig = createSelector([
|
|
72
|
+
export const selectConfig = createSelector([selectGame, selectLocale], getConfig);
|
|
73
73
|
|
|
74
|
-
export const
|
|
74
|
+
export const selectFilteredCells = selectCellFilterRoot;
|
|
75
75
|
|
|
76
|
-
export const selectCellIsFiltered = createSelector([
|
|
76
|
+
export const selectCellIsFiltered = createSelector([selectFilteredCells, selectPoint], (cellFilter, { x, y }) => {
|
|
77
77
|
return cellFilter.some((cell) => cell.x === x && cell.y === y);
|
|
78
78
|
});
|
|
79
79
|
|
|
@@ -94,7 +94,7 @@ export const selectResultsSort = createSelector([selectResultsRoot], (results) =
|
|
|
94
94
|
export const selectSortedResults = createSelector([selectResultsRaw, selectResultsSort, selectLocale], sortResults);
|
|
95
95
|
|
|
96
96
|
export const selectGroupedResults = createSelector(
|
|
97
|
-
[selectSortedResults, selectResultsQuery,
|
|
97
|
+
[selectSortedResults, selectResultsQuery, selectFilteredCells],
|
|
98
98
|
groupResults,
|
|
99
99
|
);
|
|
100
100
|
|
|
@@ -103,8 +103,8 @@ export const selectResults = createSelector([selectGroupedResults], (groupedResu
|
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
export const selectIsResultMatching = createSelector(
|
|
106
|
-
[selectResults, selectResultsQuery,
|
|
107
|
-
(results, query,
|
|
106
|
+
[selectResults, selectResultsQuery, selectFilteredCells, selectResultIndex],
|
|
107
|
+
(results, query, filteredCells, index) => {
|
|
108
108
|
if (!results) {
|
|
109
109
|
return false;
|
|
110
110
|
}
|
|
@@ -116,8 +116,8 @@ export const selectIsResultMatching = createSelector(
|
|
|
116
116
|
return false;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
if (
|
|
120
|
-
return
|
|
119
|
+
if (filteredCells) {
|
|
120
|
+
return filteredCells.every(({ x, y }) => result.cells.some((cell) => cell.x === x && cell.y === y));
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
return true;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getConfig } from '@scrabble-solver/configs';
|
|
2
2
|
import { Board } from '@scrabble-solver/types';
|
|
3
3
|
|
|
4
4
|
import localStorage from '../localStorage';
|
|
@@ -7,8 +7,8 @@ import settingsInitialState from './settingsInitialState';
|
|
|
7
7
|
|
|
8
8
|
export type BoardState = Board;
|
|
9
9
|
|
|
10
|
-
const {
|
|
11
|
-
const { boardHeight, boardWidth } =
|
|
10
|
+
const { game, locale } = settingsInitialState;
|
|
11
|
+
const { boardHeight, boardWidth } = getConfig(game, locale);
|
|
12
12
|
export const boardDefaultState = Board.create(boardWidth, boardHeight);
|
|
13
13
|
|
|
14
14
|
const boardInitialState: BoardState = localStorage.getBoard() || boardDefaultState;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
2
|
+
import { games } from '@scrabble-solver/configs';
|
|
2
3
|
import { EMPTY_CELL } from '@scrabble-solver/constants';
|
|
3
|
-
import { Board, Cell, Result, Tile } from '@scrabble-solver/types';
|
|
4
|
+
import { Board, Cell, Game, Result, Tile } from '@scrabble-solver/types';
|
|
4
5
|
|
|
5
|
-
import boardInitialState
|
|
6
|
+
import boardInitialState from './boardInitialState';
|
|
7
|
+
import settingsSlice from './settingsSlice';
|
|
6
8
|
|
|
7
9
|
const boardSlice = createSlice({
|
|
8
10
|
initialState: boardInitialState,
|
|
@@ -42,10 +44,6 @@ const boardSlice = createSlice({
|
|
|
42
44
|
return board;
|
|
43
45
|
},
|
|
44
46
|
|
|
45
|
-
reset: () => {
|
|
46
|
-
return boardDefaultState;
|
|
47
|
-
},
|
|
48
|
-
|
|
49
47
|
toggleCellIsBlank: (state, action: PayloadAction<{ x: number; y: number }>) => {
|
|
50
48
|
const newBoard = state.clone();
|
|
51
49
|
const { x, y } = action.payload;
|
|
@@ -58,6 +56,22 @@ const boardSlice = createSlice({
|
|
|
58
56
|
return newBoard;
|
|
59
57
|
},
|
|
60
58
|
},
|
|
59
|
+
extraReducers: {
|
|
60
|
+
[settingsSlice.actions.changeGame.type]: (state, action: PayloadAction<Game>) => {
|
|
61
|
+
const game = action.payload;
|
|
62
|
+
const config = Object.values(games).find((gameConfig) => gameConfig.game === game);
|
|
63
|
+
|
|
64
|
+
if (!config) {
|
|
65
|
+
throw new Error(`Cannot find config for game "${game}"`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (state.rows.length !== config.boardHeight || state.rows[0].length !== config.boardWidth) {
|
|
69
|
+
return Board.create(config.boardWidth, config.boardHeight);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return state;
|
|
73
|
+
},
|
|
74
|
+
},
|
|
61
75
|
});
|
|
62
76
|
|
|
63
77
|
export default boardSlice;
|