Play hub Phase 2: Word Search (LLM theme/words, code places the grid)

A calm second daily game, same philosophy as Daily Word — LLM proposes, code
disposes.

* LLM proposes a hopeful theme + ~8 words; code validates (alpha/length/dedup)
  and PLACES every word in a date-seeded grid, so the puzzle is always solvable.
  Curated fallback themes if the LLM is thin. Only placed words are returned;
  the solution cells (placements) are never sent to the client.
* GET /api/puzzle/wordsearch → {theme, words, grid, size}. No answer to hide:
  the grid and word list are meant to be seen — the play is finding them, which
  the client validates by reading the selected line off the grid.
* WordSearchGame.svelte: pointer-drag selection snapped to the 8 straight
  directions (mouse + touch), found-word highlighting, no-fail, no pressure
  timer — time is recorded quietly and shown at the end with a personal best.
  Spoiler-free share. localStorage progress (restores found cells + timer).
* Hub's Word Search card is now live with today's status; cycle pre-generates
  both games with the LLM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-10 20:15:19 -04:00
parent 1bc9925e40
commit 90cd0291a3
5 changed files with 390 additions and 9 deletions
+5 -3
View File
@@ -1431,10 +1431,12 @@ def create_app() -> FastAPI:
@app.get("/api/puzzle/{game}")
def daily_puzzle(game: str, variant: str = Query("5")) -> dict:
if game != "word" or variant not in games.WORD_VARIANTS:
raise HTTPException(status_code=404, detail="no such puzzle")
with get_conn() as conn:
return games.word_puzzle_response(conn, local_today(), variant)
if game == "word" and variant in games.WORD_VARIANTS:
return games.word_puzzle_response(conn, local_today(), variant)
if game == "wordsearch":
return games.wordsearch_response(conn, local_today())
raise HTTPException(status_code=404, detail="no such puzzle")
@app.post("/api/puzzle/word/guess")
def word_guess(body: WordGuessRequest) -> dict: