import InfoBox from '../components/InfoBox.astro'

A report came in that the Norwegian AI opponent had played a profanity as its move. The dictionary that word came from has to stay complete, since removing it would also make it invalid for a player who genuinely typed it, so the fix couldn't touch the dictionary at all. The locked design calls it the Wordle model: the game never volunteers a profanity as its own choice, but nothing about what a player can type changes.

That framing turned the fix into a selection filter rather than a data problem, and a quick inventory found exactly two places in the whole codebase where the game chooses a dictionary word to display on its own: the AI opponent's move, and the PvP timeout fallback that plays a word for a player who ran out the clock. Both already funneled through the same [word-candidate service](/posts/carving-seams#naming-the-seams), so the filter only needed one chokepoint, not two.

The blocklist itself is vendored, not generated: a snapshot of a public offensive-word list per language, plus a repo-owned additions file as the place to add anything the snapshot misses, compiled straight into the server bundle via a raw-text import at build time rather than read from disk or a database table at runtime, so an edit to either file ships on the next ordinary deploy with no migration and no rebuild. A word gets filtered if it matches or contains a blocklist entry in its own language, since Norwegian compounds words freely enough that exact matching alone would miss the profanity glued onto an otherwise ordinary word; over-flagging costs almost nothing under the Wordle model, since a flagged word just never gets offered by the AI and stays fully playable if someone types it themselves.

<InfoBox title="Wired in, not bolted on">
  The filter is a required dependency on the word-candidate service rather than an
  optional one, so no code path can construct the service without it in place. A
  word that does get flagged also joins the same exclusion list the service
  already used to avoid repeating a word, so the next batch it fetches offers a
  real replacement instead of coming back short.
</InfoBox>

Left out on purpose: a player's own initials or display name can carry exactly the same kind of profanity, visible to the other player rather than chosen by the game, but that's content moderation with the opposite tradeoff, a false positive there blocks a real person rather than an AI, and it stayed a separate issue rather than folding two different problems into one fix.