Make tile results legible without relying on colour alone
Early player feedback on Bluesky flagged two problems with the board:
there was no explainer, so a first-time player couldn't tell whether a
coloured tile meant "right spot" or "right letter"; and the present
colour read as red, making the palette feel like the red/green pairing
that colour-blind players struggle with.
Address both, plus two consistency issues the rework surfaced:
- Onboarding: a persistent colour/symbol legend under the board, and
a "?" header button opening a "How to play" modal (native <dialog>,
bilingual, original copy) shown once on first visit. Wires up the
previously-unused howTo strings.
- Colour-blind mode: an opt-in footer toggle adds a per-tile symbol
(right spot / wrong spot / not in word) to the board, legend, and
modal. Off by default so the glyphs don't clutter the board for
players who don't need them; persisted in localStorage via a
cb-safe class on <html>. Per-tile aria labels already covered
screen readers and are unchanged.
- Present colour nudged from burnt amber #B45309 (reads red) to gold
#A16207, clearly distinct from teal and slate.
- Keyboard inversion: untried keys are a neutral grey, a ruled-out key
drops to a darker recessive tone. It was the opposite before -- in
dark mode an absent key turned lighter than an untried one.
- "Not in word" is one shared, theme-aware tone across the board tile,
keyboard key, and legend (light: slate #5B6776; dark: #232B35),
rather than slate on the board but dark on the keyboard.
Decisions recorded in DECISIONS.md (13, 13a, 13b, 14a).
Trim defensive positioning from docs and drop footer source link
Remove the bold "not something else" framing and sales-style copy from
README, NOTICE, and DECISIONS: the no-database/no-backend tails, the
"Why it's interesting" heading, the not-a-clone paragraph, the
wordle-fr / Le Mot comparisons, the skypress.blog mentions, the
no-git-remote handoff note, and the redundant 0-vulnerabilities aside.
Also remove the tangled.org source link (and its inlined mark) from the
app footer, along with the now-unused sourceCode i18n strings, and
update the footer tests accordingly.
Make tile results legible without relying on colour alone
Early player feedback on Bluesky flagged two problems with the board:
there was no explainer, so a first-time player couldn't tell whether a
coloured tile meant "right spot" or "right letter"; and the present
colour read as red, making the palette feel like the red/green pairing
that colour-blind players struggle with.
Address both, plus two consistency issues the rework surfaced:
- Onboarding: a persistent colour/symbol legend under the board, and
a "?" header button opening a "How to play" modal (native <dialog>,
bilingual, original copy) shown once on first visit. Wires up the
previously-unused howTo strings.
- Colour-blind mode: an opt-in footer toggle adds a per-tile symbol
(right spot / wrong spot / not in word) to the board, legend, and
modal. Off by default so the glyphs don't clutter the board for
players who don't need them; persisted in localStorage via a
cb-safe class on <html>. Per-tile aria labels already covered
screen readers and are unchanged.
- Present colour nudged from burnt amber #B45309 (reads red) to gold
#A16207, clearly distinct from teal and slate.
- Keyboard inversion: untried keys are a neutral grey, a ruled-out key
drops to a darker recessive tone. It was the opposite before -- in
dark mode an absent key turned lighter than an untried one.
- "Not in word" is one shared, theme-aware tone across the board tile,
keyboard key, and legend (light: slate #5B6776; dark: #232B35),
rather than slate on the board but dark on the keyboard.
Decisions recorded in DECISIONS.md (13, 13a, 13b, 14a).
Make tile results legible without relying on colour alone
Early player feedback on Bluesky flagged two problems with the board:
there was no explainer, so a first-time player couldn't tell whether a
coloured tile meant "right spot" or "right letter"; and the present
colour read as red, making the palette feel like the red/green pairing
that colour-blind players struggle with.
Address both, plus two consistency issues the rework surfaced:
- Onboarding: a persistent colour/symbol legend under the board, and
a "?" header button opening a "How to play" modal (native <dialog>,
bilingual, original copy) shown once on first visit. Wires up the
previously-unused howTo strings.
- Colour-blind mode: an opt-in footer toggle adds a per-tile symbol
(right spot / wrong spot / not in word) to the board, legend, and
modal. Off by default so the glyphs don't clutter the board for
players who don't need them; persisted in localStorage via a
cb-safe class on <html>. Per-tile aria labels already covered
screen readers and are unchanged.
- Present colour nudged from burnt amber #B45309 (reads red) to gold
#A16207, clearly distinct from teal and slate.
- Keyboard inversion: untried keys are a neutral grey, a ruled-out key
drops to a darker recessive tone. It was the opposite before -- in
dark mode an absent key turned lighter than an untried one.
- "Not in word" is one shared, theme-aware tone across the board tile,
keyboard key, and legend (light: slate #5B6776; dark: #232B35),
rather than slate on the board but dark on the keyboard.
Decisions recorded in DECISIONS.md (13, 13a, 13b, 14a).
Build AT Mot: bilingual, database-free daily word game on atproto
AT Mot is a static client-side word game native to the AT Protocol. There is
no application database and no bespoke backend: player data lives in each
player's PDS, and the leaderboard is read from the public Constellation
backlink index.
What's included:
- Lexicons under bzh.herve.atmot.* (result, stats, defs) following the Lexicon
Style Guide, with a format linter and an idempotent publish script.
- Deterministic UTC daily word engine for EN + FR, with self-curated word lists
from public-domain/MIT sources (ENABLE, an-array-of-french-words, hermitdave
frequency) — French sourced independently of wordle-fr. Frozen epoch
2026-06-23, frozen puzzleTarget format via a single shared helper.
- Browser OAuth (atcute), write-once result records + a mutable self stats /
declaration record, all written client-side to the player's PDS.
- "Share to atproto": app.bsky.feed.post with a standard emoji grid and the
puzzle permalink as a real link facet + embed (so Constellation can discover
it). Leaderboard + per-puzzle pages aggregate Constellation backlinks in the
browser with caching and graceful degradation; live likes/reposts/replies.
- Clean minimal-nerdy UI: own brand board palette (teal/amber/slate, not
Wordle's), SkyPress fonts (Overused Grotesk + IBM Plex Mono), prefers-color-
scheme dark mode, a11y (per-tile aria, non-hue cues), mobile-first.
- Cloudflare Pages target (SPA fallback, hosted client-metadata.json).
- Dual MIT/Apache-2.0, README/NOTICE/DECISIONS, 41 passing tests, 0 audit
vulnerabilities.
Obfuscate the daily-answer sequence to deter cheating
The ordered answer list shipped as plaintext English words in
src/data/words.*.json, so anyone could open the JSON — in the repo or
the JS bundle / network tab — find their puzzle number, and read today's
or any future day's word. answers[puzzleNumber-1] was literally the
word.
Ship the sequence as `answersEnc` instead: the ordered list joined,
XOR'd against a fixed key, and base64-encoded (src/engine/obfuscate.ts),
decoded once at module load in words.ts. One isomorphic module is shared
by the build script and the runtime so the two can never drift.
This is a deliberate speed bump, not encryption. With no backend the
full list must reach every browser to compute the word offline, and the
XOR key ships in the bundle, so a determined reader can still recover it.
The goal is only to defeat the trivial "read the list / grep for the
word" cheat — encoded, the list is no longer human-readable or
greppable.
The encoding decodes back to byte-identical words, so the per-puzzle
mapping is unchanged: recorded results (keyed by <lang>-<puzzleNumber>)
and in-progress local games from before the change still match. A new
guard in tests/words.test.ts pins known puzzle->word pairs so any future
renumbering of history fails loudly.
The committed JSON was re-emitted from the existing answers, not
re-fetched from the upstream sources, so word-list drift can't silently
renumber history as part of this change.
Build AT Mot: bilingual, database-free daily word game on atproto
AT Mot is a static client-side word game native to the AT Protocol. There is
no application database and no bespoke backend: player data lives in each
player's PDS, and the leaderboard is read from the public Constellation
backlink index.
What's included:
- Lexicons under bzh.herve.atmot.* (result, stats, defs) following the Lexicon
Style Guide, with a format linter and an idempotent publish script.
- Deterministic UTC daily word engine for EN + FR, with self-curated word lists
from public-domain/MIT sources (ENABLE, an-array-of-french-words, hermitdave
frequency) — French sourced independently of wordle-fr. Frozen epoch
2026-06-23, frozen puzzleTarget format via a single shared helper.
- Browser OAuth (atcute), write-once result records + a mutable self stats /
declaration record, all written client-side to the player's PDS.
- "Share to atproto": app.bsky.feed.post with a standard emoji grid and the
puzzle permalink as a real link facet + embed (so Constellation can discover
it). Leaderboard + per-puzzle pages aggregate Constellation backlinks in the
browser with caching and graceful degradation; live likes/reposts/replies.
- Clean minimal-nerdy UI: own brand board palette (teal/amber/slate, not
Wordle's), SkyPress fonts (Overused Grotesk + IBM Plex Mono), prefers-color-
scheme dark mode, a11y (per-tile aria, non-hue cues), mobile-first.
- Cloudflare Pages target (SPA fallback, hosted client-metadata.json).
- Dual MIT/Apache-2.0, README/NOTICE/DECISIONS, 41 passing tests, 0 audit
vulnerabilities.
Build AT Mot: bilingual, database-free daily word game on atproto
AT Mot is a static client-side word game native to the AT Protocol. There is
no application database and no bespoke backend: player data lives in each
player's PDS, and the leaderboard is read from the public Constellation
backlink index.
What's included:
- Lexicons under bzh.herve.atmot.* (result, stats, defs) following the Lexicon
Style Guide, with a format linter and an idempotent publish script.
- Deterministic UTC daily word engine for EN + FR, with self-curated word lists
from public-domain/MIT sources (ENABLE, an-array-of-french-words, hermitdave
frequency) — French sourced independently of wordle-fr. Frozen epoch
2026-06-23, frozen puzzleTarget format via a single shared helper.
- Browser OAuth (atcute), write-once result records + a mutable self stats /
declaration record, all written client-side to the player's PDS.
- "Share to atproto": app.bsky.feed.post with a standard emoji grid and the
puzzle permalink as a real link facet + embed (so Constellation can discover
it). Leaderboard + per-puzzle pages aggregate Constellation backlinks in the
browser with caching and graceful degradation; live likes/reposts/replies.
- Clean minimal-nerdy UI: own brand board palette (teal/amber/slate, not
Wordle's), SkyPress fonts (Overused Grotesk + IBM Plex Mono), prefers-color-
scheme dark mode, a11y (per-tile aria, non-hue cues), mobile-first.
- Cloudflare Pages target (SPA fallback, hosted client-metadata.json).
- Dual MIT/Apache-2.0, README/NOTICE/DECISIONS, 41 passing tests, 0 audit
vulnerabilities.
Obfuscate the daily-answer sequence to deter cheating
The ordered answer list shipped as plaintext English words in
src/data/words.*.json, so anyone could open the JSON — in the repo or
the JS bundle / network tab — find their puzzle number, and read today's
or any future day's word. answers[puzzleNumber-1] was literally the
word.
Ship the sequence as `answersEnc` instead: the ordered list joined,
XOR'd against a fixed key, and base64-encoded (src/engine/obfuscate.ts),
decoded once at module load in words.ts. One isomorphic module is shared
by the build script and the runtime so the two can never drift.
This is a deliberate speed bump, not encryption. With no backend the
full list must reach every browser to compute the word offline, and the
XOR key ships in the bundle, so a determined reader can still recover it.
The goal is only to defeat the trivial "read the list / grep for the
word" cheat — encoded, the list is no longer human-readable or
greppable.
The encoding decodes back to byte-identical words, so the per-puzzle
mapping is unchanged: recorded results (keyed by <lang>-<puzzleNumber>)
and in-progress local games from before the change still match. A new
guard in tests/words.test.ts pins known puzzle->word pairs so any future
renumbering of history fails loudly.
The committed JSON was re-emitted from the existing answers, not
re-fetched from the upstream sources, so word-list drift can't silently
renumber history as part of this change.