@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.
Files changed (97) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +14 -14
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/next-server.js.nft.json +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  9. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  10. package/.next/cache/webpack/server-production/0.pack +0 -0
  11. package/.next/cache/webpack/server-production/index.pack +0 -0
  12. package/.next/next-server.js.nft.json +1 -1
  13. package/.next/prerender-manifest.json +1 -1
  14. package/.next/required-server-files.json +1 -1
  15. package/.next/routes-manifest.json +1 -1
  16. package/.next/server/chunks/131.js +478 -106
  17. package/.next/server/chunks/277.js +1202 -1013
  18. package/.next/server/chunks/44.js +36 -6
  19. package/.next/server/chunks/675.js +35 -13
  20. package/.next/server/chunks/859.js +21 -4
  21. package/.next/server/chunks/865.js +478 -106
  22. package/.next/server/chunks/911.js +894 -773
  23. package/.next/server/middleware-build-manifest.js +1 -1
  24. package/.next/server/pages/404.html +1 -1
  25. package/.next/server/pages/404.js.nft.json +1 -1
  26. package/.next/server/pages/500.html +1 -1
  27. package/.next/server/pages/_app.js +3 -1
  28. package/.next/server/pages/_app.js.nft.json +1 -1
  29. package/.next/server/pages/api/dictionary/[locale]/[word].js +4 -2
  30. package/.next/server/pages/api/solve.js +51 -20
  31. package/.next/server/pages/api/verify.js +8 -5
  32. package/.next/server/pages/index.html +1 -1
  33. package/.next/server/pages/index.js +55 -65
  34. package/.next/server/pages/index.js.nft.json +1 -1
  35. package/.next/server/pages/index.json +1 -1
  36. package/.next/server/pages-manifest.json +2 -2
  37. package/.next/static/chunks/{framework-2c5cac93e8c637b5.js → framework-2c29dc3cd933590b.js} +2 -2
  38. package/.next/static/chunks/main-4dcb7f9b52833aba.js +1 -0
  39. package/.next/static/chunks/pages/_app-e7f3d1c9c09c8f91.js +32 -0
  40. package/.next/static/chunks/pages/index-82b2939158c7729f.js +1 -0
  41. package/.next/static/css/4e8b47fe382a8a8f.css +2 -0
  42. package/.next/static/css/cfae5256f1689f57.css +1 -0
  43. package/.next/static/{_yG9K-PD5kWT_p4sMdCSV → wNIKOJJzkMSJYb2nOL21o}/_buildManifest.js +1 -1
  44. package/.next/trace +51 -52
  45. package/package.json +14 -14
  46. package/src/components/Board/Board.module.scss +33 -0
  47. package/src/components/Board/Board.tsx +3 -0
  48. package/src/components/Board/BoardPure.tsx +25 -1
  49. package/src/components/Board/components/Cell/Cell.module.scss +4 -12
  50. package/src/components/Board/components/Cell/Cell.tsx +4 -0
  51. package/src/components/Board/components/InputPrompt/InputPrompt.tsx +1 -0
  52. package/src/components/Board/hooks/useBackgroundImage.tsx +24 -6
  53. package/src/components/PlainTiles/lib/createPlainTile.ts +4 -4
  54. package/src/components/Rack/components/InputPrompt/InputPrompt.tsx +1 -0
  55. package/src/components/Radio/Radio.module.scss +4 -0
  56. package/src/components/Radio/Radio.tsx +1 -0
  57. package/src/components/SeoMessage/SeoMessage.tsx +3 -3
  58. package/src/hooks/useAppLayout.ts +1 -2
  59. package/src/hooks/useLocalStorage.ts +5 -5
  60. package/src/i18n/constants.ts +31 -61
  61. package/src/lib/extractCharacters.test.ts +3 -3
  62. package/src/lib/extractCharactersByCase.test.ts +3 -3
  63. package/src/lib/getCellSize.ts +2 -2
  64. package/src/modals/MenuModal/MenuModal.module.scss +0 -1
  65. package/src/modals/MenuModal/MenuModal.tsx +3 -3
  66. package/src/modals/SettingsModal/SettingsModal.tsx +7 -3
  67. package/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.tsx +16 -7
  68. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss +1 -4
  69. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +8 -8
  70. package/src/pages/_app.tsx +3 -1
  71. package/src/pages/api/dictionary/[locale]/[word].ts +6 -3
  72. package/src/pages/api/solve.ts +11 -7
  73. package/src/pages/api/verify.ts +11 -7
  74. package/src/pages/index.module.scss +0 -1
  75. package/src/parameters/index.ts +4 -6
  76. package/src/sdk/solve.ts +3 -3
  77. package/src/sdk/verify.ts +3 -3
  78. package/src/service-worker/routeSolveRequests.ts +3 -3
  79. package/src/state/localStorage.ts +6 -6
  80. package/src/state/sagas.ts +18 -7
  81. package/src/state/selectors.ts +10 -10
  82. package/src/state/slices/boardInitialState.ts +3 -3
  83. package/src/state/slices/boardSlice.ts +20 -6
  84. package/src/state/slices/settingsInitialState.ts +3 -4
  85. package/src/state/slices/settingsSlice.ts +5 -5
  86. package/src/styles/variables.scss +0 -9
  87. package/.next/static/chunks/main-0ecb9ccfcb6c9b24.js +0 -1
  88. package/.next/static/chunks/pages/_app-bea4539a6b8042de.js +0 -32
  89. package/.next/static/chunks/pages/index-4e8566409753e1c3.js +0 -1
  90. package/.next/static/css/58053f9594647860.css +0 -2
  91. package/.next/static/css/60e8258da7362a1a.css +0 -1
  92. package/src/i18n/i18n.module.scss +0 -27
  93. package/src/modals/SettingsModal/components/ConfigSetting/options.ts +0 -19
  94. package/src/modals/SettingsModal/components/ConfigSetting/types.ts +0 -9
  95. package/src/modals/SettingsModal/components/InputModeSetting/types.ts +0 -7
  96. package/src/modals/SettingsModal/components/LocaleSetting/types.ts +0 -9
  97. /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 { LOCALE_FLAGS } from 'i18n';
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 Flag = LOCALE_FLAGS[locale];
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
- <Flag.Icon className={styles.flag} />
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
- <Modal.Section title={translate('settings.inputMode')}>
28
- <InputModeSetting disabled={!isOpen} />
29
- </Modal.Section>
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 { selectConfigId, settingsSlice, useTypedSelector } from 'state';
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 configId = useTypedSelector(selectConfigId);
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
- dispatch(settingsSlice.actions.changeConfigId(event.target.value));
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={configId === option.value}
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="configId"
40
+ name="game"
32
41
  value={option.value}
33
42
  onChange={handleChange}
34
43
  >
@@ -13,10 +13,7 @@
13
13
  }
14
14
 
15
15
  .flag {
16
- --height: 32px;
17
-
18
- width: calc(var(--height) * var(--aspect-ratio));
19
- height: var(--height);
16
+ height: 32px;
20
17
  border-radius: var(--border--radius);
21
18
  box-shadow: var(--box-shadow);
22
19
  transition: var(--transition);
@@ -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 { LOCALE_FLAGS } from 'i18n';
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(LOCALE_FLAGS).sort((a, b) => a.name.localeCompare(b.name));
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.value}
32
+ checked={locale === option.locale}
33
33
  className={classNames(styles.option, className, {
34
- [styles.checked]: locale === option.value,
34
+ [styles.checked]: locale === option.locale,
35
35
  })}
36
36
  disabled={disabled}
37
- key={option.value}
37
+ key={option.locale}
38
38
  name="locale"
39
- value={option.value}
39
+ value={option.locale}
40
40
  onChange={handleChange}
41
41
  >
42
- <span className={classNames(styles.label, option.className)}>
43
- <Icon className={classNames(styles.flag, option.className)} />
42
+ <span className={styles.label}>
43
+ <Icon className={styles.flag} />
44
44
 
45
45
  <span>{option.label}</span>
46
46
  </span>
@@ -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 and Literaki. Quickly find top scoring words using given letters and board state. Available in English, French, German, Polish & Spanish.';
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 { scrabble } from '@scrabble-solver/configs';
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 { isLocale, Locale } from '@scrabble-solver/types';
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 = scrabble['en-US'].maximumCharactersCount;
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> => {
@@ -1,9 +1,9 @@
1
- import { getLocaleConfig, isConfigId } from '@scrabble-solver/configs';
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, isBoardJson, isLocale, Locale, Tile } from '@scrabble-solver/types';
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
- configId: request.body.configId,
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, configId, locale } = request.body;
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 (!isConfigId(configId)) {
56
- throw new Error('Invalid "configId" parameter');
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
- const config = getLocaleConfig(configId, locale);
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)) {
@@ -1,7 +1,7 @@
1
- import { getLocaleConfig, isConfigId } from '@scrabble-solver/configs';
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, isLocale, Locale } from '@scrabble-solver/types';
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
- configId: request.body.configId,
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, configId, locale } = request.body;
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 (!isConfigId(configId)) {
52
- throw new Error('Invalid "configId" parameter');
51
+ if (!isGame(game)) {
52
+ throw new Error('Invalid "game" parameter');
53
53
  }
54
54
 
55
- const config = getLocaleConfig(configId, locale);
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');
@@ -21,7 +21,6 @@
21
21
  }
22
22
 
23
23
  .logo {
24
- width: calc(var(--logo--aspect-ratio) * var(--logo--height));
25
24
  height: var(--logo--height);
26
25
  user-select: none;
27
26
  }
@@ -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
- configId: string;
8
+ game: string;
9
9
  locale: Locale;
10
10
  }
11
11
 
12
- const solve = async ({ board, characters, configId, locale }: Payload): Promise<Result[]> => {
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, configId, locale }),
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
- configId: string;
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, configId, locale }: Payload): Promise<Response> => {
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, configId, locale }),
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, Locale, Tile } from '@scrabble-solver/types';
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, configId, locale } = await request.clone().json();
36
+ const { board, characters, game, locale } = await request.clone().json();
37
37
 
38
38
  const solveLocal = async (trie: Trie) => {
39
- const config = getConfig(configId)[locale as Locale];
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 CONFIG_ID = 'config-id';
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
- getConfigId(): string | undefined {
35
- return store.get(CONFIG_ID);
34
+ getGame(): Game | undefined {
35
+ return store.get(GAME_ID);
36
36
  },
37
37
 
38
- setConfigId(configId: string | undefined): void {
39
- store.set(CONFIG_ID, configId, true);
38
+ setGame(game: Game | undefined): void {
39
+ store.set(GAME_ID, game, true);
40
40
  },
41
41
 
42
42
  getInputMode(): InputMode | undefined {
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable max-lines */
2
2
 
3
3
  import { PayloadAction } from '@reduxjs/toolkit';
4
- import { Locale, Result } from '@scrabble-solver/types';
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.changeConfigId.type, onConfigIdChange);
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* onConfigIdChange(): AnyGenerator {
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 put(boardSlice.actions.reset());
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
- configId: config.id,
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
- configId: config.id,
201
+ game: config.game,
191
202
  locale,
192
203
  });
193
204
  yield put(verifySlice.actions.submitSuccess({ board, invalidWords, validWords }));
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable max-lines */
2
2
 
3
3
  import { createSelector } from '@reduxjs/toolkit';
4
- import { getLocaleConfig } from '@scrabble-solver/configs';
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 selectConfigId = createSelector([selectSettingsRoot], (settings) => settings.configId);
70
+ export const selectGame = createSelector([selectSettingsRoot], (settings) => settings.game);
71
71
 
72
- export const selectConfig = createSelector([selectConfigId, selectLocale], getLocaleConfig);
72
+ export const selectConfig = createSelector([selectGame, selectLocale], getConfig);
73
73
 
74
- export const selectCellFilter = selectCellFilterRoot;
74
+ export const selectFilteredCells = selectCellFilterRoot;
75
75
 
76
- export const selectCellIsFiltered = createSelector([selectCellFilter, selectPoint], (cellFilter, { x, y }) => {
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, selectCellFilter],
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, selectCellFilter, selectResultIndex],
107
- (results, query, cellFilter, index) => {
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 (cellFilter) {
120
- return cellFilter.every(({ x, y }) => result.cells.some((cell) => cell.x === x && cell.y === y));
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 { getLocaleConfig } from '@scrabble-solver/configs';
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 { configId, locale } = settingsInitialState;
11
- const { boardHeight, boardWidth } = getLocaleConfig(configId, locale);
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, { boardDefaultState } from './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;