fuzzyworld

word-waltz / files / AGENTS.md

AGENTS.md

Blame: the commit that last changed each line · file · history

f755bca6 2026-03-23 Pavel Safronov1# Word Waltz — Agent Instructions
2
3This file contains instructions and context for AI coding agents (GitHub Copilot, etc.) working in this repository.
4
5## Project overview
6
7Word Waltz is a two-player asynchronous Wordle duel web app. Players alternate challenging each other with secret 5-letter words and scoring based on how many guesses it takes to solve. Games run indefinitely with a running scoreboard.
8
9## Tech stack (non-negotiable)
10
11- **Build**: Vite — all source under `src/`, output to `dist/`
12- **Hosting**: GitLab Pages (CI pipeline in `.gitlab-ci.yml`)
13- **Storage**: Firebase Firestore
14- **Encryption**: Web Crypto API, AES-GCM 256-bit, key embedded in URL fragment
15- **Language**: TypeScript (strict mode, ES modules, no framework)
16- **QR codes**: `qrcode` npm package
17- **Styling**: Plain CSS (no CSS framework unless the user explicitly approves one)
18
19## Repository structure
20
21```text
22src/
23 index.html # Single-page app shell
24 main.ts # Entry point: parses URL, bootstraps the right view
25 types.ts # Shared TypeScript types (GameState, Player, Round, etc.)
26 styles/main.css # Global styles
27 components/
28 WordPicker.ts # Word selection UI (challenge creation)
29 WordleBoard.ts # 6×5 guess grid rendering and interaction
30 Keyboard.ts # On-screen keyboard; colors keys as letters are used
31 Scoreboard.ts # Displays cumulative scores for both players
32 ShareDialog.ts # Shows shareable URL and renders a QR code
33 firebase/
34 config.ts # Firebase SDK init — never commit real credentials
35 gameService.ts # All Firestore operations (read/write game documents)
36 crypto/
37 encryption.ts # encrypt(plaintext, key) / decrypt(ciphertext, key)
38 utils/
39 wordList.ts # Word list and isValidWord(word) helper
40 scoring.ts # calculateScore(guessCount, solved) → number
41 shareLink.ts # buildLink(gameId, key) / parseLink(url) → {gameId, key}
42```
43
44## Game rules (implement faithfully)
45
46- Words must be exactly 5 letters, from the valid word list.
47- The guess grid is 6 rows × 5 columns.
48- Tile states after a guess:
49 - **Green** (`correct`): right letter, right position.
50 - **Yellow** (`present`): right letter, wrong position.
51 - **Gray** (`absent`): letter not in the word at all.
52- The on-screen keyboard mirrors the best-known state for each letter.
53- A round ends when the solver either guesses correctly or exhausts all 6 attempts.
54- Score = `max(0, 7 - guessCount)` (perfect = 6, failed = 0).
55- After a round ends, the solver becomes the challenger and picks a new word.
56- Both players' cumulative scores are always visible.
57
58## Firestore document shape
59
60Game data is stored in two layers. The Firestore document contains only the
61encryption envelope plus one intentionally-plaintext field used for cleanup:
62
63```js
64// What is stored in Firestore (top-level fields)
65{
66 iv: string, // base64 — AES-GCM initialisation vector
67 ciphertext: string, // base64 — encrypted JSON blob
68 lastPlayed: number, // Unix ms — UNENCRYPTED; the only plaintext field; used for automated cleanup
69}
70```
71
72The decrypted `ciphertext` contains the full game state as JSON:
73
74```js
75// Decrypted game state (JSON inside ciphertext)
76{
77 players: {
78 a: { name: string, score: number },
79 b: { name: string, score: number },
80 },
81 rounds: [
82 {
83 challenger: 'a' | 'b',
84 word: string, // the secret word
85 guesses: string[], // ordered list of guesses made
86 solved: boolean,
87 score: number,
88 },
89 // ...
90 ],
91 currentRound: number, // index into rounds[]
92 turn: 'pick' | 'solve', // what the non-challenger should do next
93 createdAt: number, // Unix ms
94 updatedAt: number,
95}
96```
97
98`lastPlayed` is updated on every write alongside the ciphertext. It reveals
99only that the game was active at that time — no game content is exposed.
100The encryption key is only in the URL fragment.
101
102## Encryption model
103
104- Key generation: `crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'])`
105- Export key as `raw`, encode as URL-safe base64 for the URL fragment.
106- IV: random 12-byte value prepended to every ciphertext blob.
107- Firestore document fields: `{ iv: base64, ciphertext: base64, lastPlayed: unixMs }`
108- Functions live in `src/crypto/encryption.js` and must be the sole place encryption/decryption happens.
109
110## URL format
111
112```text
113https://host/game/#gameId=<id>&key=<base64url-key>
114```
115
116- Fragment is never sent over the network by the browser.
117- `shareLink.js` owns all construction and parsing of this format.
118- Never put the key in the path or query string.
119
120## Coding conventions
121
122- ES modules throughout (`import`/`export`); no CommonJS.
123- No build-time secrets. Firebase config is committed as a placeholder; real values are injected via environment variables in CI using Vite's `import.meta.env` pattern, prefixed with `VITE_`.
124- Components are plain functions that accept a DOM container and return a cleanup function, e.g.:
125
126 ```ts
127 export function mountWordleBoard(container: HTMLElement, props: WordleBoardProps): () => void {
128 // ... render into container
129 return function cleanup() { /* teardown */ };
130 }
131 ```
132
133- No global mutable state outside of `main.ts`. Pass state down as arguments.
134- All async Firebase and Crypto operations must handle errors and surface them to the user.
135- CSS class names follow BEM: `block__element--modifier`.
136
137## Things to avoid
138
139- Do not introduce a JS framework (React, Vue, Svelte, etc.) without explicit user approval.
140- Do not add npm packages without explaining why and getting approval.
141- Do not store the encryption key in `localStorage`, `sessionStorage`, Firestore, or any server-visible location.
142- Do not commit real Firebase credentials. Use `VITE_*` environment variables.
143- Do not skip input validation — all words submitted by users must be checked against the word list.
144
145## Firestore security rules (guidance)
146
147Since confidentiality is enforced by client-side encryption, Firestore rules can be relatively open. A reasonable baseline:
148
149```text
150rules_version = '2';
151service cloud.firestore {
152 match /databases/{database}/documents {
153 match /games/{gameId} {
154 allow read, write: if true;
155 }
156 }
157}
158```
159
160For stricter rules (optional): require the `gameId` to match a specific format (UUID), or require at least one read before a write.
161
162## GitLab Pages deployment
163
164- CI file: `.gitlab-ci.yml`
165- Build command: `npm run build`
166- Pages artifact: `dist/`
167- Environment variables for Firebase config are set as GitLab CI/CD variables (masked), injected into Vite via `VITE_*` prefix.