# Story 1.2: Guess Validation & Feedback

Status: review

## Story

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

1. **Guess submission:** 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. The server validates against the word bank and returns `{ valid: true, colors: ['x','y','g','x','x'], correct: false }`. The grid row fills and each cell shows the color background. ✅

2. **Invalid word:** Given I type a word not in the guessable word bank, when I submit, then the server returns `{ valid: false }`. Client displays "Unknown word", allows backspace, and attempt counter does not increment. ✅

3. **Keyboard highlighting:** Given I make guesses, when the keyboard renders, then each key shows the best feedback for that letter across all guesses (g > y > x). Unused letters remain light gray. ✅

4. **Win:** Given my guess matches the target word (all green), when response has `correct: true, difficulty: 3.2`, then client transitions to Screen 3 showing "Congratulations! Solved in N attempts." Stats update and in-progress game is cleared. ✅

5. **Loss:** Given my 6th guess is wrong, when response has `correct: false, word: "crane", difficulty: 4.1`, then client transitions to Screen 3 showing "Better luck next time. The word was: crane." Stats update and in-progress game is cleared. ✅

6. **Persistence:** Given I make a guess, when the response arrives, then game state `{ wordId, guesses: [{ guess, colors }] }` is saved to localStorage. Empty guesses array = no game in progress. Status derived: last guess all-green → won; 6 without all-green → lost; else playing. ✅

7. **Statistics:** Given I am on Screen 3, when I view statistics, then I see: games played, win rate %, attempt-count histogram (1-6 + fail), current streak, max streak. ✅

8. **Statistics persist:** Given I close and reopen the browser, when I return, then my lifetime statistics are restored from localStorage. ✅

## Tasks / Subtasks

- [x] Task 1: Implement POST /api/guess endpoint (AC: 1, 2, 5)
  - [x] Create `server/src/feedback.ts` — Feedback Scoring Rule
  - [x] Create `server/src/validate.ts` — word bank lookup
  - [x] Create `server/src/routes/guess.ts` — POST /api/guess endpoint
  - [x] Register route in `server/src/index.ts`

- [x] Task 2: Update shared API types (AC: 1, 5)
  - [x] Verified `GuessRequest` and `GuessResponse` interfaces match implementation

- [x] Task 3: Implement guess entry UI (AC: 1, 2, 3)
  - [x] Create `client/src/hooks/useGame.ts` — guess management, status, API calls
  - [x] Add physical keyboard handler (keydown)
  - [x] Add on-screen keyboard click handler
  - [x] Show "Unknown word" message on invalid guess
  - [x] Wire App.tsx to useGame, pass state to Screen2Game

- [x] Task 4: Implement Screen 3 — Results (AC: 4, 5, 7)
  - [x] Create `client/src/screens/Screen3Results.tsx` — results + stats + histogram
  - [x] Wire win/loss → Screen 3 transition
  - [x] Clear in-progress game on game end
  - [x] Update lifetime statistics and persist

- [x] Task 5: Integrate and test end-to-end (all ACs)
  - [x] Tested: valid guess feedback (hello vs style), invalid word, win, loss with word reveal
  - [x] All typechecks pass, client builds

## Dev Notes

### Building on Story 1.1

The foundation from 1.1 is in place:
- Monorepo with npm workspaces ✅
- Shared types (`shared/src/api.ts`, `shared/src/game.ts`) ✅
- Express server with GET /api/daily ✅
- React SPA with Screen 1 (rules) and Screen 2 (game shell) ✅
- localStorage persistence via `usePersistence` hook ✅
- Word bank JSON with 500+ guessable words and 40 targets ✅

### Architecture Compliance

- **AD-5 Server-side validation only:** Word list never leaves server. Only single-word reveal on loss. [Source: ARCHITECTURE-SPINE.md#AD-5]
- **AD-6 Feedback scoring on server:** Single implementation in `server/src/feedback.ts`. Client only renders colors. [Source: ARCHITECTURE-SPINE.md#AD-6]
- **AD-11 Wire format:** Colors as `'g'`/`'y'`/`'x'`. 5-element array. [Source: ARCHITECTURE-SPINE.md#AD-11]
- **AD-7 Client owns game state:** Client tracks attempt number, guess history, game-over detection. Server stateless. [Source: ARCHITECTURE-SPINE.md#AD-7]
- **AD-12 Single persistence key:** All state under `wordle-state`. One hook `usePersistence`. [Source: ARCHITECTURE-SPINE.md#AD-12]

### Previous Story Intelligence (1.1)

- **Vite 7 not 8:** @vitejs/plugin-react@4.7.0 doesn't support Vite 8 peer. Stick with Vite 7.
- **Express 5 catch-all syntax:** `/{*path}` not `*`
- **Timezone fix applied:** `daily-word.ts` uses `Intl.DateTimeFormat` not `toLocaleString`
- **COLOR_MAP duplication:** Grid and Keyboard both define the same color map — consider extracting to `shared/` or a constants file in this story

### Feedback Scoring Rule (FR-7)

```
Given target word W and guess G:
1. Mark exact-position matches (W[i] === G[i]) as 'g', decrement remaining count for that letter
2. For remaining guess positions, mark as 'y' if letter exists in W with remaining count > 0, decrement
3. All other positions are 'x'
```

Example: W=`style`, G=`hello` → `['x','y','x','g','x']`

### API Contract (POST /api/guess)

**Request:** `{ wordId: number, guess: string, tryNo: number }`
**Response (valid):** `{ valid: true, colors: string[], correct: boolean, difficulty?: number }`
**Response (valid, loss):** `{ valid: true, colors: string[], correct: false, word: string, difficulty: number }`
**Response (invalid):** `{ valid: false }`

### Files to Create/Modify

**New:**
- `server/src/feedback.ts`
- `server/src/validate.ts`
- `server/src/routes/guess.ts`
- `client/src/hooks/useGame.ts`
- `client/src/screens/Screen3Results.tsx`

**Modify:**
- `server/src/index.ts` — add guess router
- `client/src/App.tsx` — integrate useGame, add Screen 3 routing, wire persistence
- `client/src/components/Keyboard.tsx` — add onClick handler for letter keys
- `client/src/screens/Screen2Game.tsx` — accept new props (onKeyPress, message)

### Out of Scope

- POST /api/play-again endpoint (Story 1.3)
- Play Again button functionality on Screen 3 (Story 1.3)
- Skill metric tracking (Story 1.3)
- Hints on attempts 5-6 (post-v1)
- Difficulty display on results screen (post-v1)

### Testing Notes

- Test feedback scoring: known word/guess pairs with expected output
- Test invalid word: guess not in word bank → valid=false
- Test win: all-green → correct=true, difficulty present
- Test loss: 6th wrong guess → correct=false, word revealed
- Test keyboard: best-feedback logic (g overrides y, y overrides x)
- Test persistence: close tab, reopen, mid-game state restored
- Test statistics: games played, histogram, streak after multiple games

### References

- [Source: _bmad-output/planning-artifacts/prds/prd-wordle-2026-07-07/prd.md#FR-5 through FR-9, FR-13, FR-14, FR-19]
- [Source: _bmad-output/planning-artifacts/architecture/architecture-wordle-2026-07-07/ARCHITECTURE-SPINE.md]
- [Source: _bmad-output/planning-artifacts/epics.md#Story 1.2]
- [Source: _bmad-output/implementation-artifacts/1-1-project-foundation-daily-word-setup.md]

## Dev Agent Record

### Agent Model Used

Claude

### Debug Log References

### Completion Notes List

### File List
