Play hub + Daily Word game (Phase 1 of the games feature)

A calm /play space — "after the brief, a small thing to enjoy." Framework-ready
for more games (Word Search next; zen/coloring later).

* Daily Word (5 letters / 6 guesses) + Long Word (6 / 7) — same Wordle mechanic,
  Upbeat Bytes flavor (no "Wordle" in the UI). Hopeful answers; after solving, a
  one-line "why this word matters."
* LLM proposes, code disposes: answers are picked deterministically by date-seed
  from a hand-curated hopeful pool that's pre-validated ⊆ the guess dictionary
  (always typeable), avoiding recent repeats; the LLM only adds the optional
  "why" (with fallback). daily_puzzles(date, game, variant, payload) stores them
  so everyone gets the same daily; the cycle pre-generates with the "why".
* Bundled guess dictionaries (words-5/6.json, ~12.6k/22.4k) for client-side guess
  validation — never the LLM. Answer lightly obfuscated (base64) in the payload.
* Private, gentle stats (played/solved/streak, guess distribution); spoiler-free
  emoji-grid share. No leaderboard, no timer, no streak-loss drama.
* Play in the bottom nav (replacing Browse, still on the lane rail) + the header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-10 16:06:20 -04:00
parent d0fb153e46
commit 215a5c4d64
15 changed files with 668 additions and 6 deletions
+16
View File
@@ -281,3 +281,19 @@ def test_since_endpoint(tmp_path, monkeypatch):
assert r["count"] == 1 and [i["id"] for i in r["items"]] == [3] # only the post-2027 article
assert tc.get("/api/since?ts=2099-01-01T00:00:00Z").json()["count"] == 0 # nothing newer
assert tc.get("/api/since?ts=not-a-date").json()["count"] == 0 # invalid ts → quiet 0
def test_puzzle_endpoint(tmp_path, monkeypatch):
import base64
app, api = _make(tmp_path, monkeypatch)
tc = TestClient(app)
r = tc.get("/api/puzzle/word?variant=5").json()
assert r["game"] == "word" and r["variant"] == "5" and r["length"] == 5 and r["guesses"] == 6
assert len(base64.b64decode(r["answer"]).decode()) == 5
r6 = tc.get("/api/puzzle/word?variant=6").json()
assert len(base64.b64decode(r6["answer"]).decode()) == 6 and r6["guesses"] == 7
# deterministic for the same day
assert tc.get("/api/puzzle/word?variant=5").json()["answer"] == r["answer"]
# unknown variant / game → 404
assert tc.get("/api/puzzle/word?variant=9").status_code == 404
assert tc.get("/api/puzzle/wordsearch").status_code == 404