Implement a Wordle solver at /solver (EN) and /ru/solver (RU), using a 6×5
game-board grid as the primary input mechanism.
/solver → English solver (uses word-bank.json)/ru/solver → Russian solver (uses word-bank-ru.json)detectLang() recognizes /solver as EN, /ru/solver as RU.Dictionaries will grow (more languages, variable word lengths). All filtering runs on the server to keep the SPA lean.
POST /api/solver (EN)
POST /ru/api/solver (RU)
Request:
{
"rows": [
{ "guess": "ocean", "result": "gyyxx" },
{ "guess": "train", "result": "xxgxy" }
]
}
rows: array of guess+result objects, one per row the user has filled in. Ordered from
first guess to most recent. May be empty or contain up to 6 entries.
guess: 5-letter word the user tried (lowercase).result: 5-char feedback string. Each character is g (green — correct letter,
correct position), y (yellow — correct letter, wrong position), or x (gray —
letter not in the target word).Response (200):
{
"words": ["crane", "crone", "crony"],
"totalCount": 3,
"suggestedGuess": "crane",
"bestLetters": [
{ "letter": "r", "pct": 100 },
{ "letter": "n", "pct": 100 }
]
}
words: matching candidates, sorted alphabetically. Capped at 200.totalCount: true count (may exceed words.length).suggestedGuess: best next guess from the candidate set (null if 0 candidates).
If exactly 1 candidate, it is that word.bestLetters: top 15 letters by frequency across candidates, with percentage (0–100).Validation (400):
rows is empty (no complete guesses submitted): { "error": "At least one complete guess row required" }.
The UI ensures every letter has a color (gray by default), so every fully-typed row is complete.┌───────────────────────────────┐
│ [LangSwitch] │
│ Wordle Solver │
│ │
│ ┌───┬───┬───┬───┬───┐ │
│ │ C │ R │ A │ N │ E │ ← 1 │ ← gray row of tiles
│ ├───┼───┼───┼───┼───┤ │ (C,A,E gray, R yellow, N green)
│ │ │ │ │ │ │ ← 2 │
│ ├───┼───┼───┼───┼───┤ │
│ │ │ │ │ │ │ ← 3 │
│ ├───┼───┼───┼───┼───┤ │
│ │ │ │ │ │ │ ← 4 │
│ ├───┼───┼───┼───┼───┤ │
│ │ │ │ │ │ │ ← 5 │
│ ├───┼───┼───┼───┼───┤ │
│ │ │ │ │ │ │ ← 6 │
│ └───┴───┴───┴───┴───┘ │
│ │
│ [ Update ] │
│ │
│ ─── Results ─── │
│ ... │
└───────────────────────────────┘
#787c7e gray background.#c9b458 background.#6aaa64 background.Tapping a tile does two things depending on context:
| Condition | Action |
|---|---|
| Tile is not focused (cursor elsewhere) | Move cursor to this tile |
| Tile is focused AND has a letter | Cycle color: gray → yellow → green → gray → … |
| Tile is focused AND is empty | No-op (stay in place) |
Letters default to gray when typed. To mark a letter as yellow or green, tap the focused tile to cycle its color. To edit a letter: tap the tile once (focuses it), then type the replacement letter.
The on-screen keyboard from the game is reused. In addition to the 3 letter rows and nav row, the solver adds a small help row above the keyboard:
[ Tap tile: focus ] [ Tap focused tile: cycle color ] [ Update ]
Or more minimal: just a hint text above the keyboard: "Tap a tile to focus it. Tap an already focused tile to cycle its color."
Letter keys:
Backspace:
Enter:
Arrow keys:
Mirrors the on-screen keyboard behavior:
[a-zA-Zа-яА-Я] → type letter at cursor (defaults to gray).Backspace → clear tile, move left. If all 5 tiles in a row are cleared, the row becomes empty.ArrowLeft/Right/Up/Down → move cursor.Enter → Update (if enabled).{ rows: [{guess, result}] } payload,
sends POST /api/solver, displays results.Rows are either completely filled (all 5 tiles have letters) or completely empty — no partial rows. The Update button stays disabled while any row has 1–4 letters. Since every letter is gray by default, a row is "complete" as soon as the 5th letter is typed.
Each filled row produces one { guess, result } object:
guess: the 5 letters, joined into a lowercase string.result: the 5 colors, each mapped to g / y / x, joined into a 5-char string.Rows are sent in the order they appear on the board (row 1 first).
Appears below the Update button after a successful API response.
totalCount > words.length: show "…and N more" at the bottom.Add to Messages interface and both language files:
| Key | EN | RU |
|---|---|---|
solver.title |
Wordle Solver | Вордли-помощник |
solver.updateButton |
Update | Обновить |
solver.suggestedGuess |
Suggested | Совет |
solver.theWordIs |
The word is | Это слово |
solver.noMatches |
No matching words | Нет подходящих слов |
solver.bestLetters |
Best Letters | Лучшие буквы |
solver.possibleWords |
Possible Words | Возможные слова |
solver.resultsCount |
{n} words found |
Найдено слов: {n} |
solver.andNMore |
…and {n} more |
…и ещё {n} |
solver.needOneRow |
Enter at least one guess | Введите хотя бы одну попытку |
solver.backToGame |
← Game | ← Игра |
solver.tapHint |
Letters start gray · tap focused tile to cycle: gray→yellow→green | Буквы начинаются серым · нажмите ещё раз: серый→жёлтый→зелёный |
shared/src/api.ts — SolverRequest, SolverResponse types
server/src/routes/solver.ts — POST /api/solver (filtering + scoring logic)
server/src/index.ts — mount solver route
client/src/api.ts — postSolver() client function
client/src/messages/index.ts — solver i18n keys
client/src/messages/en.ts — EN strings
client/src/messages/ru.ts — RU strings
client/src/screens/ScreenSolver.tsx — solver screen (board + results)
client/src/components/SolverBoard.tsx — 6×5 editable board with cursor + color cycling
client/src/App.tsx — route /solver, /ru/solver to solver screen
The existing Keyboard component is reused as-is. SolverBoard is a new component
(significantly different from the game Grid — editable, per-tile color cycling, 6-row
cursor navigation).
Each row { guess, result } yields constraints. The server merges constraints from all rows
into a consolidated filter.
Per-row constraint extraction:
greens[i] = guess[i] for each i where result[i] == 'g'
yellows += guess[i] for each i where result[i] == 'y'
grays += guess[i] for each i where result[i] == 'x'
Merge rules across rows:
Candidate filtering:
function filterCandidates(words, rows):
extract greens, yellows, grays from rows as above
candidates = []
for word in words:
// Green: positional match
reject if any greens[i] is set and word[i] != greens[i]
// Gray: excluded letters
for letter in grays:
if letter in word: reject
// Yellow: from each row, extract per-position constraints.
// A yellow at (row k, position i) means: target contains row.guess[i],
// but NOT at position i. Accumulate forbidden positions and multiplicity.
// Then:
for each (letter, forbiddenPos) in yellowConstraints:
if word[forbiddenPos] == letter: reject
for each (letter, minCount) in yellowMultiplicity:
if countIn(word, letter) < minCount: reject
candidates.append(word)
return candidates
Suggested guess scoring: sum positional letter frequencies across all candidates; pick the candidate with the highest score.
/solver → 6×5 board, empty tiles, cursor at row 1 col 1, Update disabled./ru/solver → same board, Russian labels, works with Russian dictionary.{ rows: [{guess, result}, ...] } → API returns candidates./solver and /ru/solver./ or /ru/.