|
|
@@ -0,0 +1,328 @@
|
|
|
+# 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.
|