scrabble-solver
Version:
Scrabble Solver 2 - Free, open-source, cross-platform, multi-language analysis tool for Scrabble, Scrabble Duel, Super Scrabble, Letter League, Literaki, and Kelimelik. Quickly find the top-scoring words using the given board and tiles.
240 lines (199 loc) • 8.38 kB
text/typescript
/* eslint-disable max-lines, no-implicit-globals */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { type PayloadAction } from '@reduxjs/toolkit';
import { hasConfig, languages } from '@scrabble-solver/configs';
import { Board, type Locale, type Result } from '@scrabble-solver/types';
import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { LOCALE_FEATURES } from 'i18n';
import { memoize } from 'lib';
import { findWordDefinitions, solve, verify, visit } from 'sdk';
import { initialize, reset } from './actions';
import { boardSlice, selectBoard } from './board';
import { cellFiltersSlice, selectCellFilter } from './cellFilters';
import { dictionarySlice, selectDictionary } from './dictionary';
import { rackSlice, selectCharacters, selectRack } from './rack';
import { resultsSlice } from './results';
import {
selectConfig,
selectGame,
selectLocale,
selectLocaleAutoGroupTiles,
selectRemoveCellFilters,
settingsSlice,
} from './settings';
import { solveSlice } from './solve';
import { verifySlice } from './verify';
const SUBMIT_DELAY = 150;
const memoizedFindWordDefinitions = memoize(findWordDefinitions);
// Can't conveniently type generators for sagas yet,
// see: https://github.com/microsoft/TypeScript/issues/43632
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyGenerator = Generator<any, any, any>;
export function* rootSaga(): AnyGenerator {
yield takeEvery(boardSlice.actions.changeCellValue.type, onCellValueChange);
yield takeEvery([rackSlice.actions.changeCharacter.type, rackSlice.actions.changeCharacters.type], onRackValueChange);
yield takeEvery(resultsSlice.actions.applyResult.type, onApplyResult);
yield takeEvery(resultsSlice.actions.changeResultCandidate.type, onResultCandidateChange);
yield takeEvery(settingsSlice.actions.changeGame.type, onGameChange);
yield takeEvery(settingsSlice.actions.changeLocale.type, onLocaleChange);
yield takeLatest(dictionarySlice.actions.submit.type, onDictionarySubmit);
yield takeLatest(initialize.type, onInitialize);
yield takeLatest(reset.type, onReset);
yield takeLatest(solveSlice.actions.submit.type, onSolve);
yield takeLatest(verifySlice.actions.submit.type, onVerify);
}
function* onCellValueChange({ payload }: PayloadAction<{ value: string; x: number; y: number }>): AnyGenerator {
const filter = yield select((state) => selectCellFilter(state, payload));
if (filter) {
yield put(cellFiltersSlice.actions.cancel(payload));
}
yield put(resultsSlice.actions.changeResultCandidate(null));
yield put(verifySlice.actions.submit());
}
function* onRackValueChange(): AnyGenerator {
yield put(resultsSlice.actions.changeResultCandidate(null));
}
function* onApplyResult({ payload: result }: PayloadAction<Result>): AnyGenerator {
const autoGroupTiles = yield select(selectLocaleAutoGroupTiles);
const removeCellFilters = yield select(selectRemoveCellFilters);
yield put(boardSlice.actions.applyResult(result));
if (removeCellFilters === 'never') {
yield put(cellFiltersSlice.actions.removeCells(result.cells));
} else {
yield put(cellFiltersSlice.actions.reset());
}
yield put(rackSlice.actions.removeTiles(result.tiles));
yield put(rackSlice.actions.groupTiles(autoGroupTiles));
yield put(verifySlice.actions.submit());
}
function* onGameChange(): AnyGenerator {
const characters = yield select(selectCharacters);
if (characters.length > 0) {
yield put(solveSlice.actions.submit());
} else {
yield put(resultsSlice.actions.reset());
}
yield put(resultsSlice.actions.reset());
yield* resetRack();
yield put(verifySlice.actions.submit());
}
function* onDictionarySubmit(): AnyGenerator {
const { input: word } = yield select(selectDictionary);
const locale = yield select(selectLocale);
if (!memoizedFindWordDefinitions.hasCache(locale, word)) {
yield delay(SUBMIT_DELAY);
}
try {
const wordDefinitions = yield call(memoizedFindWordDefinitions, locale, word);
const { input: currentWord } = yield select(selectDictionary);
if (word === currentWord) {
yield put(dictionarySlice.actions.submitSuccess(wordDefinitions));
}
} catch (error) {
const { input: currentWord } = yield select(selectDictionary);
if (word === currentWord) {
yield put(dictionarySlice.actions.submitFailure(error));
}
}
}
function* onInitialize(): AnyGenerator {
const board = yield select(selectBoard);
yield call(visit);
if (!board.isEmpty()) {
yield* resetRack();
yield put(verifySlice.actions.submit());
}
}
function* onReset(): AnyGenerator {
const config = yield select(selectConfig);
yield put(boardSlice.actions.init(Board.create(config.boardWidth, config.boardHeight)));
yield put(cellFiltersSlice.actions.reset());
yield put(dictionarySlice.actions.reset());
yield put(rackSlice.actions.reset());
yield put(resultsSlice.actions.reset());
yield put(solveSlice.actions.reset());
yield put(verifySlice.actions.submit());
}
function* onLocaleChange({ payload: locale }: PayloadAction<Locale>): AnyGenerator {
const game = yield select(selectGame);
if (!hasConfig(game, locale)) {
const defaultConfig = Object.values(languages).find((config) => config.locale === locale);
if (defaultConfig) {
yield put(settingsSlice.actions.changeGame(defaultConfig.game));
}
}
const characters = yield select(selectCharacters);
if (characters.length > 0) {
yield put(solveSlice.actions.submit());
} else {
yield put(resultsSlice.actions.reset());
}
yield put(dictionarySlice.actions.reset());
yield put(resultsSlice.actions.changeResultCandidate(null));
yield put(verifySlice.actions.submit());
}
function* onResultCandidateChange({ payload: result }: PayloadAction<Result | null>): AnyGenerator {
if (result) {
const locale: Locale = yield select(selectLocale);
const uniqueWords = Array.from(new Set(result.words));
const input = uniqueWords.join(LOCALE_FEATURES[locale].separator);
yield put(dictionarySlice.actions.changeInput(input));
yield put(dictionarySlice.actions.submit());
}
}
function* onSolve(): AnyGenerator {
const board = yield select(selectBoard);
const { config } = yield select(selectConfig);
const locale = yield select(selectLocale);
const characters = yield select(selectCharacters);
if (characters.length === 0) {
yield put(solveSlice.actions.submitSuccess({ board, characters }));
yield put(resultsSlice.actions.changeResults([]));
return;
}
try {
const results = yield call(solve, {
board: board.toJson(),
characters,
game: config.game,
locale,
});
yield put(resultsSlice.actions.changeResults(results));
yield put(solveSlice.actions.submitSuccess({ board, characters }));
} catch (error) {
yield put(resultsSlice.actions.changeResults([]));
yield put(solveSlice.actions.submitFailure(error));
}
}
function* onVerify(): AnyGenerator {
yield delay(SUBMIT_DELAY);
const board = yield select(selectBoard);
const { config } = yield select(selectConfig);
const locale = yield select(selectLocale);
try {
const { invalidWords, validWords } = yield call(verify, {
board: board.toJson(),
game: config.game,
locale,
});
yield put(verifySlice.actions.submitSuccess({ board, invalidWords, validWords }));
} catch (_error) {
yield put(verifySlice.actions.submitFailure());
}
}
function* resetRack(): AnyGenerator {
const { config } = yield select(selectConfig);
const rack = yield select(selectRack);
if (config.rackSize > rack.length) {
const differenceCount = Math.abs(config.rackSize - rack.length);
yield put(rackSlice.actions.init([...rack, ...Array(differenceCount).fill(null)]));
} else if (config.rackSize < rack.length) {
const nonNulls = rack.filter(Boolean).slice(0, config.rackSize);
const differenceCount = Math.abs(config.rackSize - nonNulls.length);
const autoGroupTiles = yield select(selectLocaleAutoGroupTiles);
yield put(rackSlice.actions.init([...nonNulls, ...Array(differenceCount).fill(null)]));
yield put(rackSlice.actions.groupTiles(autoGroupTiles));
}
}