5a9ff8dda9
Foundation for a gallery of RL-environment demos. Three decisions worth recording here rather than in a commit nobody reads: The word lists are built, not copied. `envs/wordle_five/words/build_words.py` intersects Wordnik (MIT, 11,846 five-letter words) with SCOWL's common-American tier to produce 4,603 answers. The intersection is the point: the list is derived from two permissive sources by a stated rule rather than copied from anyone's editorial selection, and both inputs are committed so a rebuild is byte-identical. The design tokens are PIG's, inlined as literals. PIG writes its accent onto the root at runtime because a user picks it; this site has no such choice, so the runtime theme layer would be a moving part buying nothing. Board tiles get their own named tokens with measured contrast ratios, because the board is the one place where colour carries meaning. pnpm 11 no longer reads the "pnpm" field in package.json. Settings live in pnpm-workspace.yaml, and an unapproved build script makes `pnpm install` exit 1 rather than warn — so this would have failed CI on a clean checkout, not here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
60 lines
2.4 KiB
Python
60 lines
2.4 KiB
Python
"""Build the guess and answer lists from two permissively-licensed sources.
|
|
|
|
Deterministic and offline: the inputs are committed beside this script, so a
|
|
rebuild on any machine reproduces the shipped JSON byte for byte. See
|
|
PROVENANCE.md for the licences and why the lists are constructed this way
|
|
rather than copied from the original game.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).parent
|
|
|
|
# Words we never make the hidden answer. Guesses stay permissive — the original
|
|
# game accepts these too — but no boardroom demo should ever reveal one as the
|
|
# solution. Deliberately short and hand-curated: a large imported blocklist
|
|
# would drag in its own licence and its own false positives.
|
|
BLOCKED = {
|
|
"bitch", "boner", "chink", "cocks", "coons", "crack", "cunts", "dagos",
|
|
"dicks", "dildo", "dykes", "fagot", "farts", "gooks", "gypsy", "harem",
|
|
"hussy", "incel", "junky", "kikes", "lynch", "micks", "nazis", "negro",
|
|
"nonce", "pussy", "queer", "raped", "rapes", "retch", "sperm", "spick",
|
|
"spics", "tards", "titty", "twats", "wench", "whore", "wogs",
|
|
}
|
|
|
|
FIVE = re.compile(r"^[a-z]{5}$")
|
|
|
|
|
|
def five_letter(path: Path) -> set[str]:
|
|
"""Every five-letter lowercase ASCII word in a whitespace/quote-delimited file."""
|
|
raw = path.read_text(encoding="utf-8", errors="ignore")
|
|
return {w for w in raw.replace('"', " ").replace(",", " ").lower().split() if FIVE.match(w)}
|
|
|
|
|
|
def main() -> None:
|
|
guesses = five_letter(HERE / "wordnik-20210729.txt")
|
|
common = five_letter(HERE / "scowl-wamerican-2020.12.07.txt")
|
|
|
|
# Answers are the intersection: a word must be in Wordnik (so it is a real
|
|
# headword) AND in SCOWL's common-American tier (so a non-specialist has
|
|
# plausibly met it). The intersection is what makes this list ours — it is
|
|
# derived from two permissive sources by a stated rule, not copied from
|
|
# anyone's editorial selection.
|
|
answers = sorted((guesses & common) - BLOCKED)
|
|
|
|
# Every answer must also be guessable, or the game is unwinnable.
|
|
assert set(answers) <= guesses, "answer list escaped the guess list"
|
|
|
|
out = HERE
|
|
(out / "guesses.json").write_text(json.dumps(sorted(guesses)) + "\n")
|
|
(out / "answers.json").write_text(json.dumps(answers) + "\n")
|
|
print(f"guesses={len(guesses)} answers={len(answers)} blocked={len(BLOCKED & (guesses & common))}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|