epics.md 14 KB


stepsCompleted: [1, 2, 3, 4] inputDocuments:

  • prds/prd-wordle-2026-07-07/prd.md
  • architecture/architecture-wordle-2026-07-07/ARCHITECTURE-SPINE.md ---

Wordle Clone - Epic Breakdown

Overview

This document provides the complete epic and story breakdown for the Wordle Clone, decomposing the requirements from the PRD and Architecture into implementable stories.

Requirements Inventory

Functional Requirements

FR-1: The system can detect whether the player has previously visited and show or skip the rules screen accordingly. FR-2: The system can display the game rules: 5-letter words, 6 attempts allowed, green/yellow/gray feedback meaning, and a Play button to proceed. FR-3: The system can render a 5-column × 6-row grid. Each cell displays one guessed letter with its feedback color after submission. FR-4: The system can render an on-screen QWERTY keyboard. After each guess, keys update to reflect the best feedback for that letter across all guesses. FR-5: The player can type a 5-letter word via physical keyboard or on-screen keyboard and submit it. FR-6: The system can validate the guess against the word bank. If the guess is not in the bank, display "Unknown word," allow backspace to clear the row, and do not consume the attempt. FR-7: The system can apply the Feedback Scoring Rule to each guess: greens first (exact position matches), then yellows allocated to remaining letter count in the word, excess instances gray. FR-8: The system can detect when all 5 letters are green (correct guess) and transition to Screen 3 with a congratulations message. FR-9: The system can detect when the 6th attempt is exhausted without a correct guess and transition to Screen 3 with the correct word revealed. FR-10: The system can select and serve the same target word to all players for a given calendar day. Word changes at midnight EST/EDT. FR-11: The system can generate a new word after the daily word is completed, selected randomly from the word bank with weighting based on the player's skill level. FR-12: The system can compute and store a player skill metric derived from past game performance. FR-13: The system can show the game outcome: congratulations with attempt count (win) or correct word revealed (loss). FR-14: The system can show lifetime statistics: games played, win rate (%), attempt-count histogram (1-6 + fail), current streak, max streak. FR-15: The system can display a Play Again button on Screen 3 that triggers a new game. FR-16: The system can run at least two distinct solving algorithms against every word in the target word bank and record the number of attempts each algorithm required. FR-17: The system can produce a difficulty score for each word derived from solver attempt data, stored in a format consumable by the game. FR-18: The system can load a word bank of valid 5-letter English words, filtered for recent internet usage frequency (last ~5 years). Distinguishes guessable words from target words. FR-19: The system can persist player state (rules-seen cookie, lifetime statistics, skill metric) to browser storage and restore it on subsequent visits.

NonFunctional Requirements

NFR-1: Responsive design — the game must be usable on viewport widths from 375px (small phone) through desktop, via CSS media queries. NFR-2: Feedback latency — guess validation round-trip should complete within acceptable web-app responsiveness. No optimistic UI required for v1. NFR-3: Persistence — statistics, rules cookie, and skill metric must survive browser close and restore on next visit. NFR-4: Stateless server — the server stores no session state between requests. All context is carried in the request. NFR-5: Wire format — API feedback colors use single-character codes: 'g' (green), 'y' (yellow), 'x' (gray). NFR-6: TypeScript — all code (client, server, solver) is TypeScript with shared types in a shared/ package. NFR-7: Word bank confidentiality — the guessable word list never leaves the server. Only single-word reveal on game over.

Additional Requirements

  • Project structure: Monorepo with client/ (React + Vite), server/ (Express), solver/ (offline script), shared/ (TypeScript types), data/ (word bank JSON)
  • Starter template: React + Vite + TypeScript for client; Node.js + Express + TypeScript for server. Vite 8.x, React 19.x, Node 24 LTS, Express 5.x, TypeScript 6.x
  • API endpoints: GET /api/daily → {wordId}, POST /api/guess → {valid, colors?, correct?, word?}, POST /api/play-again → {wordId}. All contracts defined as TypeScript interfaces in shared/src/api.ts before implementation (AD-10)
  • No database: Word bank is a static JSON file (data/word-bank.json). Shape: { guessable: string[], targets: { id, word, difficulty }[] } (AD-4)
  • Daily word: Deterministic — (daysSinceEpoch(EST/EDT) mod targetCount) + 1 (AD-2)
  • Play Again: Weighted random selection around player skill level (AD-8)
  • Persistence: Single localStorage key wordle-state with typed PersistedState interface (AD-12)
  • Deployment: Home Linux server, internet-exposed. Express serves both API and static SPA build
  • Solver: Standalone TypeScript script, runs offline, writes difficulty scores into data/word-bank.json

UX Design Requirements

No UX design document provided. UX requirements are derived from PRD user journeys:

  • Three-screen flow: Rules (Screen 1) → Game (Screen 2) → Results (Screen 3)
  • CSS media queries for mobile/desktop responsive layout
  • Color feedback uses green/yellow/dark-gray backgrounds on grid cells and keyboard keys
  • Invalid word: "Unknown word" message displayed on Screen 2
  • Win: brief celebration on Screen 3 (visual feedback TBD in implementation)
  • Loss: correct word revealed on Screen 3

FR Coverage Map

FR Epic Description
FR-1 Epic 1 First-visit detection
FR-2 Epic 1 Rules display
FR-3 Epic 1 Grid rendering
FR-4 Epic 1 Keyboard rendering and highlighting
FR-5 Epic 1 Guess submission
FR-6 Epic 1 Invalid word rejection
FR-7 Epic 1 Feedback scoring
FR-8 Epic 1 Win detection
FR-9 Epic 1 Loss detection
FR-10 Epic 1 Daily word selection
FR-11 Epic 1 Play Again word selection
FR-12 Epic 1 Player skill tracking
FR-13 Epic 1 Results display
FR-14 Epic 1 Statistics display
FR-15 Epic 1 Play Again button
FR-18 Epic 1 Word bank curation
FR-19 Epic 1 Browser persistence
FR-16 Epic 2 Multi-algorithm solver
FR-17 Epic 2 Word ranking output

Epic List

Epic 1: Play the Game

The player can play the complete Wordle clone: open the browser, see rules (once), guess the daily word with green/yellow/gray feedback, view results with lifetime statistics, and play again with skill-appropriate words. End-to-end: React SPA client + Express REST API + static word bank + browser persistence.

FRs covered: FR-1, FR-2, FR-3, FR-4, FR-5, FR-6, FR-7, FR-8, FR-9, FR-10, FR-11, FR-12, FR-13, FR-14, FR-15, FR-18, FR-19

Story 1.1: Project Foundation & Daily Word Setup

As a player, I want to open the game in my browser and see the game board ready to play, So that I can start playing without friction.

Acceptance Criteria:

Given I am a first-time visitor with no prior cookie, When I open the game URL, Then Screen 1 displays: game rules (5 letters, 6 attempts, green/yellow/gray meaning) and a Play button. And tapping Play navigates to Screen 2 with an empty 5×6 grid and QWERTY keyboard in light gray.

Given I am a returning visitor with a prior-visit cookie and no game in progress, When I open the game URL, Then I land directly on Screen 2 (game board) without seeing the rules screen.

Given I am a returning visitor who previously closed the browser mid-game, When I open the game URL, Then my in-progress game state (word ID, guesses so far with their colors, attempt number) is restored from localStorage. And I continue from where I left off.

Given the server has a word bank loaded, When the client calls GET /api/daily, Then the response contains { wordId: number } where wordId is the same for all players on the same calendar day (EST/EDT).

Given the project is initialized, When I run the client and server, Then the monorepo structure matches: client/ (React+Vite), server/ (Express), shared/ (TypeScript types), data/ (word-bank.json). And shared/src/api.ts defines concrete TypeScript interfaces for GET /api/daily request/response shapes. And the client renders a responsive layout usable at 375px (phone) through desktop widths.

Story 1.2: Guess Validation & Feedback

As a player, I want to type 5-letter guesses and see color feedback on each letter, So that I can deduce the target word within 6 attempts, and resume if I close the browser.

Acceptance Criteria:

Given I am on Screen 2 with a daily word loaded, When I type a valid 5-letter word and submit (Enter or on-screen), Then the client sends POST /api/guess { wordId, guess, tryNo } to the server. And the server validates the guess against the word bank. And the server returns { valid: true, colors: ['x','y','g','x','x'], correct: false } with 5 single-char codes. And the grid row fills with the guess letters and each cell shows the corresponding background color (green='g', yellow='y', dark gray='x'). And the keyboard updates with each key's best feedback color across all guesses.

Given I type a word not in the guessable word bank, When I submit, Then the server returns { valid: false }. And the client displays "Unknown word" message. And I can backspace all letters on the current row. And the attempt counter does not increment.

Given I make a guess and receive feedback, When the guess response arrives from the server, Then the current game state is saved to localStorage: { wordId, guesses: [{ guess, colors }] }. And an empty or absent guesses array means no game in progress. And game status is derived: last guess all-green → won; 6 guesses without all-green → lost; else playing.

Given I submit a guess where all 5 letters match the target word in the correct positions, When the response has correct: true, difficulty: 3.2, Then the client transitions to Screen 3 showing "Congratulations!" and "Solved in N attempts." And lifetime statistics update: games played incremented, win recorded, streak incremented, attempt-count histogram updated. And the in-progress game is cleared from localStorage.

Given I submit a 6th guess that is not correct, When the server responds with correct: false, word: "crane", difficulty: 4.1, Then the client transitions to Screen 3 showing "Better luck next time" and "The word was: crane." And lifetime statistics update: games played incremented, loss recorded, streak reset. And the in-progress game is cleared from localStorage.

Given I close the browser and reopen, When I return to the game, Then my lifetime statistics (games played, win rate, histogram, streak) are restored from localStorage key wordle-state. And my rules-seen flag is restored.

Given I am on Screen 3, When I view the statistics, Then I see: games played, win rate percentage, attempt-count histogram (1 through 6 + fail), current streak, max streak.

Story 1.3: Play Again & Skill Tracking

As a player, I want to play additional rounds after the daily word, with words appropriate to my skill level, So that I can keep playing and the challenge adapts to me.

Acceptance Criteria:

Given I am on Screen 3 after completing or failing a game, When I view the screen, Then a Play Again button is visible.

Given I tap Play Again, When the client sends POST /api/play-again { skillMetric: number }, Then the server selects a word from the target bank using weighted random selection (words near the player's skill level are more likely). And the response contains { wordId: number }. And the client navigates to Screen 2 with an empty grid for the new word.

Given I complete any game (win or loss), When the final guess response includes difficulty: number, Then the client uses this difficulty value to update statistics. And the client recalculates the skill metric from lifetime statistics (average attempts weighted by word difficulty). And the updated skill metric is persisted to localStorage.

Given I tap Play Again repeatedly, When I play multiple Play Again rounds, Then each request sends the latest skill metric for word selection. And no repeat-word prevention is applied (target set is large enough).

Given the server has no solver-ranked word bank yet (Epic 2 not complete), When Play Again selects a word, Then all target words have a default neutral difficulty score (e.g., 3.0), so selection is effectively random pending solver data.

Epic 2: Solver Engine & Word Ranking

Computer solver pre-computes attempt counts for every word using multiple algorithms and produces objective difficulty scores. Replaces manual/default difficulty estimates with data-driven rankings that improve Play Again word selection.

FRs covered: FR-16, FR-17

Story 2.1: Solver Script & Word Ranking

As a developer, I want a solver script that pre-computes attempt counts for every target word using multiple algorithms, So that word difficulty scores are data-driven, not manual estimates.

Acceptance Criteria:

Given the solver script is run against the word bank, When it processes each target word with at least two solving algorithms, Then it records per-algorithm attempt counts for each word. And it computes an aggregated difficulty score per word (e.g., average attempts across algorithms). And it writes the updated data/word-bank.json with difficulty fields populated.

Given the solver output exists, When the server loads data/word-bank.json at startup, Then Play Again word selection uses real difficulty scores instead of default neutral values.

Given the solver is implemented in TypeScript, When it imports word bank types from shared/, Then it uses the same WordBank and TargetWord interfaces as the server.

Given a word is not solvable within 6 attempts by any algorithm, When the solver processes it, Then that word is flagged or removed from the target set.