|
@@ -4,29 +4,51 @@ type: architecture-spine
|
|
|
purpose: build-substrate
|
|
purpose: build-substrate
|
|
|
altitude: feature
|
|
altitude: feature
|
|
|
paradigm: 'SPA + REST API, stateless server'
|
|
paradigm: 'SPA + REST API, stateless server'
|
|
|
-scope: 'Wordle Clone v1 — web application, solver engine, word bank'
|
|
|
|
|
|
|
+scope: 'Wordle Clone — bilingual (EN/RU) web application, solver engine, word bank, solver tool'
|
|
|
status: final
|
|
status: final
|
|
|
created: '2026-07-07'
|
|
created: '2026-07-07'
|
|
|
-updated: '2026-07-07'
|
|
|
|
|
|
|
+updated: '2026-08-14'
|
|
|
binds: ['FR-1'..'FR-19', 'UJ-1'..'UJ-5']
|
|
binds: ['FR-1'..'FR-19', 'UJ-1'..'UJ-5']
|
|
|
-sources: ['prds/prd-wordle-2026-07-07/prd.md']
|
|
|
|
|
|
|
+sources:
|
|
|
|
|
+ - 'prds/prd-wordle-2026-07-07/prd.md'
|
|
|
|
|
+ - 'raw/01…13 (post-v1 change log)'
|
|
|
|
|
+ - 'source code (reconciled 2026-08-14)'
|
|
|
companions: []
|
|
companions: []
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
# Architecture Spine — Wordle Clone
|
|
# Architecture Spine — Wordle Clone
|
|
|
|
|
|
|
|
|
|
+> **Reconciliation note (2026-08-14):** this document has been updated to match the
|
|
|
|
|
+> implemented code, which evolved past the original v1 design via the changes logged in
|
|
|
|
|
+> `raw/01…13`. Decisions marked `[ADOPTED]` reflect what is in the code, not the original
|
|
|
|
|
+> design intent. Where a decision changed materially, the change is noted inline.
|
|
|
|
|
+
|
|
|
## Design Paradigm
|
|
## Design Paradigm
|
|
|
|
|
|
|
|
-**SPA + stateless REST API.** The client is a single-page application loaded once; all subsequent interaction is REST calls against a stateless server. The server holds no session state — every request carries the context it needs (word ID, attempt number, skill metric). Deterministic computation replaces stored state wherever possible (daily word ID is a pure function of the date).
|
|
|
|
|
|
|
+**SPA + stateless REST API.** The client is a single-page application loaded once; all
|
|
|
|
|
+subsequent interaction is REST calls against a stateless server. The server holds no
|
|
|
|
|
+session state — every request carries the context it needs (word ID, attempt number, skill
|
|
|
|
|
+metric). Deterministic computation replaces stored state wherever possible (daily word ID
|
|
|
|
|
+is a pure function of the date; word difficulty is a pure function of solver output,
|
|
|
|
|
+computed and cached at runtime rather than stored).
|
|
|
|
|
+
|
|
|
|
|
+The application is **bilingual (English + Russian)**. The same server and SPA serve both
|
|
|
|
|
+languages via URL-prefixed routes (`/api` vs `/ru/api`, `/solver` vs `/ru/solver`) and
|
|
|
|
|
+language-specific word banks.
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
Browser (React SPA) ─── REST ─── Server (Express, stateless)
|
|
Browser (React SPA) ─── REST ─── Server (Express, stateless)
|
|
|
│
|
|
│
|
|
|
- ├── data/word-bank.json (static)
|
|
|
|
|
- └── solver output (pre-computed, static)
|
|
|
|
|
|
|
+ ├── data/word-bank.json (EN, static)
|
|
|
|
|
+ ├── data/word-bank-ru.json (RU, static)
|
|
|
|
|
+ └── solver cache (lazy, in-memory)
|
|
|
|
|
+ └── forked child process for full 4-solver run
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-**Layer map:** `client/` (presentation + game logic) → `server/` (REST API, guess validation, word selection) → `data/` (word bank, solver rankings). `shared/` holds TypeScript types consumed by all layers. `solver/` is an offline script, not part of the runtime.
|
|
|
|
|
|
|
+**Layer map:** `client/` (presentation + game state) → `server/` (REST API, guess
|
|
|
|
|
+validation, feedback, word selection, solver filtering) → `data/` (word banks). `shared/`
|
|
|
|
|
+holds the TypeScript types **and the solver algorithms** consumed by all layers. `solver/`
|
|
|
|
|
+is an offline validation CLI, not part of the runtime.
|
|
|
|
|
|
|
|
## Invariants & Rules
|
|
## Invariants & Rules
|
|
|
|
|
|
|
@@ -34,55 +56,60 @@ Browser (React SPA) ─── REST ─── Server (Express, stateless)
|
|
|
|
|
|
|
|
- **Binds:** server/
|
|
- **Binds:** server/
|
|
|
- **Prevents:** server-side session state, in-memory game tracking, per-player storage on the server
|
|
- **Prevents:** server-side session state, in-memory game tracking, per-player storage on the server
|
|
|
-- **Rule:** Every request carries all context needed to process it. The server stores nothing between requests. `[ADOPTED]`
|
|
|
|
|
|
|
+- **Rule:** Every request carries all context needed to process it. The server stores nothing between requests. The only in-memory state is the solver *cache*, which is derived data (keyed by word ID), not per-player state. `[ADOPTED]`
|
|
|
|
|
|
|
|
### AD-2 — Deterministic daily word
|
|
### AD-2 — Deterministic daily word
|
|
|
|
|
|
|
|
-- **Binds:** GET /api/daily, server/src/daily.ts
|
|
|
|
|
|
|
+- **Binds:** GET /api/daily, GET /ru/api/daily, server/src/daily-word.ts
|
|
|
- **Prevents:** stored daily-word state, cron jobs for rotation, clock-drift between requests
|
|
- **Prevents:** stored daily-word state, cron jobs for rotation, clock-drift between requests
|
|
|
-- **Rule:** Daily word ID = `(daysSinceEpoch(EST/EDT) mod targetWordCount) + 1`. Same date always maps to same word for every player. Timezone: America/New_York. `[ADOPTED]`
|
|
|
|
|
|
|
+- **Rule:** Daily word ID = `daysSinceEpoch(America/New_York) mod targetCount`, **0-indexed** and used directly as an array index into `targets` (there is **no `+1`**). Same date always maps to same word for every player. Epoch is a fixed reference (2026-01-01 EST). The date is extracted in the target timezone via `Intl.DateTimeFormat('en-CA', { timeZone: 'America/New_York' })` to avoid server-timezone drift. `[ADOPTED]`
|
|
|
|
|
|
|
|
### AD-3 — TypeScript everywhere
|
|
### AD-3 — TypeScript everywhere
|
|
|
|
|
|
|
|
- **Binds:** all
|
|
- **Binds:** all
|
|
|
- **Prevents:** multi-language drift, duplicate type definitions, solver/API type mismatches
|
|
- **Prevents:** multi-language drift, duplicate type definitions, solver/API type mismatches
|
|
|
-- **Rule:** All code (client, server, solver) is TypeScript. Shared types in `shared/` are the single source of truth for API contracts, word bank shape, and game types. `[ADOPTED]`
|
|
|
|
|
|
|
+- **Rule:** All code (client, server, solver, shared) is TypeScript. `shared/` is the single source of truth for API contracts, word-bank shape, game types, **and the solver algorithms**. `[ADOPTED]`
|
|
|
|
|
|
|
|
-### AD-4 — Word bank as static data
|
|
|
|
|
|
|
+### AD-4 — Word bank as static data; difficulty computed at runtime
|
|
|
|
|
|
|
|
-- **Binds:** server/, solver/, data/
|
|
|
|
|
-- **Prevents:** database dependency, migration overhead, runtime word-list mutation
|
|
|
|
|
-- **Rule:** The word bank is a single static JSON file (`data/word-bank.json`) loaded at server startup. Shape: `{ guessable: string[], targets: { id: number, word: string, difficulty: number }[] }`. The `difficulty` field is a single aggregated score (e.g., average attempts across all solver algorithms). The solver script produces this shape; the server consumes it. Per-algorithm attempt counts are solver-internal and not exposed in the bank. `[ADOPTED]`
|
|
|
|
|
|
|
+- **Binds:** server/, solver/, shared/src/word-bank.ts, data/
|
|
|
|
|
+- **Prevents:** database dependency, migration overhead, stale pre-computed difficulty in the JSON
|
|
|
|
|
+- **Rule:** The word bank is a static JSON file loaded once at server startup. Shape:
|
|
|
|
|
+ `{ guessable: string[], targets: { word: string, hint?: string }[] }`.
|
|
|
|
|
+ - A target's **ID is its array index** (there is no `id` field — removed in raw/08).
|
|
|
|
|
+ - **Difficulty and solver replays are NOT stored** in the JSON (removed in raw/01/02); they are computed at runtime from solver output and cached (see AD-14).
|
|
|
|
|
+ - `hint` is an optional synonym shown after the 5th unsuccessful guess (raw/06).
|
|
|
|
|
+ The solver does **not** write difficulty back into `data/`. `[ADOPTED]`
|
|
|
|
|
|
|
|
### AD-5 — Guess validation server-side only
|
|
### AD-5 — Guess validation server-side only
|
|
|
|
|
|
|
|
- **Binds:** server/src/validate.ts, POST /api/guess
|
|
- **Binds:** server/src/validate.ts, POST /api/guess
|
|
|
- **Prevents:** word bank leaking to client, client-side validation drift from server
|
|
- **Prevents:** word bank leaking to client, client-side validation drift from server
|
|
|
-- **Rule:** The guessable word list never leaves the server. Every guess is validated server-side against the full word bank. Invalid guesses return `{ valid: false }` without consuming an attempt. **Exception:** On game over (loss, `tryNo=6 && !correct`), the correct word string is returned in the guess response — this is a single-word reveal, not a list leak. `[ADOPTED]`
|
|
|
|
|
|
|
+- **Rule:** The guessable word list never leaves the server in the core game flow. Every guess is validated server-side against the full word bank. Invalid guesses return `{ valid: false }` without consuming an attempt. **Exception 1:** on loss (`tryNo=6 && !correct`) the correct word is returned — a single-word reveal, not a list leak. **Exception 2 (post-v1, deliberate):** the solver tool endpoint (`POST /solver`) returns filtered *candidate lists*, capped at 200 words, to keep the SPA lean (raw/10). `[ADOPTED]`
|
|
|
|
|
|
|
|
### AD-6 — Feedback scoring on server
|
|
### AD-6 — Feedback scoring on server
|
|
|
|
|
|
|
|
-- **Binds:** server/src/feedback.ts, POST /api/guess
|
|
|
|
|
|
|
+- **Binds:** server/src/feedback.ts, shared/src/solvers/feedback.ts, POST /api/guess
|
|
|
- **Prevents:** client and server computing feedback differently, scoring rule divergence
|
|
- **Prevents:** client and server computing feedback differently, scoring rule divergence
|
|
|
-- **Rule:** The Feedback Scoring Rule (greens first, yellows to remaining count, excess gray) is implemented once on the server. The client only renders the colors it receives. `[ADOPTED]`
|
|
|
|
|
|
|
+- **Rule:** The Feedback Scoring Rule (greens first, yellows to remaining count, excess gray) is implemented on the server; the client only renders the colors it receives. Note: there are **two** implementations that must stay in sync — `server/src/feedback.ts` (returns a `string[]`) and `shared/src/solvers/feedback.ts` (returns a `'ggggg'` string + `matchesFeedback()`, used by the solvers). `[ADOPTED]`
|
|
|
|
|
|
|
|
### AD-7 — Client owns game state
|
|
### AD-7 — Client owns game state
|
|
|
|
|
|
|
|
- **Binds:** client/
|
|
- **Binds:** client/
|
|
|
- **Prevents:** server needing to track attempts, win/loss, or game progress
|
|
- **Prevents:** server needing to track attempts, win/loss, or game progress
|
|
|
-- **Rule:** The client tracks: current attempt number, guess history, game-over detection, rules-seen cookie, lifetime statistics, and skill metric. All persist to browser storage. The server never knows what attempt the player is on except what `tryNo` the client sends. `[ADOPTED]`
|
|
|
|
|
|
|
+- **Rule:** The client tracks: current attempt number, guess history, game-over detection, rules-seen flag, lifetime statistics, and the skill metric. All persist to browser storage. The server never knows what attempt the player is on except what `tryNo` the client sends. The **skill metric is a client-side running average of word difficulty** (starts at 3.0, updated after each completed game). `[ADOPTED]`
|
|
|
|
|
|
|
|
### AD-8 — Word selection by Gaussian sampling
|
|
### AD-8 — Word selection by Gaussian sampling
|
|
|
|
|
|
|
|
- **Binds:** POST /api/play-again, server/src/play-again.ts
|
|
- **Binds:** POST /api/play-again, server/src/play-again.ts
|
|
|
- **Prevents:** fixed-difficulty buckets, deterministic word ordering in play-again mode
|
|
- **Prevents:** fixed-difficulty buckets, deterministic word ordering in play-again mode
|
|
|
-- **Rule:** Play Again word selection is weighted random: words near the player's skill level are more likely, harder words become more likely as skill improves. Specific distribution (Gaussian or other) is an implementation detail. No repeat-word tracking — the target set is large enough that repeats are negligible. `[ADOPTED]`
|
|
|
|
|
|
|
+- **Rule:** Play Again word selection is weighted random — weight for each word is `1 / (1 + (difficulty - skill)^2)` (a Gaussian-like falloff). Words near the player's skill are more likely. Uncached words default to difficulty 3.0 until played. No repeat-word tracking — the target set is large enough that repeats are negligible. `[ADOPTED]`
|
|
|
|
|
|
|
|
### AD-9 — Monorepo with shared types
|
|
### AD-9 — Monorepo with shared types
|
|
|
|
|
|
|
|
- **Binds:** all
|
|
- **Binds:** all
|
|
|
- **Prevents:** type mismatches across package boundaries, duplicated interface definitions
|
|
- **Prevents:** type mismatches across package boundaries, duplicated interface definitions
|
|
|
-- **Rule:** Single repository with packages: `client/`, `server/`, `solver/`, `shared/`, `data/`. `shared/` exports TypeScript interfaces for API request/response shapes, word bank structure, and game types. Every package imports types from `shared/`; no package defines its own copy of a shared type. `[ADOPTED]`
|
|
|
|
|
|
|
+- **Rule:** Single repository with npm workspaces: `client/`, `server/`, `shared/`, `solver/`, plus static `data/`. `shared/` exports TypeScript interfaces for API request/response shapes, word-bank structure, and game types. **The solver algorithms also live in `shared/src/solvers/`** so both the server (runtime replays) and the `solver/` CLI (validation) reuse them; `solver/` is an offline validation script, not the home of the algorithms. Every package imports types from `shared/`; no package defines its own copy of a shared type. `[ADOPTED]`
|
|
|
|
|
|
|
|
### AD-10 — Typed API contracts before implementation
|
|
### AD-10 — Typed API contracts before implementation
|
|
|
|
|
|
|
@@ -100,19 +127,38 @@ Browser (React SPA) ─── REST ─── Server (Express, stateless)
|
|
|
|
|
|
|
|
- **Binds:** client/src/hooks/usePersistence.ts
|
|
- **Binds:** client/src/hooks/usePersistence.ts
|
|
|
- **Prevents:** localStorage key collisions, schema drift between hooks, unversioned state corruption
|
|
- **Prevents:** localStorage key collisions, schema drift between hooks, unversioned state corruption
|
|
|
-- **Rule:** All browser-persisted state lives under a single `localStorage` key (`wordle-state`). The value is a JSON-serialized `PersistedState` interface defined in `shared/src/game.ts`. One hook (`usePersistence`) owns all read/write access; `useStats` and `useGame` consume it, never touch storage directly. `[ADOPTED]`
|
|
|
|
|
|
|
+- **Rule:** All browser-persisted state lives under a single `localStorage` key — **`wordle-state-en`** or **`wordle-state-ru`** (language-suffixed since the RU language was added). The value is a JSON-serialized `PersistedState` interface defined in `shared/src/game.ts`. One hook (`usePersistence`) owns all read/write access; `useGame` consumes it and never touches storage directly. `[ADOPTED]`
|
|
|
|
|
+
|
|
|
|
|
+### AD-13 — Bilingual EN/RU (post-v1)
|
|
|
|
|
+
|
|
|
|
|
+- **Binds:** server/src/index.ts, client/src/messages/*, data/word-bank*.json
|
|
|
|
|
+- **Prevents:** duplicated client builds per language, diverging EN/RU logic
|
|
|
|
|
+- **Rule:** One server + one SPA serve both languages. Language is derived from the URL path (`/ru…` → Russian, otherwise English) via `detectLang()`. The server mounts every router twice (`/api` for EN, `/ru/api` for RU) against a per-language word bank and per-language solver cache. All UI strings live in `client/src/messages/{en,ru}.ts` behind a structural `Messages` interface; keyboard layouts are per-language (RU uses a Cyrillic layout). `[ADOPTED]`
|
|
|
|
|
+
|
|
|
|
|
+### AD-14 — Runtime difficulty with lazy solver cache + forked worker (post-v1)
|
|
|
|
|
+
|
|
|
|
|
+- **Binds:** server/src/index.ts, server/src/routes/guess.ts, server/src/solver-pool.ts, server/src/solver-worker.ts, shared/src/solvers/index.ts
|
|
|
|
|
+- **Prevents:** slow startup (pre-computing difficulty for all words), blocking the event loop on the ~14 s entropy run
|
|
|
|
|
+- **Rule:** The cache (`Map<wordId, CachedResult>`) starts empty. On first request for a word, the server computes replays/difficulty with **3 fast solvers (~20 ms)** synchronously for an immediate response, then launches the **full 4-solver run (including entropy, ~14 s) in a forked child process** and upgrades the cache entry when it completes. `GET /daily` and `POST /play-again` pre-warm the cache the same way. The child process is spawned with `child_process.fork(..., { execArgv: ['--import', 'tsx'] })` — fork rather than `worker_threads` so tsx can load the TypeScript solver modules. Difficulty = average of per-solver attempt counts, a failed solver (>6 attempts) counting as 10, rounded to 2 decimals. `[ADOPTED]`
|
|
|
|
|
+
|
|
|
|
|
+### AD-15 — Player-facing solver tool (post-v1)
|
|
|
|
|
+
|
|
|
|
|
+- **Binds:** server/src/routes/solver.ts, client/src/screens/ScreenSolver.tsx, client/src/components/SolverBoard.tsx
|
|
|
|
|
+- **Prevents:** shipping the full dictionary to the client, duplicating filtering logic client-side
|
|
|
|
|
+- **Rule:** A solver tool at `/solver` (EN) and `/ru/solver` (RU) lets the player filter the dictionary by entering guess→feedback rows. All filtering runs **server-side** (`POST /solver`); the client sends `{ rows: [{ guess, result }] }` and renders the returned candidates (alphabetical, capped at 200), a suggested next guess, and the top-15 letter frequencies. An empty board returns a best-starting-word hint. `[ADOPTED]`
|
|
|
|
|
|
|
|
## Consistency Conventions
|
|
## Consistency Conventions
|
|
|
|
|
|
|
|
| Concern | Convention |
|
|
| Concern | Convention |
|
|
|
| --- | --- |
|
|
| --- | --- |
|
|
|
| Naming (files, functions, interfaces) | camelCase functions/variables, PascalCase types/interfaces/components, kebab-case files |
|
|
| Naming (files, functions, interfaces) | camelCase functions/variables, PascalCase types/interfaces/components, kebab-case files |
|
|
|
|
|
+| Module imports | ES modules with explicit `.js` extensions on relative imports (`./foo.js`), even in TypeScript; `@wordle/shared` for cross-package imports |
|
|
|
| API contracts | Request/response shapes defined as TypeScript interfaces in `shared/src/api.ts` |
|
|
| API contracts | Request/response shapes defined as TypeScript interfaces in `shared/src/api.ts` |
|
|
|
-| Error shapes | API errors return `{ error: string }` with appropriate HTTP status (400 invalid guess, 404 unknown wordId) |
|
|
|
|
|
|
|
+| Error shapes | API errors return `{ error: string }` with appropriate HTTP status (400 invalid guess/request, 404 unknown wordId) |
|
|
|
| Responsive design | CSS media queries, mobile-first. Target: usable at 375px width (small phone) through desktop |
|
|
| Responsive design | CSS media queries, mobile-first. Target: usable at 375px width (small phone) through desktop |
|
|
|
| State mutation | Client state is immutable-style — each guess produces a new state object; React renders from state |
|
|
| State mutation | Client state is immutable-style — each guess produces a new state object; React renders from state |
|
|
|
-| Network errors | Client displays error message on request failure; player can retry. No offline mode for v1 |
|
|
|
|
|
-| Logging | Server logs requests to stdout (method, path, status, latency). No structured logging framework for v1 |
|
|
|
|
|
|
|
+| Network errors | Client displays an error message on request failure and, for daily-word init, falls back to persisted state (offline-tolerant init; no full offline mode) |
|
|
|
|
|
+| Logging | Minimal — no structured logging framework; the solver worker logs failures to stderr |
|
|
|
|
|
|
|
|
## Stack
|
|
## Stack
|
|
|
|
|
|
|
@@ -121,8 +167,9 @@ Browser (React SPA) ─── REST ─── Server (Express, stateless)
|
|
|
| TypeScript | ^6.x |
|
|
| TypeScript | ^6.x |
|
|
|
| Node.js | ^24 LTS |
|
|
| Node.js | ^24 LTS |
|
|
|
| React | ^19.x |
|
|
| React | ^19.x |
|
|
|
-| Vite | ^8.x |
|
|
|
|
|
|
|
+| Vite | ^7.x |
|
|
|
| Express | ^5.x |
|
|
| Express | ^5.x |
|
|
|
|
|
+| tsx | ^4.x (server + solver runtime TS loader) |
|
|
|
| `shared/` types package | workspace |
|
|
| `shared/` types package | workspace |
|
|
|
|
|
|
|
|
## Structural Seed
|
|
## Structural Seed
|
|
@@ -131,11 +178,13 @@ Browser (React SPA) ─── REST ─── Server (Express, stateless)
|
|
|
|
|
|
|
|
```mermaid
|
|
```mermaid
|
|
|
graph LR
|
|
graph LR
|
|
|
- B[Browser<br/>React SPA] -->|REST| S[Express Server]
|
|
|
|
|
- S -->|reads at startup| WB[data/word-bank.json]
|
|
|
|
|
- SV[Solver script] -->|writes| WB
|
|
|
|
|
|
|
+ B[Browser<br/>React SPA] -->|REST /api or /ru/api| S[Express Server]
|
|
|
|
|
+ S -->|reads at startup| WB[data/word-bank*.json<br/>EN + RU]
|
|
|
|
|
+ S -->|lazy compute| C[In-memory solver cache]
|
|
|
|
|
+ C -->|full 4-solver run| W[forked child process<br/>solver-worker.ts]
|
|
|
S -->|serves static files| B
|
|
S -->|serves static files| B
|
|
|
- B -->|stores| LS[Browser localStorage]
|
|
|
|
|
|
|
+ B -->|stores| LS[Browser localStorage<br/>wordle-state-{lang}]
|
|
|
|
|
+ SV[Offline solver CLI<br/>solver/] -->|validates| WB
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
### Source tree
|
|
### Source tree
|
|
@@ -144,36 +193,44 @@ graph LR
|
|
|
wordle/
|
|
wordle/
|
|
|
client/ # React + Vite SPA
|
|
client/ # React + Vite SPA
|
|
|
src/
|
|
src/
|
|
|
- screens/ # Screen1 (Rules), Screen2 (Game), Screen3 (Results)
|
|
|
|
|
- components/ # Grid, Keyboard, Stats, shared UI
|
|
|
|
|
- hooks/ # useGame, useStats, usePersistence
|
|
|
|
|
|
|
+ screens/ # Screen1Rules, Screen2Game, Screen3Results, ScreenSolver
|
|
|
|
|
+ components/ # Grid, Keyboard, LangSwitch, SolverBoard
|
|
|
|
|
+ hooks/ # useGame, usePersistence
|
|
|
|
|
+ messages/ # i18n: index (Messages iface, detectLang), en, ru
|
|
|
api.ts # fetch wrappers for /api/*
|
|
api.ts # fetch wrappers for /api/*
|
|
|
|
|
+ App.tsx # screen state machine + solver route + stats/skill
|
|
|
|
|
+ main.tsx
|
|
|
index.html
|
|
index.html
|
|
|
- server/ # Express REST API
|
|
|
|
|
|
|
+ server/ # Express REST API (stateless)
|
|
|
src/
|
|
src/
|
|
|
- index.ts # app entry, static file serving
|
|
|
|
|
|
|
+ index.ts # entry, dual /api + /ru/api mounts, static serving
|
|
|
routes/
|
|
routes/
|
|
|
- daily.ts # GET /api/daily
|
|
|
|
|
- guess.ts # POST /api/guess
|
|
|
|
|
- play-again.ts # POST /api/play-again
|
|
|
|
|
- feedback.ts # green/yellow/gray scoring logic
|
|
|
|
|
|
|
+ daily.ts # GET /daily
|
|
|
|
|
+ guess.ts # POST /guess
|
|
|
|
|
+ play-again.ts # POST /play-again
|
|
|
|
|
+ validate.ts # GET /validate/:word
|
|
|
|
|
+ solver.ts # POST /solver
|
|
|
|
|
+ feedback.ts # green/yellow/gray scoring (array form)
|
|
|
validate.ts # word bank lookup
|
|
validate.ts # word bank lookup
|
|
|
daily-word.ts # date → wordId function
|
|
daily-word.ts # date → wordId function
|
|
|
|
|
+ play-again.ts # skill-weighted word selection
|
|
|
|
|
+ solver-pool.ts # forked child-process runner
|
|
|
|
|
+ solver-worker.ts # child: computeReplays / computeReplaysFull
|
|
|
|
|
+ __tests__/ # (empty — no tests yet)
|
|
|
package.json
|
|
package.json
|
|
|
- solver/ # offline pre-compute
|
|
|
|
|
- src/
|
|
|
|
|
- index.ts # orchestrates solvers, writes rankings
|
|
|
|
|
- entropy.ts # entropy-based solver
|
|
|
|
|
- minimax.ts # minimax solver (or alternative)
|
|
|
|
|
- package.json
|
|
|
|
|
- shared/ # TypeScript types
|
|
|
|
|
|
|
+ shared/ # TypeScript types + solver algorithms (single source of truth)
|
|
|
src/
|
|
src/
|
|
|
api.ts # request/response interfaces
|
|
api.ts # request/response interfaces
|
|
|
- word-bank.ts # WordBank, TargetWord, etc.
|
|
|
|
|
- game.ts # Guess, Feedback, Colors, etc.
|
|
|
|
|
|
|
+ word-bank.ts # WordBank, TargetWord, SolverReplay
|
|
|
|
|
+ game.ts # GuessEntry, GameStats, PersistedState
|
|
|
|
|
+ index.ts # re-exports
|
|
|
|
|
+ solvers/ # feedback, entropy, frequency, vowel-first, greedy, index
|
|
|
package.json
|
|
package.json
|
|
|
- data/ # generated / static data
|
|
|
|
|
- word-bank.json # guessable + targets with difficulty scores
|
|
|
|
|
|
|
+ solver/ # offline CLI: validates all targets solvable in ≤6
|
|
|
|
|
+ src/index.ts
|
|
|
|
|
+ data/ # static word banks
|
|
|
|
|
+ word-bank.json # EN: guessable + targets { word, hint }
|
|
|
|
|
+ word-bank-ru.json # RU
|
|
|
package.json # workspace root
|
|
package.json # workspace root
|
|
|
```
|
|
```
|
|
|
|
|
|
|
@@ -181,36 +238,42 @@ wordle/
|
|
|
|
|
|
|
|
| Capability / Area | Lives in | Governed by |
|
|
| Capability / Area | Lives in | Governed by |
|
|
|
| --- | --- | --- |
|
|
| --- | --- | --- |
|
|
|
-| FR-1 First-visit detection | client/src/hooks/usePersistence.ts | AD-7 |
|
|
|
|
|
-| FR-2 Rules display | client/src/screens/Screen1.tsx | AD-7 |
|
|
|
|
|
|
|
+| FR-1 First-visit detection | client/src/hooks/usePersistence.ts, client/src/App.tsx | AD-7 |
|
|
|
|
|
+| FR-2 Rules display | client/src/screens/Screen1Rules.tsx | AD-7 |
|
|
|
| FR-3 Grid rendering | client/src/components/Grid.tsx | AD-7 |
|
|
| FR-3 Grid rendering | client/src/components/Grid.tsx | AD-7 |
|
|
|
-| FR-4 Keyboard | client/src/components/Keyboard.tsx | AD-7 |
|
|
|
|
|
|
|
+| FR-4 Keyboard | client/src/components/Keyboard.tsx | AD-7, AD-13 |
|
|
|
| FR-5 Guess submission | client/src/hooks/useGame.ts → server/src/routes/guess.ts | AD-5, AD-6 |
|
|
| FR-5 Guess submission | client/src/hooks/useGame.ts → server/src/routes/guess.ts | AD-5, AD-6 |
|
|
|
| FR-6 Invalid word rejection | server/src/validate.ts | AD-5 |
|
|
| FR-6 Invalid word rejection | server/src/validate.ts | AD-5 |
|
|
|
| FR-7 Feedback scoring | server/src/feedback.ts | AD-6 |
|
|
| FR-7 Feedback scoring | server/src/feedback.ts | AD-6 |
|
|
|
| FR-8 Win detection | client/src/hooks/useGame.ts | AD-7 |
|
|
| FR-8 Win detection | client/src/hooks/useGame.ts | AD-7 |
|
|
|
| FR-9 Loss detection | client/src/hooks/useGame.ts | AD-7 |
|
|
| FR-9 Loss detection | client/src/hooks/useGame.ts | AD-7 |
|
|
|
| FR-10 Daily word selection | server/src/daily-word.ts | AD-2 |
|
|
| FR-10 Daily word selection | server/src/daily-word.ts | AD-2 |
|
|
|
-| FR-11 Play Again word selection | server/src/routes/play-again.ts | AD-8 |
|
|
|
|
|
-| FR-12 Player skill tracking | client/src/hooks/useStats.ts | AD-7 |
|
|
|
|
|
-| FR-13 Results display | client/src/screens/Screen3.tsx | AD-7 |
|
|
|
|
|
-| FR-14 Statistics display | client/src/screens/Screen3.tsx | AD-7 |
|
|
|
|
|
-| FR-15 Play Again button | client/src/screens/Screen3.tsx | AD-7 |
|
|
|
|
|
-| FR-16 Multi-algorithm solver | solver/src/ | Paradigm (offline script) |
|
|
|
|
|
-| FR-17 Word ranking output | solver/src/ → data/word-bank.json | AD-4 |
|
|
|
|
|
-| FR-18 Word bank curation | data/word-bank.json (generated) | AD-4 |
|
|
|
|
|
-| FR-19 Browser persistence | client/src/hooks/usePersistence.ts | AD-7 |
|
|
|
|
|
-
|
|
|
|
|
-## Deferred
|
|
|
|
|
-
|
|
|
|
|
-- **Deployment mechanism** — how the server process is started, restarted, proxied (nginx, systemd). Owned by the deployment environment, not the architecture.
|
|
|
|
|
-- **Solver algorithm selection** — which two or more specific algorithms (entropy, minimax, frequency-based). Deferred to implementation; the solver package interface is the invariant.
|
|
|
|
|
-- **Skill metric formula** — exact calculation (simple average, weighted recent). Deferred to implementation; the API already carries a `skillMetric` number.
|
|
|
|
|
-- **Word bank source** — which public-domain word list. Deferred to implementation; the `WordBank` interface is the invariant.
|
|
|
|
|
-- **Hints on attempts 5-6** — post-v1. API already carries `tryNo`; the guess response shape is ready for a `hint` field.
|
|
|
|
|
-- **HTTPS/TLS termination** — owned by the deployment environment (reverse proxy), not the application.
|
|
|
|
|
-- **npm workspace configuration** — tooling detail, not an architectural invariant. The package boundary rules in AD-9 are sufficient.
|
|
|
|
|
-- **Screen navigation/routing** — 3 screens, state-based switching (React state or reducer). No router library needed at this scale.
|
|
|
|
|
-- **Feedback latency** — v1 accepts network round-trip per guess. Optimistic UI (show letters immediately, confirm colors) is a future enhancement.
|
|
|
|
|
-- **Accessibility (color-blind mode, ARIA, screen reader)** — deferred to post-v1. Core game uses green/yellow color distinction; a color-blind accessible palette is a future enhancement.
|
|
|
|
|
-- **Win celebration animation** — UI detail owned by the Results Screen component. No architectural constraint.
|
|
|
|
|
|
|
+| FR-11 Play Again word selection | server/src/play-again.ts, server/src/routes/play-again.ts | AD-8 |
|
|
|
|
|
+| FR-12 Player skill tracking | client/src/App.tsx (running average) + usePersistence | AD-7 |
|
|
|
|
|
+| FR-13 Results display | client/src/screens/Screen3Results.tsx | AD-7 |
|
|
|
|
|
+| FR-14 Statistics display | client/src/screens/Screen3Results.tsx | AD-7 |
|
|
|
|
|
+| FR-15 Play Again button | client/src/screens/Screen3Results.tsx | AD-7 |
|
|
|
|
|
+| FR-16 Multi-algorithm solver | shared/src/solvers/ | AD-3, AD-9 |
|
|
|
|
|
+| FR-17 Word ranking output | shared/src/solvers/index.ts (runtime difficulty) | AD-4, AD-14 |
|
|
|
|
|
+| FR-18 Word bank curation | data/word-bank*.json (generated by raw/ scripts) | AD-4 |
|
|
|
|
|
+| FR-19 Browser persistence | client/src/hooks/usePersistence.ts | AD-7, AD-12 |
|
|
|
|
|
+
|
|
|
|
|
+## Deferred / Resolved
|
|
|
|
|
+
|
|
|
|
|
+**Resolved since v1 (moved from "deferred" into ADs or implemented):**
|
|
|
|
|
+
|
|
|
|
|
+- **Solver algorithm selection** — four algorithms chosen: entropy, frequency, vowel-first, greedy (no minimax). See AD-9/AD-14.
|
|
|
|
|
+- **Skill metric formula** — running average of per-game difficulty, client-side (AD-7).
|
|
|
|
|
+- **Word bank source** — frequency-sorted word lists generated by scripts in `raw/` (see raw/03, raw/07).
|
|
|
|
|
+- **Hints on attempt 5** — implemented as synonym hints (raw/06, AD-4).
|
|
|
|
|
+- **npm workspace configuration** — npm workspaces in place (AD-9).
|
|
|
|
|
+- **Screen navigation/routing** — state-based switching plus a pathname check in `App.tsx` for `/solver` routes; no router library.
|
|
|
|
|
+
|
|
|
|
|
+**Still deferred:**
|
|
|
|
|
+
|
|
|
|
|
+- **Deployment mechanism** — how the server process is started, restarted, proxied (nginx, systemd). Owned by the deployment environment, not the architecture. (A `rsync` deploy script exists in the root `package.json`.)
|
|
|
|
|
+- **HTTPS/TLS termination** — owned by the deployment environment (reverse proxy).
|
|
|
|
|
+- **Optimistic UI** — v1 accepts a network round-trip per guess; showing letters immediately is a future enhancement.
|
|
|
|
|
+- **Accessibility (color-blind mode, ARIA, screen reader)** — post-v1. Core game uses green/yellow color distinction.
|
|
|
|
|
+- **Win celebration animation** — UI detail owned by the Results Screen component.
|
|
|
|
|
+- **Automated tests** — `server/src/__tests__/` exists but is empty; no test suite has been written yet.
|