Word Waltz
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.
How it works
- Challenge — Player A picks a valid 5-letter word to challenge Player B.
- Solve — Player B gets the classic Wordle experience: - Correct letter, correct position → green - Correct letter, wrong position → yellow - Not in the word → gray - An on-screen keyboard tracks letter states as you play.
- Score — Fewer guesses = higher score (6 tries possible; solving in 1 is perfect).
- Volley — Player B then picks a word to challenge Player A right back.
- Repeat — Games go on indefinitely; a running scoreboard tracks totals.
Tech stack
| Concern | Tool |
|---|---|
| Build | Vite (static output) |
| Hosting | GitLab Pages |
| Game storage | Firebase Firestore |
| Encryption | Web Crypto API (AES-GCM, 256-bit) |
| QR codes | qrcode |
| Language | TypeScript (strict mode, ES modules) |
Security model — URL-keyed encryption
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.
https://your-app.gitlab.io/game/#gameId=xxxx&key=base64encodedkey
- A fresh AES-GCM-256 key is generated in the browser at game creation time.
- The key is encoded into the URL fragment and never sent to Firebase or any server.
- Firebase stores only ciphertext. Even a Firebase admin cannot read game state without the URL.
- Sharing the link = sharing access. Losing the link = losing access.
Flow — joining a game
- Player A creates a game → browser generates a game ID + encryption key.
- Player A receives a shareable URL and a QR code.
- Player A shares either with Player B (text, QR scan, etc.).
- Both players load the same URL; both can read/write the game via the shared key.
Scoring
| Guesses taken | Points |
|---|---|
| 1 | 6 |
| 2 | 5 |
| 3 | 4 |
| 4 | 3 |
| 5 | 2 |
| 6 | 1 |
| Failed (> 6) | 0 |
Scores accumulate across rounds. There is no set game length — keep playing as long as you like.
Project structure
word-waltz/
├── .fw-ci.yml # GitLab Pages deploy pipeline
├── package.json
├── tsconfig.json
├── vite.config.ts
├── AGENTS.md # AI coding-agent instructions
└── src/
├── index.html # App shell
├── main.ts # Entry point — router/bootstrap
├── types.ts # Shared TypeScript types
├── styles/
│ └── main.css
├── components/
│ ├── WordPicker.ts # UI: pick a challenge word
│ ├── WordleBoard.ts # UI: 6×5 guess grid
│ ├── Keyboard.ts # UI: on-screen keyboard with letter states
│ ├── Scoreboard.ts # UI: running score display
│ └── ShareDialog.ts # UI: shareable link + QR code
├── firebase/
│ ├── config.ts # Firebase project configuration
│ └── gameService.ts # Firestore read/write for game state
├── crypto/
│ └── encryption.ts # AES-GCM encrypt/decrypt via Web Crypto API
└── utils/
├── wordList.ts # Valid 5-letter word list + validation
├── scoring.ts # Score calculation logic
└── shareLink.ts # URL construction / parsing (fragment-based)
Local development
npm install
npm run dev # Vite dev server at http://localhost:5173
You will need a Firebase project. Copy your config into src/firebase/config.js (see the placeholder there).
Deployment (GitLab Pages)
Every push to main triggers .gitlab-ci.yml, which runs npm run build and publishes dist/ to GitLab Pages automatically.
npm run build # Outputs to dist/
Firebase setup
- Create a Firebase project at console.firebase.google.com.
- Enable Firestore in Native mode.
- Set Firestore security rules (see guidance in
AGENTS.md). - The Firebase project config is committed directly in
src/firebase/config.ts.