10-feature-wordle-solver.md 13 KB

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:

{
  "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):

  • 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.