# Feature: Wordle Solver Tool Implement a Wordle solver at `/solver` (EN) and `/ru/solver` (RU), using a 6×5 game-board grid as the primary input mechanism. --- ## 1. Routing - `/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. - The solver is a client-side screen served by the existing SPA. --- ## 2. Architecture: Server-Side Filtering 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:** ```json { "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):** ```json { "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):** - If `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. --- ## 3. UI: 6×5 Game-Board Grid ### 3.1 Board Layout ``` ┌───────────────────────────────┐ │ [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 ─── │ │ ... │ └───────────────────────────────┘ ``` - 6 rows × 5 columns of tiles. - Each tile: 52×52px (same as game grid). - Empty tile: white background, light border. - Letter typed (default gray): white letter on `#787c7e` gray background. - Yellow: white letter on `#c9b458` background. - Green: white letter on `#6aaa64` background. - Every letter has a color from the moment it's typed — gray is the default. ### 3.2 Cursor - One tile at a time has the **cursor** (blinking underline, same as game screen). - Cursor is always in the **lowest non-empty row**, or row 1 if all rows empty. - Initially cursor is at row 1, column 1. ### 3.3 Tile Interaction (Tap) 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. ### 3.4 Keyboard Input 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:** - If the cursor is at a tile with no letter → fills that tile with the typed letter, colors it gray by default, advances cursor to the next column. - If the cursor is at a tile that already has a letter → replaces the letter, resets color to gray, keeps cursor in place (user might want to cycle the color next). - If cursor is past column 5 → stays at column 5 (row is full). **Backspace:** - Clears the letter AND color at cursor position. - Moves cursor left one column. - If already at column 1 → moves to row above, column 5 (if previous row exists). **Enter:** - Triggers Update (same as clicking the Update button), if enabled. **Arrow keys:** - Left/Right: move cursor within current row (clamped). - Up: move to same column in previous row (if it exists). If that row is empty, cursor goes to last filled column in that row, or column 1. - Down: move to same column in next row (if it exists). ### 3.5 Physical Keyboard 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). ### 3.6 Update Button - Rendered below the board. - **Disabled** (greyed out) when: - No letters have been entered anywhere on the board (all 30 tiles empty), OR - Any row has 1–4 letters (a partially filled row). The user must either complete that row to 5 letters or clear it entirely (Backspace each tile). - **Enabled** when: at least one row is fully typed (all 5 letters) AND no partial rows exist. - On click: gathers all fully-typed rows, builds the `{ rows: [{guess, result}] }` payload, sends `POST /api/solver`, displays results. - While request is in flight: button shows spinner, is disabled (prevents double-submit). ### 3.7 Row Submission 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). --- ## 4. Results Panel Appears below the Update button after a successful API response. ### 4.1 Results Summary - **N words found** (localized). - **Suggested: CRANE** (or "The word is: CRANE" if N=1, or "No matching words" if N=0). ### 4.2 Best Letters - Top 15 letters by frequency among candidates. - Each row: letter, green horizontal bar proportional to percentage, percentage number. - Sorted by frequency descending. ### 4.3 Possible Words - Scrollable list in monospace font, alphabetical order. - Max height to keep results visible without excessive scrolling. - If `totalCount > words.length`: show "…and N more" at the bottom. --- ## 5. i18n 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 | Буквы начинаются серым · нажмите ещё раз: серый→жёлтый→зелёный | --- ## 6. New / Modified Files ``` 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). --- ## 7. Filtering Logic (Server-Side) 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:** - Green: later rows override earlier rows for the same position (they reflect more recent information). - Yellow: accumulate all yellow letters across rows. If a letter appears as green in ANY row, remove it from yellows. - Gray: accumulate all gray letters. If a letter appears as green or yellow in ANY row, remove it from grays (green/yellow take precedence). **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. --- ## 8. Acceptance Criteria 1. `/solver` → 6×5 board, empty tiles, cursor at row 1 col 1, Update disabled. 2. `/ru/solver` → same board, Russian labels, works with Russian dictionary. 3. Tap a tile → cursor moves there. 4. Type a letter → fills the tile in gray, cursor advances to next column. 5. Tap a focused tile with a letter → color cycles: gray → yellow → green → gray. 6. Backspace → clears tile, cursor moves left. Clearing all 5 tiles makes the row empty. 7. Arrow keys navigate within/across rows. 8. No letters anywhere OR a partial row exists (1-4 letters) → Update greyed/disabled. 9. ≥1 fully-typed row AND no partial rows → Update enabled. 10. Update → sends `{ rows: [{guess, result}, ...] }` → API returns candidates. 11. Rows are either fully filled (5 letters) or empty. No partial rows are sent. 12. Suggested guess / "The word is" / "No matching words" shown correctly. 13. Best Letters panel shows frequency bars. 14. Possible Words list scrolls, capped at 200. 15. LangSwitch toggles between `/solver` and `/ru/solver`. 16. "← Game" returns to `/` or `/ru/`. 17. Loading spinner during API call. 18. Mobile-responsive at 320px.