From 215a5c4d645fbd148590c6dcbb0f6d7d3e85535f Mon Sep 17 00:00:00 2001 From: jay Date: Wed, 10 Jun 2026 16:06:20 -0400 Subject: [PATCH] Play hub + Daily Word game (Phase 1 of the games feature) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/package-lock.json | 11 + frontend/package.json | 1 + frontend/src/lib/components/BottomNav.svelte | 8 +- frontend/src/lib/components/Header.svelte | 4 + frontend/src/lib/components/WordGame.svelte | 246 +++++++++++++++++++ frontend/src/routes/+page.svelte | 2 +- frontend/src/routes/play/+page.svelte | 125 ++++++++++ frontend/static/words-5.json | 1 + frontend/static/words-6.json | 1 + goodnews/api.py | 10 +- goodnews/cli.py | 9 + goodnews/data/wordpool.json | 101 ++++++++ goodnews/db.py | 10 + goodnews/games.py | 129 ++++++++++ tests/test_admin.py | 16 ++ 15 files changed, 668 insertions(+), 6 deletions(-) create mode 100644 frontend/src/lib/components/WordGame.svelte create mode 100644 frontend/src/routes/play/+page.svelte create mode 100644 frontend/static/words-5.json create mode 100644 frontend/static/words-6.json create mode 100644 goodnews/data/wordpool.json create mode 100644 goodnews/games.py diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f3cc29b..aa8ab6c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@sveltejs/adapter-static": "^3.0.6", "@sveltejs/kit": "^2.8.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", + "an-array-of-english-words": "^2.0.0", "svelte": "^5.1.0", "vite": "^6.0.0" } @@ -940,6 +941,16 @@ "node": ">=0.4.0" } }, + "node_modules/an-array-of-english-words": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/an-array-of-english-words/-/an-array-of-english-words-2.0.0.tgz", + "integrity": "sha512-FXnNvZSOI27kkKXeLSquhaTGP7z198UOQ4txaYO9fCfrjCh+D5SV7G7XqzEH0229+pAi4cjBEZ4WIQYgjKtO7Q==", + "dev": true, + "bin": { + "an-array-of-english-words": "cli.js", + "words": "cli.js" + } + }, "node_modules/aria-query": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 82559a5..c7a213d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "@sveltejs/adapter-static": "^3.0.6", "@sveltejs/kit": "^2.8.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", + "an-array-of-english-words": "^2.0.0", "svelte": "^5.1.0", "vite": "^6.0.0" } diff --git a/frontend/src/lib/components/BottomNav.svelte b/frontend/src/lib/components/BottomNav.svelte index 4f08d47..49e3c18 100644 --- a/frontend/src/lib/components/BottomNav.svelte +++ b/frontend/src/lib/components/BottomNav.svelte @@ -2,7 +2,7 @@ // Mobile-only primary navigation. Highlights = the brief, Latest = the // chronological feed, Browse = mood/topic discovery, You = account. import Avatar from './Avatar.svelte'; - let { active = 'today', onToday, onLatest, onBrowse, onYou, user = null } = $props(); + let { active = 'today', onToday, onLatest, onPlay, onYou, user = null } = $props();