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:
+25
-1
@@ -295,7 +295,7 @@ def test_puzzle_endpoint(tmp_path, monkeypatch):
|
||||
assert "answer" not in r # the public puzzle response never carries the answer
|
||||
assert tc.get("/api/puzzle/word?variant=6").json()["guesses"] == 7
|
||||
assert tc.get("/api/puzzle/word?variant=9").status_code == 404
|
||||
assert tc.get("/api/puzzle/wordsearch").status_code == 404
|
||||
assert tc.get("/api/puzzle/nonsense").status_code == 404
|
||||
|
||||
# server-adjudicated guessing (answer revealed only on solve / exhaustion)
|
||||
c = sqlite3.connect(os.environ["GOODNEWS_DB"]); c.row_factory = sqlite3.Row
|
||||
@@ -308,3 +308,27 @@ def test_puzzle_endpoint(tmp_path, monkeypatch):
|
||||
assert last["answer"] == ans # exhausting guesses reveals it even when wrong
|
||||
# wrong length → 400
|
||||
assert tc.post("/api/puzzle/word/guess", json={"variant": "5", "guess": "toolong", "n": 1}).status_code == 400
|
||||
|
||||
|
||||
def test_wordsearch_endpoint(tmp_path, monkeypatch):
|
||||
app, api = _make(tmp_path, monkeypatch)
|
||||
tc = TestClient(app)
|
||||
r = tc.get("/api/puzzle/wordsearch").json()
|
||||
assert r["game"] == "wordsearch" and r["theme"]
|
||||
assert len(r["grid"]) == r["size"] and all(len(row) == r["size"] for row in r["grid"])
|
||||
assert "placements" not in r and len(r["words"]) >= 6 # solution cells never sent
|
||||
# every returned word is genuinely placed → the puzzle is solvable
|
||||
grid, n = r["grid"], r["size"]
|
||||
dirs = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
|
||||
|
||||
def findable(w):
|
||||
for r0 in range(n):
|
||||
for c0 in range(n):
|
||||
for dr, dc in dirs:
|
||||
cells = [(r0 + dr * i, c0 + dc * i) for i in range(len(w))]
|
||||
if all(0 <= rr < n and 0 <= cc < n for rr, cc in cells) and \
|
||||
"".join(grid[rr][cc] for rr, cc in cells) == w:
|
||||
return True
|
||||
return False
|
||||
|
||||
assert all(findable(w) for w in r["words"])
|
||||
|
||||
Reference in New Issue
Block a user