word-waltz / files / README.md
README.md
f755bca6 2026-03-23 Pavel Safronov | 1 | # Word Waltz |
| 2 | ||
| 3 | A two-player, asynchronous Wordle duel. You challenge your opponent with a secret 5-letter word; they play Wordle against it and score based on how many guesses it takes. Then the tables turn — back and forth, indefinitely, keeping a running tally. | |
| 4 | ||
5a9bfdda 2026-10-27 Pavel Safronov | 5 | [Play now!](https://apps.fuzzyworld.net/word-waltz/) |
a1d37b6e 2026-06-07 Pavel Safronov | 6 | |
f755bca6 2026-03-23 Pavel Safronov | 7 | ## How it works |
| 8 | ||
| 9 | 1. **Challenge** — Player A picks a valid 5-letter word to challenge Player B. | |
| 10 | 2. **Solve** — Player B gets the classic Wordle experience: | |
| 11 | - Correct letter, correct position → **green** | |
| 12 | - Correct letter, wrong position → **yellow** | |
| 13 | - Not in the word → **gray** | |
| 14 | - An on-screen keyboard tracks letter states as you play. | |
| 15 | 3. **Score** — Fewer guesses = higher score (6 tries possible; solving in 1 is perfect). | |
| 16 | 4. **Volley** — Player B then picks a word to challenge Player A right back. | |
| 17 | 5. **Repeat** — Games go on indefinitely; a running scoreboard tracks totals. | |
| 18 | ||
| 19 | ## Tech stack | |
| 20 | ||
a0df624b 2026-03-23 Pavel Safronov | 21 | | Concern | Tool | |
| 22 | | ------------ | ---------------------------------------------------------------- | | |
| 23 | | Build | [Vite](https://vitejs.dev/) (static output) | | |
| 24 | | Hosting | [GitLab Pages](https://docs.gitlab.com/ee/user/project/pages/) | | |
f755bca6 2026-03-23 Pavel Safronov | 25 | | Game storage | [Firebase Firestore](https://firebase.google.com/docs/firestore) | |
a0df624b 2026-03-23 Pavel Safronov | 26 | | Encryption | Web Crypto API (AES-GCM, 256-bit) | |
| 27 | | QR codes | [qrcode](https://www.npmjs.com/package/qrcode) | | |
| 28 | | Language | TypeScript (strict mode, ES modules) | | |
f755bca6 2026-03-23 Pavel Safronov | 29 | |
| 30 | ## Security model — URL-keyed encryption | |
| 31 | ||
| 32 | All game state stored in Firebase is encrypted at rest. The decryption key **lives only in the URL fragment** (the `#` portion), which browsers never transmit over the network. | |
| 33 | ||
| 34 | ```text | |
| 35 | https://your-app.gitlab.io/game/#gameId=xxxx&key=base64encodedkey | |
| 36 | ``` | |
| 37 | ||
| 38 | - A fresh AES-GCM-256 key is generated in the browser at game creation time. | |
| 39 | - The key is encoded into the URL fragment and never sent to Firebase or any server. | |
| 40 | - Firebase stores only ciphertext. Even a Firebase admin cannot read game state without the URL. | |
| 41 | - Sharing the link = sharing access. Losing the link = losing access. | |
| 42 | ||
| 43 | ## Flow — joining a game | |
| 44 | ||
| 45 | 1. Player A creates a game → browser generates a game ID + encryption key. | |
| 46 | 2. Player A receives a shareable URL and a QR code. | |
| 47 | 3. Player A shares either with Player B (text, QR scan, etc.). | |
| 48 | 4. Both players load the same URL; both can read/write the game via the shared key. | |
| 49 | ||
| 50 | ## Scoring | |
| 51 | ||
| 52 | | Guesses taken | Points | | |
a0df624b 2026-03-23 Pavel Safronov | 53 | | ------------- | ------ | |
| 54 | | 1 | 6 | | |
| 55 | | 2 | 5 | | |
| 56 | | 3 | 4 | | |
| 57 | | 4 | 3 | | |
| 58 | | 5 | 2 | | |
| 59 | | 6 | 1 | | |
| 60 | | Failed (> 6) | 0 | | |
f755bca6 2026-03-23 Pavel Safronov | 61 | |
| 62 | Scores accumulate across rounds. There is no set game length — keep playing as long as you like. | |
| 63 | ||
| 64 | ## Project structure | |
| 65 | ||
| 66 | ```text | |
| 67 | word-waltz/ | |
91f31835 2026-10-27 Pavel Safronov | 68 | ├── .fw-ci.yml # GitLab Pages deploy pipeline |
f755bca6 2026-03-23 Pavel Safronov | 69 | ├── package.json |
| 70 | ├── tsconfig.json | |
| 71 | ├── vite.config.ts | |
| 72 | ├── AGENTS.md # AI coding-agent instructions | |
| 73 | └── src/ | |
| 74 | ├── index.html # App shell | |
| 75 | ├── main.ts # Entry point — router/bootstrap | |
| 76 | ├── types.ts # Shared TypeScript types | |
| 77 | ├── styles/ | |
| 78 | │ └── main.css | |
| 79 | ├── components/ | |
| 80 | │ ├── WordPicker.ts # UI: pick a challenge word | |
| 81 | │ ├── WordleBoard.ts # UI: 6×5 guess grid | |
| 82 | │ ├── Keyboard.ts # UI: on-screen keyboard with letter states | |
| 83 | │ ├── Scoreboard.ts # UI: running score display | |
| 84 | │ └── ShareDialog.ts # UI: shareable link + QR code | |
| 85 | ├── firebase/ | |
| 86 | │ ├── config.ts # Firebase project configuration | |
| 87 | │ └── gameService.ts # Firestore read/write for game state | |
| 88 | ├── crypto/ | |
| 89 | │ └── encryption.ts # AES-GCM encrypt/decrypt via Web Crypto API | |
| 90 | └── utils/ | |
| 91 | ├── wordList.ts # Valid 5-letter word list + validation | |
| 92 | ├── scoring.ts # Score calculation logic | |
| 93 | └── shareLink.ts # URL construction / parsing (fragment-based) | |
| 94 | ``` | |
| 95 | ||
| 96 | ## Local development | |
| 97 | ||
| 98 | ```bash | |
| 99 | npm install | |
| 100 | npm run dev # Vite dev server at http://localhost:5173 | |
| 101 | ``` | |
| 102 | ||
| 103 | You will need a Firebase project. Copy your config into `src/firebase/config.js` (see the placeholder there). | |
| 104 | ||
| 105 | ## Deployment (GitLab Pages) | |
| 106 | ||
| 107 | Every push to `main` triggers `.gitlab-ci.yml`, which runs `npm run build` and publishes `dist/` to GitLab Pages automatically. | |
| 108 | ||
| 109 | ```bash | |
| 110 | npm run build # Outputs to dist/ | |
| 111 | ``` | |
| 112 | ||
| 113 | ## Firebase setup | |
| 114 | ||
| 115 | 1. Create a Firebase project at [console.firebase.google.com](https://console.firebase.google.com). | |
| 116 | 2. Enable **Firestore** in Native mode. | |
82674e8a 2026-03-23 Pavel Safronov | 117 | 3. Set Firestore security rules (see guidance in `AGENTS.md`). |
| 118 | 4. The Firebase project config is committed directly in `src/firebase/config.ts`. |