Daily Word: server-adjudicate guesses (answer no longer in the response)
Per Codex's v2 hardening. The GET /api/puzzle/word response no longer carries the answer at all — guesses POST to /api/puzzle/word/guess and the server returns the colour pattern, computed against the day's answer. The answer (and the "why") are revealed only once solved or the guesses are spent. This removes the "open DevTools, read the answer" issue without pretending to be a fortress (a deliberate crafted request can still peek; there's no leaderboard or prize, so that's fine). Client keeps local progress/stats; dict validation stays client-side. Trade-off accepted: each guess needs the API (the site already depends on it for today's content). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+18
-7
@@ -284,16 +284,27 @@ def test_since_endpoint(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def test_puzzle_endpoint(tmp_path, monkeypatch):
|
||||
import base64
|
||||
import os
|
||||
import sqlite3
|
||||
from goodnews import games
|
||||
from goodnews.localtime import local_today
|
||||
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 "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
|
||||
|
||||
# server-adjudicated guessing (answer revealed only on solve / exhaustion)
|
||||
c = sqlite3.connect(os.environ["GOODNEWS_DB"]); c.row_factory = sqlite3.Row
|
||||
ans = games.generate_word_puzzle(c, local_today(), "5")["answer"]
|
||||
mid = tc.post("/api/puzzle/word/guess", json={"variant": "5", "guess": "xxxxx", "n": 1}).json()
|
||||
assert len(mid["colors"]) == 5 and mid["solved"] is False and mid["answer"] is None
|
||||
win = tc.post("/api/puzzle/word/guess", json={"variant": "5", "guess": ans, "n": 2}).json()
|
||||
assert win["solved"] is True and win["answer"] == ans and all(x == "correct" for x in win["colors"])
|
||||
last = tc.post("/api/puzzle/word/guess", json={"variant": "5", "guess": "xxxxx", "n": 6}).json()
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user