@scrabble-solver/scrabble-solver 2.8.6 → 2.8.8
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 +10 -10
- 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 +266 -110
- package/.next/server/chunks/{206.js → 429.js} +2 -4137
- package/.next/server/chunks/515.js +197 -91
- 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/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/615-d258f6c528c18622.js +1 -0
- package/.next/static/chunks/pages/{404-30c06e61d256c5b2.js → 404-8eb3ba4f0ba17e08.js} +1 -1
- package/.next/static/chunks/pages/_app-4a663fd3d5ca4524.js +1 -0
- package/.next/static/chunks/pages/index-1a9826d740cc8830.js +1 -0
- package/.next/static/css/180c6c26317ac90f.css +1 -0
- package/.next/static/css/751e8a14776d05d8.css +1 -0
- package/.next/static/z3J3qmq1nazbDv_ENIkCo/_buildManifest.js +1 -0
- package/.next/static/{VjSpyGDWyVaO0muz54q_j → z3J3qmq1nazbDv_ENIkCo}/_ssgManifest.js +0 -0
- package/.next/trace +41 -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/Board/components/Cell/Cell.module.scss +34 -9
- package/src/components/Board/components/Cell/Cell.tsx +23 -4
- package/src/components/Board/components/Cell/CellPure.tsx +29 -1
- package/src/components/Board/hooks/useGrid.ts +1 -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/i18n/de.json +1 -0
- package/src/i18n/en.json +1 -0
- package/src/i18n/es.json +1 -0
- package/src/i18n/fr.json +1 -0
- package/src/i18n/pl.json +1 -0
- package/src/icons/Flag.svg +4 -0
- package/src/icons/Star.svg +4 -0
- package/src/icons/index.ts +2 -0
- 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/rootReducer.ts +10 -1
- package/src/state/sagas.ts +32 -12
- package/src/state/selectors.ts +41 -7
- package/src/state/slices/cellFilterInitialState.ts +7 -0
- package/src/state/slices/cellFilterSlice.ts +24 -0
- package/src/state/slices/dictionaryInitialState.ts +3 -3
- package/src/state/slices/dictionarySlice.ts +4 -10
- package/src/state/slices/index.ts +2 -0
- package/src/types/index.ts +1 -0
- 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/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
|
-
}
|
package/src/pages/index.tsx
CHANGED
|
@@ -74,8 +74,10 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
74
74
|
<>
|
|
75
75
|
<div className={classNames(styles.index, { [styles.initialized]: isInitialized })}>
|
|
76
76
|
<div className={styles.nav}>
|
|
77
|
-
<div className={styles.
|
|
78
|
-
<
|
|
77
|
+
<div className={styles.navLogo}>
|
|
78
|
+
<a className={styles.logoContainer} href="/" title={version}>
|
|
79
|
+
<Logo className={styles.logo} />
|
|
80
|
+
</a>
|
|
79
81
|
</div>
|
|
80
82
|
|
|
81
83
|
<NavButtons
|
|
@@ -114,8 +116,6 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
114
116
|
<Rack className={styles.rack} />
|
|
115
117
|
<input className={styles.submitInput} tabIndex={-1} type="submit" />
|
|
116
118
|
</form>
|
|
117
|
-
|
|
118
|
-
<span className={styles.version}>v{version}</span>
|
|
119
119
|
</div>
|
|
120
120
|
|
|
121
121
|
<Settings isOpen={showSettings} onClose={() => setShowSettings(false)} />
|
|
@@ -138,7 +138,7 @@ const readVersion = async (): Promise<string> => {
|
|
|
138
138
|
const packageJsonFilepath = path.resolve(process.cwd(), 'package.json');
|
|
139
139
|
const data = await fs.promises.readFile(packageJsonFilepath, 'utf-8');
|
|
140
140
|
const packageJson = JSON.parse(data);
|
|
141
|
-
return packageJson.version
|
|
141
|
+
return `v${packageJson.version}`;
|
|
142
142
|
};
|
|
143
143
|
|
|
144
144
|
export default Index;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Locale, WordDefinition } from '@scrabble-solver/types';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const findWordDefinitions = async (locale: Locale, word: string): Promise<WordDefinition[]> => {
|
|
4
4
|
const url = `/api/dictionary/${locale}/${word}`;
|
|
5
5
|
const json = await fetch(url).then((response) => response.json());
|
|
6
|
-
return WordDefinition.fromJson
|
|
6
|
+
return json.map(WordDefinition.fromJson);
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export default
|
|
9
|
+
export default findWordDefinitions;
|
package/src/sdk/index.ts
CHANGED
package/src/state/rootReducer.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { combineReducers } from 'redux';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
boardSlice,
|
|
5
|
+
dictionarySlice,
|
|
6
|
+
cellFilterSlice,
|
|
7
|
+
rackSlice,
|
|
8
|
+
resultsSlice,
|
|
9
|
+
settingsSlice,
|
|
10
|
+
solveSlice,
|
|
11
|
+
} from './slices';
|
|
4
12
|
|
|
5
13
|
const rootReducer = combineReducers({
|
|
6
14
|
board: boardSlice.reducer,
|
|
15
|
+
cellFilter: cellFilterSlice.reducer,
|
|
7
16
|
dictionary: dictionarySlice.reducer,
|
|
8
17
|
rack: rackSlice.reducer,
|
|
9
18
|
results: resultsSlice.reducer,
|
package/src/state/sagas.ts
CHANGED
|
@@ -3,22 +3,31 @@ import { Result } from '@scrabble-solver/types';
|
|
|
3
3
|
import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
|
|
4
4
|
|
|
5
5
|
import { memoize } from 'lib';
|
|
6
|
-
import {
|
|
6
|
+
import { findWordDefinitions, solve, visit } from 'sdk';
|
|
7
7
|
|
|
8
8
|
import { initialize, reset } from './actions';
|
|
9
9
|
import {
|
|
10
10
|
selectAutoGroupTiles,
|
|
11
11
|
selectBoard,
|
|
12
|
+
selectCellIsFiltered,
|
|
12
13
|
selectCharacters,
|
|
13
14
|
selectConfig,
|
|
14
15
|
selectDictionary,
|
|
15
16
|
selectLocale,
|
|
16
17
|
} from './selectors';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
boardSlice,
|
|
20
|
+
cellFilterSlice,
|
|
21
|
+
dictionarySlice,
|
|
22
|
+
rackSlice,
|
|
23
|
+
resultsSlice,
|
|
24
|
+
settingsSlice,
|
|
25
|
+
solveSlice,
|
|
26
|
+
} from './slices';
|
|
18
27
|
|
|
19
28
|
const SUBMIT_DELAY = 150;
|
|
20
29
|
|
|
21
|
-
const
|
|
30
|
+
const memoizedFindWordDefinitions = memoize(findWordDefinitions);
|
|
22
31
|
|
|
23
32
|
// Can't conveniently type generators for sagas yet,
|
|
24
33
|
// see: https://github.com/microsoft/TypeScript/issues/43632
|
|
@@ -26,6 +35,7 @@ const memoizedFindWordDefinition = memoize(findWordDefinition);
|
|
|
26
35
|
type AnyGenerator = Generator<any, any, any>;
|
|
27
36
|
|
|
28
37
|
export function* rootSaga(): AnyGenerator {
|
|
38
|
+
yield takeEvery(boardSlice.actions.changeCellValue.type, onCellValueChange);
|
|
29
39
|
yield takeEvery(resultsSlice.actions.applyResult.type, onApplyResult);
|
|
30
40
|
yield takeEvery(resultsSlice.actions.changeResultCandidate.type, onResultCandidateChange);
|
|
31
41
|
yield takeEvery(settingsSlice.actions.changeConfigId.type, onConfigIdChange);
|
|
@@ -36,9 +46,18 @@ export function* rootSaga(): AnyGenerator {
|
|
|
36
46
|
yield takeLatest(solveSlice.actions.submit.type, onSubmit);
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
function* onCellValueChange({ payload }: PayloadAction<{ value: string; x: number; y: number }>): AnyGenerator {
|
|
50
|
+
const isFiltered = yield select((state) => selectCellIsFiltered(state, payload));
|
|
51
|
+
|
|
52
|
+
if (isFiltered) {
|
|
53
|
+
yield put(cellFilterSlice.actions.toggle(payload));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
function* onApplyResult({ payload: result }: PayloadAction<Result>): AnyGenerator {
|
|
40
58
|
const autoGroupTiles = yield select(selectAutoGroupTiles);
|
|
41
59
|
yield put(boardSlice.actions.applyResult(result));
|
|
60
|
+
yield put(cellFilterSlice.actions.reset());
|
|
42
61
|
yield put(rackSlice.actions.removeTiles(result.tiles));
|
|
43
62
|
yield put(rackSlice.actions.groupTiles(autoGroupTiles));
|
|
44
63
|
}
|
|
@@ -53,13 +72,13 @@ function* onDictionarySubmit(): AnyGenerator {
|
|
|
53
72
|
const { input: word } = yield select(selectDictionary);
|
|
54
73
|
const locale = yield select(selectLocale);
|
|
55
74
|
|
|
56
|
-
if (!
|
|
75
|
+
if (!memoizedFindWordDefinitions.hasCache(locale, word)) {
|
|
57
76
|
yield delay(SUBMIT_DELAY);
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
try {
|
|
61
|
-
const
|
|
62
|
-
yield put(dictionarySlice.actions.submitSuccess(
|
|
80
|
+
const wordDefinitions = yield call(memoizedFindWordDefinitions, locale, word);
|
|
81
|
+
yield put(dictionarySlice.actions.submitSuccess(wordDefinitions));
|
|
63
82
|
} catch (error) {
|
|
64
83
|
yield put(dictionarySlice.actions.submitFailure());
|
|
65
84
|
}
|
|
@@ -72,6 +91,7 @@ function* onInitialize(): AnyGenerator {
|
|
|
72
91
|
|
|
73
92
|
function* onReset(): AnyGenerator {
|
|
74
93
|
yield put(boardSlice.actions.reset());
|
|
94
|
+
yield put(cellFilterSlice.actions.reset());
|
|
75
95
|
yield put(dictionarySlice.actions.reset());
|
|
76
96
|
yield put(rackSlice.actions.reset());
|
|
77
97
|
yield put(resultsSlice.actions.reset());
|
|
@@ -85,7 +105,7 @@ function* onLocaleChange(): AnyGenerator {
|
|
|
85
105
|
|
|
86
106
|
function* onResultCandidateChange({ payload: result }: PayloadAction<Result | null>): AnyGenerator {
|
|
87
107
|
if (result) {
|
|
88
|
-
yield put(dictionarySlice.actions.changeInput(result.
|
|
108
|
+
yield put(dictionarySlice.actions.changeInput(result.words.join(', ')));
|
|
89
109
|
yield put(dictionarySlice.actions.submit());
|
|
90
110
|
}
|
|
91
111
|
}
|
|
@@ -121,12 +141,12 @@ function* ensureProperTilesCount(): AnyGenerator {
|
|
|
121
141
|
const { config } = yield select(selectConfig);
|
|
122
142
|
const characters = yield select(selectCharacters);
|
|
123
143
|
|
|
124
|
-
if (config.
|
|
125
|
-
const differenceCount = Math.abs(config.
|
|
144
|
+
if (config.maximumCharactersCount > characters.length) {
|
|
145
|
+
const differenceCount = Math.abs(config.maximumCharactersCount - characters.length);
|
|
126
146
|
yield put(rackSlice.actions.init([...characters, ...Array(differenceCount).fill(null)]));
|
|
127
|
-
} else if (config.
|
|
128
|
-
const nonNulls = characters.filter(Boolean).slice(0, config.
|
|
129
|
-
const differenceCount = Math.abs(config.
|
|
147
|
+
} else if (config.maximumCharactersCount < characters.length) {
|
|
148
|
+
const nonNulls = characters.filter(Boolean).slice(0, config.maximumCharactersCount);
|
|
149
|
+
const differenceCount = Math.abs(config.maximumCharactersCount - nonNulls.length);
|
|
130
150
|
const autoGroupTiles = yield select(selectAutoGroupTiles);
|
|
131
151
|
yield put(rackSlice.actions.init([...nonNulls, ...Array(differenceCount).fill(null)]));
|
|
132
152
|
yield put(rackSlice.actions.groupTiles(autoGroupTiles));
|
package/src/state/selectors.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSelector } from '@reduxjs/toolkit';
|
|
2
2
|
import { getLocaleConfig } from '@scrabble-solver/configs';
|
|
3
|
-
import { Cell, Config, Tile } from '@scrabble-solver/types';
|
|
3
|
+
import { Cell, Config, Result, Tile } from '@scrabble-solver/types';
|
|
4
4
|
|
|
5
5
|
import i18n from 'i18n';
|
|
6
6
|
import { findCell, getRemainingTiles, getRemainingTilesGroups, sortResults, unorderedArraysEqual } from 'lib';
|
|
@@ -10,6 +10,8 @@ import { RootState } from './types';
|
|
|
10
10
|
|
|
11
11
|
const selectCell = (_: unknown, cell: Cell): Cell => cell;
|
|
12
12
|
|
|
13
|
+
const selectPoint = (_: unknown, point: { x: number; y: number }): { x: number; y: number } => point;
|
|
14
|
+
|
|
13
15
|
const selectCharacter = (_: unknown, character: string | null): string | null => character;
|
|
14
16
|
|
|
15
17
|
const selectTile = (_: unknown, tile: Tile | null): Tile | null => tile;
|
|
@@ -18,6 +20,8 @@ const selectBoardRoot = (state: RootState): RootState['board'] => state.board;
|
|
|
18
20
|
|
|
19
21
|
const selectDictionaryRoot = (state: RootState): RootState['dictionary'] => state.dictionary;
|
|
20
22
|
|
|
23
|
+
const selectCellFilterRoot = (state: RootState): RootState['cellFilter'] => state.cellFilter;
|
|
24
|
+
|
|
21
25
|
const selectRackRoot = (state: RootState): RootState['rack'] => state.rack;
|
|
22
26
|
|
|
23
27
|
const selectResultsRoot = (state: RootState): RootState['results'] => state.results;
|
|
@@ -38,6 +42,12 @@ export const selectConfigId = createSelector([selectSettingsRoot], (settings) =>
|
|
|
38
42
|
|
|
39
43
|
export const selectConfig = createSelector([selectConfigId, selectLocale], getLocaleConfig);
|
|
40
44
|
|
|
45
|
+
export const selectCellFilter = selectCellFilterRoot;
|
|
46
|
+
|
|
47
|
+
export const selectCellIsFiltered = createSelector([selectCellFilter, selectPoint], (cellFilter, { x, y }) => {
|
|
48
|
+
return cellFilter.some((cell) => cell.x === x && cell.y === y);
|
|
49
|
+
});
|
|
50
|
+
|
|
41
51
|
export const selectResults = createSelector([selectResultsRoot], (results) => results.results);
|
|
42
52
|
|
|
43
53
|
export const selectResultsQuery = createSelector([selectResultsRoot], (results) => results.query);
|
|
@@ -51,16 +61,40 @@ export const selectSortedResults = createSelector(
|
|
|
51
61
|
sortResults,
|
|
52
62
|
);
|
|
53
63
|
|
|
64
|
+
const filterResultsByQuery = (results: Result[], query: string): Result[] => {
|
|
65
|
+
if (query.trim().length === 0) {
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let regExp: RegExp | undefined;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
regExp = new RegExp(query, 'gi');
|
|
73
|
+
} catch {
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return results.filter((result) => {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
79
|
+
return regExp!.test(result.word);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
54
83
|
export const selectSortedFilteredResults = createSelector(
|
|
55
|
-
[selectSortedResults, selectResultsQuery],
|
|
56
|
-
(results, query) => {
|
|
57
|
-
if (!results
|
|
84
|
+
[selectSortedResults, selectResultsQuery, selectCellFilter],
|
|
85
|
+
(results, query, cellFilter) => {
|
|
86
|
+
if (!results) {
|
|
58
87
|
return results;
|
|
59
88
|
}
|
|
60
89
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
90
|
+
const filteredByQuery = filterResultsByQuery(results, query);
|
|
91
|
+
|
|
92
|
+
if (!cellFilter) {
|
|
93
|
+
return filteredByQuery;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return filteredByQuery.filter((result) => {
|
|
97
|
+
return cellFilter.every(({ x, y }) => result.cells.some((cell) => cell.x === x && cell.y === y));
|
|
64
98
|
});
|
|
65
99
|
},
|
|
66
100
|
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
2
|
+
|
|
3
|
+
import cellFilterInitialState, { Point } from './cellFilterInitialState';
|
|
4
|
+
|
|
5
|
+
const cellFilterSlice = createSlice({
|
|
6
|
+
initialState: cellFilterInitialState,
|
|
7
|
+
name: 'cellFilter',
|
|
8
|
+
reducers: {
|
|
9
|
+
toggle: (state, action: PayloadAction<Point>) => {
|
|
10
|
+
const { x, y } = action.payload;
|
|
11
|
+
const has = state.some((point) => point.x === x && point.y === y);
|
|
12
|
+
|
|
13
|
+
if (has) {
|
|
14
|
+
return state.filter((point) => point.x !== x || point.y !== y);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return [...state, action.payload];
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
reset: () => cellFilterInitialState,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default cellFilterSlice;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { WordDefinition } from '@scrabble-solver/types';
|
|
2
|
+
|
|
1
3
|
const dictionaryInitialState = {
|
|
2
|
-
definitions: [] as string[],
|
|
3
4
|
input: '',
|
|
4
|
-
isAllowed: null as boolean | null,
|
|
5
5
|
isLoading: false,
|
|
6
|
-
|
|
6
|
+
results: [] as WordDefinition[],
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export default dictionaryInitialState;
|
|
@@ -16,30 +16,24 @@ const dictionarySlice = createSlice({
|
|
|
16
16
|
submit: (state) => {
|
|
17
17
|
return {
|
|
18
18
|
...state,
|
|
19
|
-
definitions: dictionaryInitialState.definitions,
|
|
20
|
-
isAllowed: dictionaryInitialState.isAllowed,
|
|
21
19
|
isLoading: true,
|
|
22
|
-
|
|
20
|
+
results: dictionaryInitialState.results,
|
|
23
21
|
};
|
|
24
22
|
},
|
|
25
23
|
|
|
26
24
|
submitFailure: (state) => {
|
|
27
25
|
return {
|
|
28
26
|
...state,
|
|
29
|
-
definitions: dictionaryInitialState.definitions,
|
|
30
|
-
isAllowed: false,
|
|
31
27
|
isLoading: false,
|
|
32
|
-
|
|
28
|
+
results: dictionaryInitialState.results,
|
|
33
29
|
};
|
|
34
30
|
},
|
|
35
31
|
|
|
36
|
-
submitSuccess: (state, action: PayloadAction<WordDefinition>) => {
|
|
32
|
+
submitSuccess: (state, action: PayloadAction<WordDefinition[]>) => {
|
|
37
33
|
return {
|
|
38
34
|
...state,
|
|
39
|
-
definitions: action.payload.definitions,
|
|
40
|
-
isAllowed: action.payload.isAllowed,
|
|
41
35
|
isLoading: false,
|
|
42
|
-
|
|
36
|
+
results: action.payload,
|
|
43
37
|
};
|
|
44
38
|
},
|
|
45
39
|
},
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { default as boardInitialState } from './boardInitialState';
|
|
2
2
|
export { default as boardSlice } from './boardSlice';
|
|
3
|
+
export { default as cellFilterInitialState } from './cellFilterInitialState';
|
|
4
|
+
export { default as cellFilterSlice } from './cellFilterSlice';
|
|
3
5
|
export { default as dictionaryInitialState } from './dictionaryInitialState';
|
|
4
6
|
export { default as dictionarySlice } from './dictionarySlice';
|
|
5
7
|
export { default as rackInitialState } from './rackInitialState';
|
package/src/types/index.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
self.__BUILD_MANIFEST=function(s,a,e){return{__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":[s,a,e,"static/css/3159cfe62ff742a3.css","static/chunks/pages/index-13ea7770a65c69ee.js"],"/404":[s,a,e,"static/chunks/pages/404-30c06e61d256c5b2.js"],"/_error":["static/chunks/pages/_error-a4ba2246ff8fb532.js"],sortedPages:["/","/404","/_app","/_error"]}}("static/chunks/758-f333b1dcdb941547.js","static/css/729bb37fe8f9bee6.css","static/chunks/56-e2797384ae4b0fc0.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
|