AT Mot — a bilingual (EN/FR) daily word game native to the AT Protocol.
0

Configure Feed

Select the types of activity you want to include in your feed.

at trunk 143 lines 7.6 kB View raw View rendered
1# AT Mot 2 3**A bilingual (English / French) daily word game, native to the [AT Protocol](https://atproto.com).** 4Guess the hidden five-letter word in six tries. Your results live in **your own PDS**, and the 5leaderboard is read from the public **[Constellation](https://constellation.microcosm.blue)** 6backlink index. 7 8🌐 **[atmot.herve.bzh](https://atmot.herve.bzh)** 9 10> _“mot” is French for “word” — and a play on at**proto**._ 11 12--- 13 14AT Mot uses a **”data on your PDS, reads via Constellation, no AppView”** architecture for atproto 15apps: 16 17- **Your data is yours.** Each finished puzzle is written as a `bzh.herve.atmot.result` record to 18 your repo via browser OAuth. There is no server storing your games — there is no server. 19- **The leaderboard has no database.** Every result record links to a canonical per-day permalink. 20 The daily stats and shared grids are built by querying Constellation for backlinks to that 21 permalink and aggregating **in the browser**. 22- **Deterministic & global.** Each language is its own daily series; the day's word is computed on 23 every client from a bundled list, indexed by whole **UTC** days since launch. Everyone worldwide 24 gets the same word on the same calendar day, with no server in the loop. 25- **Standards-conformant lexicons** under the `bzh.herve.atmot.*` authority, following the 26 [Lexicon Style Guide](https://atproto.com/guides/lexicon-style-guide). 27- **Installable to your home screen.** A web manifest makes it installable as a standalone app. 28 29## Architecture at a glance 30 31``` 32 ┌─────────────── browser (static SPA on Cloudflare Pages) ───────────────┐ 33 │ │ 34 play │ deterministic daily word (bundled list, UTC day index) │ 35 │ │ │ 36 write │ OAuth session ── createRecord ──▶ YOUR PDS │ 37 │ result (write-once) ├─ bzh.herve.atmot.result │ 38 │ stats (self, mutable) └─ bzh.herve.atmot.stats │ 39 │ │ 40 share │ createRecord ──▶ app.bsky.feed.post (emoji grid + puzzle permalink) │ 41 │ │ 42 read │ Constellation backlinks to the permalink ──▶ leaderboard + shared │ 43 │ (counts + record lists) grids + live likes/reposts/replies │ 44 └─────────────────────────────────────────────────────────────────────────┘ 45``` 46 47The only permitted server-side component is an **optional, stateless** Cloudflare Worker cache 48(documented fallback, not used by default). Core gameplay and reads work entirely client-side. 49 50## Lexicons 51 52Published as `com.atproto.lexicon.schema` records under **`bzh.herve.atmot.*`**: 53 54| NSID | Key | Purpose | 55| --- | --- | --- | 56| `bzh.herve.atmot.result` | `any` (`<lang>-<n>`) | One **immutable, write-once** record per completed puzzle. Stores tile **colours only** — never the answer or guessed letters. Carries the `puzzleTarget` permalink that Constellation indexes. | 57| `bzh.herve.atmot.stats` | `self` | A single **mutable** streak/stats record. Also the app's **declaration record**: its presence means "this account plays AT Mot"; its deletion means opt-out. | 58| `bzh.herve.atmot.defs` | — | Shared client-side view objects (`resultView`, `leaderboardEntry`, `dailyStats`). | 59 60The `puzzleTarget` is a canonical web URL — `https://atmot.herve.bzh/p/<lang>/<puzzleNumber>` 61**emitted from a single shared helper** so the write path and the Constellation read path can never 62diverge. The format is frozen (Constellation compares it literally). 63 64Validate the schemas against the Style Guide rules: 65 66```sh 67npm run lexicons:validate 68``` 69 70Publish them (run by the account that controls the namespace authority): 71 72```sh 73ATMOT_PUBLISH_IDENTIFIER=you.example ATMOT_PUBLISH_PASSWORD=<app-password> npm run lexicons:publish 74``` 75 76> For network-wide lexicon resolution, the authority domain also needs a `_lexicon` DNS TXT record 77> (`did=<publishing did>`) pointing at the publishing account's DID. 78 79## Word lists 80 81Self-curated per language, from permissively-licensed sources, normalized to **uppercase, 82unaccented A–Z** at build time (French diacritics are stripped and ligatures expanded — `Œ→OE`, 83`Æ→AE` — matching the accent-free convention of the official French Scrabble dictionary): 84 85| Language | Dictionary | Commonness ranking | 86| --- | --- | --- | 87| English | [ENABLE](https://github.com/dolph/dictionary) — **Public Domain** | hermitdave/FrequencyWords (MIT) | 88| French | [an-array-of-french-words](https://github.com/words/an-array-of-french-words) — **MIT** | hermitdave/FrequencyWords (MIT) | 89 90Lists are generated by `npm run build:words` and committed (the generated JSON _is_ the game 91content); the build is reproducible (same sources + fixed seeds → identical output). 92 93## Develop 94 95```sh 96npm install 97npm run dev # http://127.0.0.1:12520 (atproto OAuth forbids localhost; use the loopback IP) 98npm test # vitest — deterministic engine, scoring, stats, share, lexicon format 99npm run typecheck # tsc (app + node scripts) 100npm run lint 101npm run build # typecheck + production build into dist/ 102``` 103 104OAuth in dev uses atproto's loopback client (no hosted metadata needed). In production the SPA serves 105`/client-metadata.json`, and `client_id` equals that exact URL. 106 107AT Mot requests **granular OAuth scopes** (least privilege) rather than the broad `transition:generic` 108— write access is limited to exactly the three collections it creates: `bzh.herve.atmot.result`, 109`bzh.herve.atmot.stats`, and `app.bsky.feed.post`. See DECISIONS.md (#17b). 110 111## Deploy (Cloudflare Pages) 112 113The git remote lives on [tangled.org](https://tangled.org), so Cloudflare's git-connected builds 114aren't available. Deployment uses **Wrangler direct upload** instead — build locally, then push the 115`dist/` folder straight to the `atmot` Pages project: 116 117```bash 118npm run deploy 119``` 120 121This runs `npm run build` (typecheck + Vite build) and then 122`wrangler pages deploy dist --project-name=atmot --branch=trunk`. The first run prompts for 123`wrangler login`. 124 125The `--branch=trunk` flag is important: Wrangler tags a deployment with your current local git 126branch, and Cloudflare only promotes deployments whose branch matches the project's **production 127branch** (`trunk`). Without it, deploying from a feature branch produces a preview URL and leaves the 128live site unchanged. 129 130- **Output directory:** `dist` 131- **Pages project:** `atmot` 132- **Production branch:** `trunk` 133- **Custom domain:** `atmot.herve.bzh` 134 135`public/_redirects` provides the SPA history fallback so the `/p/<lang>/<n>` permalinks resolve to 136real pages. No origin server or environment variables are required for core gameplay. 137 138## License 139 140Dual-licensed under **MIT** OR **Apache-2.0** (mirroring atproto), at your option. See 141[LICENSE-MIT](./LICENSE-MIT) and [LICENSE-APACHE](./LICENSE-APACHE). Third-party data and font 142attributions are in [NOTICE.md](./NOTICE.md). Design rationale and judgment calls are recorded in 143[DECISIONS.md](./DECISIONS.md).