961c538Status: review
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.
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. ✅
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. ✅
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. ✅
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). ✅
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. ✅
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 }). ✅
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 }. ✅
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. ✅
[x] Task 1: Initialize monorepo with npm workspaces (AC: 5)
package.json with workspaces: ["client", "server", "shared"]shared/ package with tsconfig.jsonserver/ package with Express + TypeScript + tsconfig.jsonclient/ package with Vite + React + TypeScript + tsconfig.jsondata/ directory with placeholder word-bank.json[x] Task 2: Define shared TypeScript types (AC: 6, 7)
shared/src/api.ts with DailyResponse interfaceshared/src/game.ts with PersistedState, GameStats, GuessResult interfacesshared/src/word-bank.ts with WordBank, TargetWord interfacesshared/package.json to export types properly[x] Task 3: Implement Express server skeleton + daily word endpoint (AC: 4)
server/src/index.ts — Express app, serve static files from client/dist in productionserver/src/daily-word.ts — deterministic daily word: (daysSinceEpoch(EST/EDT) mod targetCount) + 1server/src/routes/daily.ts — GET /api/daily, loads word bank, returns { wordId }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)
client/src/main.tsx — React entry, Vite configclient/src/App.tsx — screen router (state-based: rules | game | results)client/src/screens/Screen1Rules.tsx — rules display + Play buttonclient/src/screens/Screen2Game.tsx — empty grid (5×6) + keyboard (QWERTY, light gray)client/src/components/Grid.tsx — renders grid, accepts guesses array as propsclient/src/components/Keyboard.tsx — renders keyboard, accepts letter colors mapclient/src/hooks/usePersistence.ts — read/write PersistedState to localStorage key wordle-statePersistedState, detect first visit (no rulesSeen), restore in-progress game if present[x] Task 5: Create initial word bank data (AC: 4)
data/word-bank.json with shape { guessable: string[], targets: { id: number, word: string, difficulty: number }[] }wordId = (daysSinceEpoch(EST/EDT) mod targetWordCount) + 1. Timezone: America/New_York. [Source: ARCHITECTURE-SPINE.md#AD-2]shared/ are the single source of truth. [Source: ARCHITECTURE-SPINE.md#AD-3]{ guessable: string[], targets: { id: number, word: string, difficulty: number }[] }. Loaded at startup. [Source: ARCHITECTURE-SPINE.md#AD-4]client/, server/, solver/, shared/, data/. Every package imports from shared/. [Source: ARCHITECTURE-SPINE.md#AD-9]shared/src/api.ts before implementation. [Source: ARCHITECTURE-SPINE.md#AD-10]wordle-state. One hook usePersistence owns all access. [Source: ARCHITECTURE-SPINE.md#AD-12]| 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]
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 }[] }
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).PersistedState.rulesSeen from localStorage on initial load.{ screen: 'rules' | 'game' | 'results' }. No React Router needed for 3 screens.data/word-bank.json on startup via fs.readFileSync (or async equivalent). Path resolved relative to server root. In dev, ../data/word-bank.json.daily-word.ts with known dates to verify deterministic outputGET /api/daily returns { wordId } with valid numberPersistedStateClaude
* to /{*path}{"wordId":29}package.json (root, npm workspaces)client/package.json, client/tsconfig.json, client/vite.config.ts, client/index.htmlclient/src/main.tsx, client/src/App.tsx, client/src/api.tsclient/src/screens/Screen1Rules.tsx, client/src/screens/Screen2Game.tsxclient/src/components/Grid.tsx, client/src/components/Keyboard.tsxclient/src/hooks/usePersistence.tsserver/package.json, server/tsconfig.jsonserver/src/index.ts, server/src/daily-word.ts, server/src/routes/daily.tsshared/package.json, shared/tsconfig.jsonshared/src/index.ts, shared/src/api.ts, shared/src/game.ts, shared/src/word-bank.tsdata/word-bank.json