import { useCallback, useEffect, useMemo, useState } from 'react'; import { Moon, RotateCcw } from 'lucide-react'; import { AccessWarning } from './components/AccessWarning'; import { Board } from './components/Board'; import { GameOverPanel } from './components/GameOverPanel'; import { Keyboard } from './components/Keyboard'; import { ModeSelector } from './components/ModeSelector'; import { SettingsPanel } from './components/SettingsPanel'; import { StatsPanel } from './components/StatsPanel'; import { DEFAULT_SETTINGS, MODE_LABELS, getMaxGuesses } from './lib/modes'; import { useLocalStorage } from './lib/storage'; import { getDateKey, getInfiniteLength, getSuperEvilLength, isValidGuess, mergeKeyboardState, normalizeGuess, pickDailyWord, pickRandomWord, } from './lib/wordUtils'; import type { GameMode, GameStatus, SavedProgress, Settings, Stats } from './types'; type Session = { mode: GameMode; target: string; guesses: string[]; currentGuess: string; status: GameStatus; level: number; isPractice: boolean; message: string; }; const initialStats: Stats = { gamesPlayed: 0, wins: 0, losses: 0, currentStreak: 0, bestStreak: 0, infiniteLevel: 1, infiniteWins: 0, infiniteLosses: 0, }; const getModeLength = (mode: GameMode, settings: Settings, level: number, practice: boolean) => { if (mode === 'custom') return settings.wordLength; if (mode === 'infinite') return getInfiniteLength(level); if (mode === 'super-evil') return practice ? getSuperEvilLength() : 10 + (level % 6); return 5; }; const createSession = ( mode: GameMode, settings: Settings, level = 1, isPractice = false, ): Session => { const wordLength = getModeLength(mode, settings, level, isPractice); const target = mode === 'daily' && !isPractice ? pickDailyWord(wordLength, 'daily') : mode === 'evil-daily' && !isPractice ? pickDailyWord(wordLength, 'evil') : pickRandomWord(wordLength); return { mode, target, guesses: [], currentGuess: '', status: 'playing', level, isPractice, message: mode === 'super-evil' ? 'Super Evil gives fewer guesses and very long words.' : '', }; }; const restoreSession = (saved: SavedProgress): Session => ({ mode: saved.mode, target: saved.target, guesses: saved.guesses, currentGuess: '', status: saved.status, level: saved.level, isPractice: saved.isPractice, message: '', }); const canRestore = (saved: SavedProgress | null) => { if (!saved) return false; if ((saved.mode === 'daily' || saved.mode === 'evil-daily') && !saved.isPractice) { return saved.dateKey === getDateKey(); } return true; }; export default function App() { const [settings, setSettings] = useLocalStorage('evil-wordle-settings', DEFAULT_SETTINGS); const [stats, setStats] = useLocalStorage('evil-wordle-stats', initialStats); const [savedProgress, setSavedProgress] = useLocalStorage('evil-wordle-progress', null); const [shake, setShake] = useState(false); const [session, setSession] = useState(() => canRestore(savedProgress) ? restoreSession(savedProgress as SavedProgress) : createSession('daily', settings), ); const wordLength = session.target.length; const maxGuesses = useMemo( () => getMaxGuesses(session.mode, wordLength, settings.difficulty), [session.mode, settings.difficulty, wordLength], ); const evilMemoryPhase = session.mode === 'evil-daily' && session.guesses.length >= 2; const keyboardStates = useMemo( () => (evilMemoryPhase ? new Map() : mergeKeyboardState(session.guesses, session.target)), [evilMemoryPhase, session.guesses, session.target], ); useEffect(() => { setSavedProgress({ dateKey: getDateKey(), mode: session.mode, target: session.target, guesses: session.guesses, status: session.status, level: session.level, isPractice: session.isPractice, }); }, [session, setSavedProgress]); useEffect(() => { if (!shake) return undefined; const timer = window.setTimeout(() => setShake(false), 300); return () => window.clearTimeout(timer); }, [shake]); const startNewSession = useCallback( (mode: GameMode, level = mode === 'infinite' ? stats.infiniteLevel : 1, practice = false) => { setSession(createSession(mode, settings, level, practice)); }, [settings, stats.infiniteLevel], ); const updateStatsForResult = useCallback( (status: GameStatus, mode: GameMode, level: number) => { if (status === 'playing') return; setStats((current) => { const won = status === 'won'; const nextStreak = won ? current.currentStreak + 1 : 0; return { ...current, gamesPlayed: current.gamesPlayed + 1, wins: current.wins + (won ? 1 : 0), losses: current.losses + (won ? 0 : 1), currentStreak: nextStreak, bestStreak: Math.max(current.bestStreak, nextStreak), infiniteLevel: mode === 'infinite' && won ? Math.max(current.infiniteLevel, level + 1) : current.infiniteLevel, infiniteWins: current.infiniteWins + (mode === 'infinite' && won ? 1 : 0), infiniteLosses: current.infiniteLosses + (mode === 'infinite' && !won ? 1 : 0), }; }); }, [setStats], ); const submitGuess = useCallback(() => { if (session.status !== 'playing') return; const guess = normalizeGuess(session.currentGuess); if (guess.length !== session.target.length) { setShake(true); setSession((current) => ({ ...current, message: `Enter ${session.target.length} letters.` })); return; } if (!isValidGuess(guess, session.target.length)) { setShake(true); setSession((current) => ({ ...current, message: 'That word is not in the word list.' })); return; } const guesses = [...session.guesses, guess]; const status: GameStatus = guess === session.target ? 'won' : guesses.length >= maxGuesses ? 'lost' : 'playing'; updateStatsForResult(status, session.mode, session.level); setSession({ ...session, guesses, currentGuess: '', status, message: status === 'won' ? 'Result recorded.' : status === 'lost' ? 'No guesses remain.' : session.mode === 'evil-daily' && guesses.length === 2 ? 'Memory phase begins now.' : '', }); }, [maxGuesses, session, updateStatsForResult]); const handleKey = useCallback( (key: string) => { if (session.status !== 'playing') return; if (key === 'Enter') { submitGuess(); return; } if (key === 'Backspace' || key === 'Delete') { setSession((current) => ({ ...current, currentGuess: current.currentGuess.slice(0, -1), message: '' })); return; } const clean = normalizeGuess(key); if (clean.length !== 1) return; setSession((current) => { if (current.currentGuess.length >= current.target.length) return current; return { ...current, currentGuess: current.currentGuess + clean, message: '' }; }); }, [session.status, submitGuess], ); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.metaKey || event.ctrlKey || event.altKey) return; if (event.key === 'Enter' || event.key === 'Backspace' || /^[a-zA-Z]$/.test(event.key)) { event.preventDefault(); handleKey(event.key); } }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [handleKey]); const changeMode = (mode: GameMode) => startNewSession(mode); const changeSettings = (nextSettings: Settings) => { setSettings(nextSettings); if (session.mode === 'custom') { setSession(createSession('custom', nextSettings, 1, false)); } }; const restart = () => { if (session.mode === 'daily' && !session.isPractice) { startNewSession('daily', 1, true); return; } const nextLevel = session.mode === 'infinite' && session.status === 'won' ? session.level + 1 : session.level; startNewSession(session.mode, nextLevel, session.isPractice); }; const startPractice = () => startNewSession('daily', 1, true); const modeNote = session.mode === 'infinite' ? `Level ${session.level}. Current word length: ${wordLength}.` : session.mode === 'custom' ? `${wordLength} letter custom puzzle.` : session.mode === 'super-evil' ? `${wordLength} letters, ${maxGuesses} guesses.` : session.mode === 'evil-daily' ? evilMemoryPhase ? 'Keyboard and feedback are hidden.' : 'Two guesses before the memory phase.' : session.isPractice ? 'Practice round.' : `Daily seed: ${getDateKey()}.`; return (
{MODE_LABELS[session.mode]}

{modeNote}

{session.message}
); }