diff --git a/frontend/src/lib/components/WordSearchGame.svelte b/frontend/src/lib/components/WordSearchGame.svelte
index e70eb11..5ba808a 100644
--- a/frontend/src/lib/components/WordSearchGame.svelte
+++ b/frontend/src/lib/components/WordSearchGame.svelte
@@ -154,12 +154,15 @@
{/each}
-
@@ -183,22 +186,28 @@
.theme .lbl { display: block; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.64rem;
font-family: var(--label); color: var(--muted); margin-bottom: 2px; }
.grid {
- display: grid; grid-template-columns: repeat(var(--n), 1fr); gap: 2px;
- width: min(100%, 420px); margin: 0 auto 16px; touch-action: none; user-select: none;
- -webkit-user-select: none; border-radius: 10px; padding: 6px; background: var(--surface);
- border: 1px solid var(--line);
+ /* Constant cell size — the board grows with the grid; on a narrow phone the
+ biggest grid gently shrinks to fit the width. */
+ --cell: min(32px, calc((100vw - 44px) / var(--n)));
+ display: grid; grid-template-columns: repeat(var(--n), var(--cell)); gap: 2px;
+ width: fit-content; max-width: 100%; margin: 0 auto 16px; touch-action: none;
+ user-select: none; -webkit-user-select: none; border-radius: 10px; padding: 6px;
+ background: var(--surface); border: 1px solid var(--line);
}
.grid.done { opacity: 0.9; }
.cell {
- aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
- font-family: var(--label); font-weight: 600; font-size: clamp(0.62rem, 2.7vw, 1rem);
+ width: var(--cell); height: var(--cell); display: flex; align-items: center; justify-content: center;
+ font-family: var(--label); font-weight: 600; font-size: calc(var(--cell) * 0.5);
color: var(--ink); border-radius: 5px; background: transparent; text-transform: uppercase;
}
.cell.sel { background: var(--accent) !important; color: #fff !important; }
- .words { list-style: none; display: flex; flex-wrap: wrap; gap: 7px 9px; justify-content: center;
- padding: 0; margin: 0 0 14px; }
- .words li { font-family: var(--label); font-size: 0.8rem; letter-spacing: 0.04em; color: var(--ink);
- padding: 2px 9px; border-radius: 999px; }
+ .palette { background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
+ padding: 12px 14px 14px; margin: 0 auto 16px; max-width: 440px; box-shadow: var(--shadow); }
+ .plabel { text-transform: uppercase; letter-spacing: 0.12em; font-size: 0.62rem; font-family: var(--label);
+ color: var(--muted); text-align: center; margin: 0 0 10px; }
+ .words { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; padding: 0; margin: 0; }
+ .words li { font-family: var(--label); font-size: 0.82rem; letter-spacing: 0.04em; color: var(--ink);
+ padding: 4px 11px; border-radius: 999px; background: var(--accent-soft); transition: background 0.2s ease; }
.hint { text-align: center; color: var(--muted); font-size: 0.84rem; margin: 0; }
.result { text-align: center; }
.rmark { font-family: var(--serif); font-style: italic; color: var(--accent-deep); font-size: 1.2rem; margin: 0 0 12px; }
diff --git a/frontend/src/routes/play/+page.svelte b/frontend/src/routes/play/+page.svelte
index 78b2a16..6d867e5 100644
--- a/frontend/src/routes/play/+page.svelte
+++ b/frontend/src/routes/play/+page.svelte
@@ -113,9 +113,9 @@
{:else if view === 'wordsearch'}
-
-
-
+
+
+
{/if}
diff --git a/goodnews/games.py b/goodnews/games.py
index 40f473a..1a65361 100644
--- a/goodnews/games.py
+++ b/goodnews/games.py
@@ -159,28 +159,38 @@ def adjudicate_word_guess(conn: sqlite3.Connection, date: str, variant: str, gue
_DIRS = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
# Size tiers: bigger grid → more words → a longer sit. Built per-request from one
-# stored theme+word list, so all three sizes share the day's theme.
-WS_TIERS = {"small": {"grid": 8, "count": 6}, "med": {"grid": 11, "count": 9}, "large": {"grid": 14, "count": 13}}
+# stored theme+word list, sampling a different subset per size so each is its own
+# puzzle, while all three share the day's theme.
+WS_TIERS = {"small": {"grid": 8, "count": 6}, "med": {"grid": 11, "count": 10}, "large": {"grid": 14, "count": 14}}
+WS_TARGET = 28 # words to gather per theme, so there's variety to sample from
-# Curated fallbacks — calm and neutral everyday scenes, not just upbeat. ~13
-# words each (4–8 letters, uppercase) so the large tier has enough to place.
+# Curated fallbacks — calm and neutral everyday scenes, not just upbeat. ~22 words
+# each (4–8 letters, uppercase) so every size has a fresh subset to draw from.
_WS_FALLBACKS = [
- ("Around the house", ["TABLE", "CHAIR", "CLOCK", "SHELF", "COUCH", "LAMP", "PILLOW", "WINDOW",
- "CARPET", "MIRROR", "CANDLE", "KETTLE", "DRAWER"]),
- ("At the beach", ["WAVES", "SHELL", "SANDY", "TIDE", "SHORE", "TOWEL", "BREEZE", "SUNSET",
- "PEBBLE", "CORAL", "OCEAN", "SAILS", "PIER"]),
- ("In the kitchen", ["BREAD", "SPOON", "PLATE", "KETTLE", "FLOUR", "APRON", "WHISK", "SUGAR",
- "BUTTER", "RECIPE", "SIMMER", "PANTRY", "TEAPOT"]),
- ("In the garden", ["BLOOM", "PETAL", "ROOTS", "LEAF", "GARDEN", "FLOWER", "SUNNY", "SEEDS",
- "MEADOW", "SPROUT", "HEDGE", "TROWEL", "VINES"]),
- ("A walk outdoors", ["TRAIL", "MEADOW", "BROOK", "BIRDS", "BREEZE", "PEBBLE", "FOREST", "MAPLE",
- "ACORN", "STREAM", "BRANCH", "VALLEY", "PATH"]),
- ("Making music", ["PIANO", "DRUMS", "CHOIR", "MELODY", "GUITAR", "VIOLIN", "SINGER", "BALLAD",
- "RHYTHM", "ENCORE", "TEMPO", "NOTES", "SONG"]),
- ("Quiet calm", ["PEACE", "QUIET", "STILL", "SERENE", "REST", "SOOTHE", "GENTLE", "BREATHE",
- "CALM", "HUSH", "DRIFT", "EASE", "DREAM"]),
- ("Small joys", ["SMILE", "LAUGH", "CHEER", "HAPPY", "MERRY", "DANCE", "DELIGHT", "GLOW",
- "PLAY", "GRIN", "BEAM", "GLEE", "WARM"]),
+ ("Around the house", ["TABLE", "CHAIR", "CLOCK", "SHELF", "COUCH", "PILLOW", "WINDOW", "CARPET",
+ "MIRROR", "CANDLE", "KETTLE", "DRAWER", "CLOSET", "CURTAIN", "CUSHION",
+ "BASKET", "BOTTLE", "TOWEL", "BROOM", "LADDER", "STAIRS", "PANTRY", "BLANKET"]),
+ ("At the beach", ["WAVES", "SHELL", "SANDY", "TIDE", "SHORE", "TOWEL", "BREEZE", "SUNSET", "PEBBLE",
+ "CORAL", "OCEAN", "SAILS", "SURF", "SEAGULL", "BUCKET", "SPADE", "DUNES", "LAGOON",
+ "DRIFT", "SALTY", "SUNNY", "HORIZON", "COVE", "PIER"]),
+ ("In the kitchen", ["BREAD", "SPOON", "PLATE", "KETTLE", "FLOUR", "APRON", "WHISK", "SUGAR", "BUTTER",
+ "RECIPE", "SIMMER", "PANTRY", "TEAPOT", "SAUCER", "LADLE", "KNIFE", "BOWL", "GRATER",
+ "SKILLET", "PEPPER", "GARLIC", "HONEY", "TOAST"]),
+ ("In the garden", ["BLOOM", "PETAL", "ROOTS", "LEAF", "GARDEN", "FLOWER", "SUNNY", "SEEDS", "MEADOW",
+ "SPROUT", "HEDGE", "TROWEL", "VINES", "SOIL", "SHRUB", "BUDS", "STALK", "DAISY",
+ "TULIP", "FERNS", "SHOVEL", "BRANCH", "BREEZE"]),
+ ("A walk outdoors", ["TRAIL", "MEADOW", "BROOK", "BIRDS", "BREEZE", "PEBBLE", "FOREST", "MAPLE", "ACORN",
+ "STREAM", "BRANCH", "VALLEY", "PATH", "HILLS", "RIVER", "FIELD", "CLOUDS", "LEAVES",
+ "MOSSY", "TWIGS", "FENCE", "BENCH", "RAMBLE"]),
+ ("Making music", ["PIANO", "DRUMS", "CHOIR", "MELODY", "GUITAR", "VIOLIN", "SINGER", "BALLAD", "RHYTHM",
+ "ENCORE", "TEMPO", "NOTES", "SONG", "FLUTE", "CELLO", "BRASS", "CHORD", "STRUM",
+ "HARMONY", "LYRICS", "BANJO", "ANTHEM", "TUNES"]),
+ ("Quiet calm", ["PEACE", "QUIET", "STILL", "SERENE", "REST", "SOOTHE", "GENTLE", "BREATHE", "CALM",
+ "HUSH", "DRIFT", "EASE", "DREAM", "RELAX", "MELLOW", "STEADY", "SETTLE", "LINGER",
+ "PAUSE", "SLOW", "SOFT", "WARM"]),
+ ("Small joys", ["SMILE", "LAUGH", "CHEER", "HAPPY", "MERRY", "DANCE", "DELIGHT", "GLOW", "PLAY", "GRIN",
+ "BEAM", "GLEE", "WARM", "GIGGLE", "SHARE", "TREAT", "SUNNY", "SWEET", "LUCKY", "CHARM",
+ "SPARK", "BLISS"]),
]
@@ -193,9 +203,9 @@ def _ws_propose(client) -> tuple[str, list[str]] | None:
{"role": "system", "content": "You set up a calm word search. The theme can be uplifting OR just a "
"pleasant everyday scene (e.g. 'Around the house', 'At the beach', "
"'In the kitchen'). Reply exactly as two lines:\n"
- "THEME: <2-4 word theme>\nWORDS: W1, W2, ... W13\n"
- "Give 13 single real words, 4-8 letters, UPPERCASE, related to the theme, "
- "nothing negative or unpleasant, no phrases."},
+ "THEME: <2-4 word theme>\nWORDS: W1, W2, ... W28\n"
+ f"Give {WS_TARGET} single real words, 4-8 letters, UPPERCASE, related to the "
+ "theme, a good mix of lengths, nothing negative or unpleasant, no phrases."},
{"role": "user", "content": "Give me one calm theme."},
]
text = client.chat_text(msg) or ""
@@ -272,8 +282,11 @@ def wordsearch_response(conn: sqlite3.Connection, date: str, size: str = "med")
size = "med"
p = generate_wordsearch_puzzle(conn, date) # on-demand (curated fallback) if missing
tier = WS_TIERS[size]
- usable = [w for w in p["words"] if len(w) <= tier["grid"]][:tier["count"]]
- grid, placed = _build_grid(usable, tier["grid"], _seed(date, "wordsearch", size))
+ usable = [w for w in p["words"] if len(w) <= tier["grid"]]
+ # Sample a fresh subset per size so each tier is its own puzzle (seeded → stable).
+ rng = random.Random(_seed(date, "wordsearch", size, "pick"))
+ chosen = rng.sample(usable, min(tier["count"], len(usable))) if usable else []
+ grid, placed = _build_grid(chosen, tier["grid"], _seed(date, "wordsearch", size))
return {"game": "wordsearch", "date": date, "size": size, "theme": p["theme"],
"words": placed, "grid": grid}