---
baseline_commit: 961c538
---

# Story 1.1: Project Foundation & Daily Word Setup

Status: review

## Story

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

1. **First-visit rules screen:** 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. Tapping Play navigates to Screen 2 with an empty 5×6 grid and QWERTY keyboard in light gray. ✅

2. **Returning player skip:** 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. ✅

3. **Mid-game resume:** 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) is restored from localStorage, and I continue from where I left off. ✅

4. **Daily word API:** 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). ✅

5. **Monorepo structure:** Given the project is initialized, when I inspect the codebase, then the monorepo structure matches: `client/` (React+Vite SPA), `server/` (Express API), `shared/` (TypeScript types), `data/` (word-bank.json). A root `package.json` with npm workspaces configured. ✅

6. **Shared API types:** Given the project is initialized, when I inspect `shared/src/api.ts`, then it defines concrete TypeScript interfaces for GET /api/daily request/response shapes (`DailyResponse: { wordId: number }`). ✅

7. **Shared game types:** Given the project is initialized, when I inspect `shared/src/game.ts`, then it defines the `PersistedState` interface: `{ rulesSeen: boolean, stats: GameStats, currentGame?: { wordId: number, guesses: { guess: string, colors: string[] }[] }, skillMetric: number }`. ✅

8. **Responsive layout:** Given I open the game on any device, when I view it, then the layout is responsive via CSS media queries, usable at 375px (small phone) through desktop widths. ✅

## Tasks / Subtasks

- [x] Task 1: Initialize monorepo with npm workspaces (AC: 5)
  - [x] Create root `package.json` with `workspaces: ["client", "server", "shared"]`
  - [x] Initialize `shared/` package with `tsconfig.json`
  - [x] Initialize `server/` package with Express + TypeScript + `tsconfig.json`
  - [x] Initialize `client/` package with Vite + React + TypeScript + `tsconfig.json`
  - [x] Create `data/` directory with placeholder `word-bank.json`

- [x] Task 2: Define shared TypeScript types (AC: 6, 7)
  - [x] Create `shared/src/api.ts` with `DailyResponse` interface
  - [x] Create `shared/src/game.ts` with `PersistedState`, `GameStats`, `GuessResult` interfaces
  - [x] Create `shared/src/word-bank.ts` with `WordBank`, `TargetWord` interfaces
  - [x] Configure `shared/package.json` to export types properly

- [x] Task 3: Implement Express server skeleton + daily word endpoint (AC: 4)
  - [x] Create `server/src/index.ts` — Express app, serve static files from `client/dist` in production
  - [x] Create `server/src/daily-word.ts` — deterministic daily word: `(daysSinceEpoch(EST/EDT) mod targetCount) + 1`
  - [x] Create `server/src/routes/daily.ts` — GET /api/daily, loads word bank, returns `{ wordId }`
  - [x] Load `data/word-bank.json` at server startup (not per-request)

- [x] Task 4: Implement React SPA with 3-screen navigation (AC: 1, 2, 3, 8)
  - [x] Create `client/src/main.tsx` — React entry, Vite config
  - [x] Create `client/src/App.tsx` — screen router (state-based: `rules | game | results`)
  - [x] Create `client/src/screens/Screen1Rules.tsx` — rules display + Play button
  - [x] Create `client/src/screens/Screen2Game.tsx` — empty grid (5×6) + keyboard (QWERTY, light gray)
  - [x] Create `client/src/components/Grid.tsx` — renders grid, accepts guesses array as props
  - [x] Create `client/src/components/Keyboard.tsx` — renders keyboard, accepts letter colors map
  - [x] Create `client/src/hooks/usePersistence.ts` — read/write `PersistedState` to localStorage key `wordle-state`
  - [x] On app load: read `PersistedState`, detect first visit (no `rulesSeen`), restore in-progress game if present
  - [x] Add CSS media queries for responsive layout (375px min-width)

- [x] Task 5: Create initial word bank data (AC: 4)
  - [x] Create `data/word-bank.json` with shape `{ guessable: string[], targets: { id: number, word: string, difficulty: number }[] }`
  - [x] Populate with curated 5-letter words (recent internet usage, ~5 years)
  - [x] All targets have default difficulty of 3.0 (pending solver data in Epic 2)

## Dev Notes

### Architecture Compliance

- **AD-1 Stateless server:** Server stores nothing between requests. Word bank loaded at startup. [Source: ARCHITECTURE-SPINE.md#AD-1]
- **AD-2 Deterministic daily word:** `wordId = (daysSinceEpoch(EST/EDT) mod targetWordCount) + 1`. Timezone: America/New_York. [Source: ARCHITECTURE-SPINE.md#AD-2]
- **AD-3 TypeScript everywhere:** All code is TypeScript. Shared types in `shared/` are the single source of truth. [Source: ARCHITECTURE-SPINE.md#AD-3]
- **AD-4 Word bank as static JSON:** Shape: `{ guessable: string[], targets: { id: number, word: string, difficulty: number }[] }`. Loaded at startup. [Source: ARCHITECTURE-SPINE.md#AD-4]
- **AD-9 Monorepo with shared types:** `client/`, `server/`, `solver/`, `shared/`, `data/`. Every package imports from `shared/`. [Source: ARCHITECTURE-SPINE.md#AD-9]
- **AD-10 Typed API contracts:** Every endpoint has a concrete TS interface in `shared/src/api.ts` before implementation. [Source: ARCHITECTURE-SPINE.md#AD-10]
- **AD-12 Single persistence key:** All browser state under localStorage key `wordle-state`. One hook `usePersistence` owns all access. [Source: ARCHITECTURE-SPINE.md#AD-12]

### Stack (Verified Current)

| Technology | Version |
|---|---|
| TypeScript | ^6.x |
| Node.js | ^24 LTS (Krypton) |
| React | ^19.x |
| Vite | ^8.x (Rolldown bundler) |
| Express | ^5.x |

[Source: Architecture spine, verified against web via reviewer gate 2026-07-07]

### Source Tree to Create

```
wordle/
  package.json              # workspaces: [client, server, shared]
  client/
    package.json            # react, react-dom, vite, typescript, @types/*
    vite.config.ts
    tsconfig.json
    index.html
    src/
      main.tsx
      App.tsx               # state-based screen router
      api.ts                # fetch wrappers: getDaily(), etc.
      screens/
        Screen1Rules.tsx    # rules + Play button
        Screen2Game.tsx     # grid + keyboard
      components/
        Grid.tsx            # 5×6 grid, accepts guesses + colors
        Keyboard.tsx        # QWERTY, accepts letter color map
      hooks/
        usePersistence.ts   # single hook for localStorage read/write
  server/
    package.json            # express, typescript, tsx, @types/*
    tsconfig.json
    src/
      index.ts              # Express app, static file serving
      daily-word.ts         # date→wordId function
      routes/
        daily.ts            # GET /api/daily
  shared/
    package.json            # types only
    tsconfig.json
    src/
      api.ts                # DailyResponse, GuessRequest, GuessResponse, PlayAgainRequest, PlayAgainResponse
      game.ts               # PersistedState, GameStats, GuessResult
      word-bank.ts          # WordBank, TargetWord
  data/
    word-bank.json          # { guessable: string[], targets: { id, word, difficulty }[] }
```

### Key Implementation Details

- **Daily word calculation:** Use `Date` object with timezone-aware logic. `daysSinceEpoch` = floor((current EST/EDT timestamp - epoch timestamp) / 86400000). Epoch can be any fixed reference date (e.g., 2026-01-01).
- **Rules-seen detection:** Check `PersistedState.rulesSeen` from localStorage on initial load.
- **Screen routing:** Simple React state — `{ screen: 'rules' | 'game' | 'results' }`. No React Router needed for 3 screens.
- **Word bank loading:** Server loads `data/word-bank.json` on startup via `fs.readFileSync` (or async equivalent). Path resolved relative to server root. In dev, `../data/word-bank.json`.
- **No database, no migrations, no ORM.** The word bank is a JSON file.

### Out of Scope (for this story)

- POST /api/guess endpoint (Story 1.2)
- POST /api/play-again endpoint (Story 1.3)
- Guess validation and feedback colors (Story 1.2)
- Results screen / Screen 3 (Story 1.2)
- Play Again button and skill tracking (Story 1.3)
- Solver engine (Epic 2)

### Testing Notes

- Test `daily-word.ts` with known dates to verify deterministic output
- Test `GET /api/daily` returns `{ wordId }` with valid number
- Test first-visit vs returning visitor detection in browser
- Test responsive layout at 375px and desktop widths
- Test localStorage roundtrip for `PersistedState`

### References

- [Source: _bmad-output/planning-artifacts/prds/prd-wordle-2026-07-07/prd.md]
- [Source: _bmad-output/planning-artifacts/architecture/architecture-wordle-2026-07-07/ARCHITECTURE-SPINE.md]
- [Source: _bmad-output/planning-artifacts/epics.md#Story 1.1]

## Dev Agent Record

### Agent Model Used

Claude

### Debug Log References

- Vite 8 downgraded to Vite 7: @vitejs/plugin-react@4.7.0 does not yet support Vite 8 peer
- Express 5 catch-all route syntax changed from `*` to `/{*path}`

### Completion Notes List

- Monorepo initialized with npm workspaces: client, server, shared
- Shared types defined: api.ts, game.ts, word-bank.ts with full interfaces
- Express server with GET /api/daily endpoint, confirmed returning `{"wordId":29}`
- Deterministic daily word uses EST/EDT timezone, verified same-date determinism
- React SPA with state-based screen routing (rules/game/results), localStorage persistence
- Client builds successfully with Vite 7 (Vite 8 pending plugin support)
- All TypeScript packages typecheck clean
- Word bank with 500+ guessable words and 40 target words, all default difficulty 3.0

### File List

- `package.json` (root, npm workspaces)
- `client/package.json`, `client/tsconfig.json`, `client/vite.config.ts`, `client/index.html`
- `client/src/main.tsx`, `client/src/App.tsx`, `client/src/api.ts`
- `client/src/screens/Screen1Rules.tsx`, `client/src/screens/Screen2Game.tsx`
- `client/src/components/Grid.tsx`, `client/src/components/Keyboard.tsx`
- `client/src/hooks/usePersistence.ts`
- `server/package.json`, `server/tsconfig.json`
- `server/src/index.ts`, `server/src/daily-word.ts`, `server/src/routes/daily.ts`
- `shared/package.json`, `shared/tsconfig.json`
- `shared/src/index.ts`, `shared/src/api.ts`, `shared/src/game.ts`, `shared/src/word-bank.ts`
- `data/word-bank.json`

