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:
@@ -2,11 +2,13 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { getJSON } from '$lib/api.js';
|
||||
import WordGame from '$lib/components/WordGame.svelte';
|
||||
import WordSearchGame from '$lib/components/WordSearchGame.svelte';
|
||||
|
||||
let view = $state('hub'); // 'hub' | 'word'
|
||||
let view = $state('hub'); // 'hub' | 'word' | 'wordsearch'
|
||||
let variant = $state('5');
|
||||
let date = $state('');
|
||||
let wordStatus = $state({ 5: null, 6: null }); // null | {status, tries, max}
|
||||
let wsStatus = $state(null); // null | {status, found, total, ms}
|
||||
|
||||
function readStatus(v) {
|
||||
try {
|
||||
@@ -17,8 +19,26 @@
|
||||
} catch { /* ignore */ }
|
||||
return null;
|
||||
}
|
||||
function readWs() {
|
||||
try {
|
||||
const s = JSON.parse(localStorage.getItem(`goodnews:wordsearch:${date}`) || 'null');
|
||||
if (s && Array.isArray(s.found)) return { status: s.status, found: s.found.length, ms: s.ms };
|
||||
} catch { /* ignore */ }
|
||||
return null;
|
||||
}
|
||||
function refreshStatus() {
|
||||
wordStatus = { 5: readStatus('5'), 6: readStatus('6') };
|
||||
wsStatus = readWs();
|
||||
}
|
||||
function fmtMs(ms) {
|
||||
const s = Math.round(ms / 1000);
|
||||
return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
|
||||
}
|
||||
function wsLabel() {
|
||||
if (!wsStatus) return 'Find the day’s themed words';
|
||||
if (wsStatus.status === 'done') return `Today: cleared${wsStatus.ms ? ' · ' + fmtMs(wsStatus.ms) : ''}`;
|
||||
if (wsStatus.found > 0) return `Today: ${wsStatus.found} found`;
|
||||
return 'Find the day’s themed words';
|
||||
}
|
||||
|
||||
function wordLabel() {
|
||||
@@ -68,14 +88,14 @@
|
||||
<p class="gc-status" class:played={wordStatus['5'] || wordStatus['6']}>{wordLabel()}</p>
|
||||
</div>
|
||||
</button>
|
||||
<div class="gamecard soon" aria-disabled="true">
|
||||
<button class="gamecard" onclick={() => (view = 'wordsearch')}>
|
||||
<div class="gc-icon">▦</div>
|
||||
<div class="gc-body">
|
||||
<h2>Word Search</h2>
|
||||
<p class="gc-sub">Find the day’s themed words</p>
|
||||
<p class="gc-status">Coming soon</p>
|
||||
<p class="gc-status" class:played={wsStatus && (wsStatus.status === 'done' || wsStatus.found > 0)}>{wsLabel()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{:else if view === 'word'}
|
||||
<div class="variant">
|
||||
@@ -83,6 +103,8 @@
|
||||
<button class="vchip" class:on={variant === '6'} onclick={() => (variant = '6')}>Long Word<span>6 letters · 7 guesses</span></button>
|
||||
</div>
|
||||
<WordGame {variant} onstatus={refreshStatus} />
|
||||
{:else if view === 'wordsearch'}
|
||||
<WordSearchGame onstatus={refreshStatus} />
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user