Games: in-progress hub status + distribution-aware word-search placement (Codex)
- Play hub: word cards now surface IN-PROGRESS games too (not just won/lost) so "continue on another device" shows at a glance — card reads "5:3…" and the selection option says "Continue · 3/6". - Word Search generator: replace "prefer any crossing" with a SCORED placement — score = overlap*4 - local crowding (filled neighbours that aren't crossings) — then pick among the best ~20%. Keeps the organic interlocking but spreads words across the board instead of clumping around the first-placed (longest) words. Every word still placed (tests green). NOTE: changes today's grid layouts, so an in-progress word search resets once. 237 pytest + 11 vitest green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+26
-10
@@ -309,6 +309,20 @@ 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)]
|
||||
|
||||
|
||||
def _neighbour_fill(grid, cells, size: int) -> int:
|
||||
"""Filled cells in a candidate's footprint+border that AREN'T its own cells —
|
||||
a crowding measure, so placement can spread words out instead of clumping."""
|
||||
own = set(cells)
|
||||
rs = [r for r, _ in cells]
|
||||
cs = [c for _, c in cells]
|
||||
cnt = 0
|
||||
for r in range(max(0, min(rs) - 1), min(size, max(rs) + 2)):
|
||||
for c in range(max(0, min(cs) - 1), min(size, max(cs) + 2)):
|
||||
if (r, c) not in own and grid[r][c] is not None:
|
||||
cnt += 1
|
||||
return cnt
|
||||
|
||||
# Size tiers. The three sizes draw DISJOINT word slices from the day's pool, so
|
||||
# each is its own fresh puzzle (no repeats across sizes). small+med+large counts
|
||||
# sum to WS_NEEDED, the minimum unique words a theme must supply.
|
||||
@@ -513,23 +527,25 @@ def _build_grid(words: list[str], size: int, seed: int) -> tuple[list[str], list
|
||||
for word in sorted(words, key=len, reverse=True):
|
||||
if len(word) > size:
|
||||
continue
|
||||
# Gather valid placements, then PREFER one that crosses an already-placed
|
||||
# word (shares a matching letter) so the grid interlocks like a real word
|
||||
# search — falling back to any valid spot so every word still gets placed.
|
||||
valid = [] # (overlap_count, cells)
|
||||
# Gather valid placements and SCORE them: reward crossing an existing word
|
||||
# (so the grid interlocks like a real puzzle) but penalise crowding, so
|
||||
# words spread across the board instead of all clustering around the ones
|
||||
# placed first. Pick at random among the best ~20% to keep organic variety.
|
||||
scored = [] # (score, cells)
|
||||
for _ in range(400):
|
||||
dr, dc = rng.choice(_DIRS)
|
||||
r0, c0 = rng.randrange(size), rng.randrange(size)
|
||||
cells = [(r0 + dr * i, c0 + dc * i) for i in range(len(word))]
|
||||
if any(not (0 <= r < size and 0 <= c < size) for r, c in cells):
|
||||
continue
|
||||
if all(grid[r][c] in (None, word[i]) for i, (r, c) in enumerate(cells)):
|
||||
overlap = sum(1 for i, (r, c) in enumerate(cells) if grid[r][c] == word[i])
|
||||
valid.append((overlap, cells))
|
||||
if not valid:
|
||||
if not all(grid[r][c] in (None, word[i]) for i, (r, c) in enumerate(cells)):
|
||||
continue
|
||||
overlap = sum(1 for i, (r, c) in enumerate(cells) if grid[r][c] == word[i])
|
||||
scored.append((overlap * 4 - _neighbour_fill(grid, cells, size), cells))
|
||||
if not scored:
|
||||
continue
|
||||
crossing = [c for c in valid if c[0] > 0]
|
||||
_, cells = rng.choice(crossing if crossing else valid)
|
||||
scored.sort(key=lambda t: t[0], reverse=True)
|
||||
_, cells = rng.choice(scored[: max(1, len(scored) // 5)])
|
||||
for i, (r, c) in enumerate(cells):
|
||||
grid[r][c] = word[i]
|
||||
placed.append(word)
|
||||
|
||||
Reference in New Issue
Block a user