import { h, JSX, Fragment } from 'preact'; import { useState, useRef, useMemo, useCallback, useEffect } from 'preact/hooks'; import * as wasm from 'web-optimle'; import { clue, describeClue, type Clue, type GuessResult } from './clue'; import { gameName, speak } from './util'; import { COMMON_WORDS, UNCOMMON_WORDS } from './dictionary'; import { Row, RowState } from './Row'; import { Keyboard } from './Keyboard'; import { useLocalStorage } from './localstorage'; import { PuzzleType, PuzzleTypeDisplay } from './PuzzleType'; const MAX_GUESSES = 2; const WORD_LENGTH = 5; enum GameState { Playing, Won, Lost, } type History = GuessResult[]; type Puzzle = { history: History, seed: number, solutions: number, } interface GameProps { puzzleType: PuzzleType; puzzle: Puzzle; day: number; hidden: boolean; colorBlind: boolean; keyboardLayout: string; } type PuzzleStats = { tries: number, won: boolean, guesses?: GuessResult[], }; function defaultPuzzleStats() { return { tries: 1, won: false }; } type DailyStats = { [key in PuzzleType]: PuzzleStats }; export function Game(props: GameProps) { const { puzzleType, puzzle, day } = props; const [gameState, setGameState] = useState(GameState.Playing); const [guesses, setGuesses] = useState([]); const [currentGuess, setCurrentGuess] = useState(""); const [dailyStats, setDailyStats] = useLocalStorage<{ [key: number]: DailyStats }>("dailyStats", {}); const [todayStats, setTodayStats] = useState({ 2: defaultPuzzleStats(), 3: defaultPuzzleStats(), 4: defaultPuzzleStats(), }); const [hint, setHint] = useState(

Try to guarantee a win in 2 guesses!

); const tableRef = useRef(null); const game = useMemo( () => wasm.Game.new(COMMON_WORDS, puzzle.history), [puzzle] ); const [gameTree, setGameTree] = useState(); const reset = useCallback(() => { game.reset(); setGuesses([...puzzle.history]); setCurrentGuess(""); setGameState(GameState.Playing); setTodayStats((oldStats) => ({ ...oldStats, [puzzleType]: { ...oldStats[puzzleType], guesses: [], }, })); }, [puzzle]); useEffect(() => { const selectedPuzzle = todayStats[puzzleType]; setGuesses([...puzzle.history, ...(selectedPuzzle.guesses || [])]); game.reset(); setGameTree(undefined); selectedPuzzle.guesses?.forEach( (result) => game.guess(result.word) ); if (selectedPuzzle.won) { setGameState(GameState.Won); if (selectedPuzzle.tries === 1) { setHint(<> You won today's {' '} puzzle on your first try! Amazing job. ); } else { setHint(<> You won today's {' '} puzzle in {selectedPuzzle.tries} tries! Great job. ); } } else if ((selectedPuzzle.guesses?.length || 0) >= 2) { setGameState(GameState.Lost); setHint(

Not quite! The answer could've been {game.possible_word().toUpperCase()}. (Enter to try again)

); } else { setGameState(GameState.Playing); setHint(""); } }, [puzzle, puzzleType, todayStats]); useEffect(() => { const storedTodayStats = dailyStats[day]; if (storedTodayStats !== undefined) { setTodayStats(storedTodayStats); } }, []); useEffect(() => { setDailyStats({ ...dailyStats, [day]: todayStats, }); }, [todayStats]); async function share(copiedHint: string, text?: string) { const url = window.location.origin + window.location.pathname; const body = `${text}\n${url}`; if ( /android|iphone|ipad|ipod|webos/i.test(navigator.userAgent) && !/firefox/i.test(navigator.userAgent) ) { try { await navigator.share({ text: body }); return; } catch (e) { console.warn("navigator.share failed:", e); } } try { await navigator.clipboard.writeText(body); setHint(copiedHint); return; } catch (e) { console.warn("navigator.clipboard.writeText failed:", e); } setHint(url); } const onKey = (key: string) => { if (gameState !== GameState.Playing) { if (key === "Enter" && gameState === GameState.Lost) { reset(); } return; } if (guesses.length === MAX_GUESSES) return; if (/^[a-z]$/i.test(key)) { // We're about to update the current guess, but // we can't rely on the value updating in this render const newGuess = currentGuess + key.toLowerCase(); setCurrentGuess((guess) => (guess + key.toLowerCase()).slice(0, WORD_LENGTH) ); tableRef.current?.focus(); if (newGuess.length === WORD_LENGTH && !COMMON_WORDS.includes(newGuess) && UNCOMMON_WORDS.includes(newGuess) ) { setHint(`(The word ${newGuess.toUpperCase()} isn't in the secret list)`); } else { setHint(""); } } else if (key === "Backspace") { setCurrentGuess((guess) => guess.slice(0, -1)); setHint(""); } else if (key === "Enter") { if (currentGuess.length !== WORD_LENGTH) { setHint("Too short"); return; } if (!COMMON_WORDS.includes(currentGuess) && !UNCOMMON_WORDS.includes(currentGuess) ) { setHint("Not a valid word"); return; } const matches: number[] = game.guess(currentGuess); const result: GuessResult = { word: currentGuess, matches: matches, } setGuesses((guesses) => guesses.concat([result])); setTodayStats((oldStats) => ({ ...oldStats, [puzzleType]: { ...oldStats[puzzleType], guesses: [ ...(oldStats[puzzleType].guesses || []), result, ], }, })); setCurrentGuess(""); if (matches.every((m) => m === 2)) { setTodayStats((oldStats) => ({ ...oldStats, [puzzleType]: { ...oldStats[puzzleType], won: true, guesses: [ ...(oldStats[puzzleType].guesses || []), result, ], }, })); } else if (guesses.length + 1 === puzzle.history.length + MAX_GUESSES) { setTodayStats((oldStats) => ({ ...oldStats, [puzzleType]: { ...oldStats[puzzleType], tries: oldStats[puzzleType].tries + 1, }, })); } else { speak(describeClue(clue(result))); } } }; useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (!e.ctrlKey && !e.metaKey) { onKey(e.key); } if (e.key === "Backspace") { e.preventDefault(); } }; document.addEventListener("keydown", onKeyDown); return () => { document.removeEventListener("keydown", onKeyDown); }; }, [currentGuess, gameState]); let letterInfo = new Map(); const tableRows = Array(puzzle.history.length + MAX_GUESSES) .fill(undefined) .map((_, i) => { const isPartOfPuzzle = i < puzzle.history.length; const result = [ ...guesses, { word: currentGuess, matches: [0, 0, 0, 0, 0] }, ][i] ?? { word: "", matches: [0, 0, 0, 0, 0] }; const cluedLetters = clue(result); const lockedIn = i < guesses.length; if (lockedIn) { for (const { clue, letter } of cluedLetters) { if (clue === undefined) break; const old = letterInfo.get(letter); if (old === undefined || clue > old) { letterInfo.set(letter, clue); } } } return ( ); }); return (
{tableRows}

{hint || `\u00a0`}

{gameState === GameState.Won && (

)}

{gameTree && (

Here are all the ways the game could've responded:

{gameTree?.map((option) =>
{option.map((guessResult, i) => { const cluedLetters = clue(guessResult); return ( ); })}
)}
)}

Today's puzzle (#{puzzle.seed}) has {puzzle.solutions} path{puzzle.solutions === 1 ? '' : 's'} to a win.

); }