diff --git a/src/App.css b/src/App.css index cafb124..5e9b290 100644 --- a/src/App.css +++ b/src/App.css @@ -201,6 +201,22 @@ body { } } +/* Auto-solve blink animation */ +.cell.auto-blinking .cell-value { + animation: autoFillBlink 0.4s ease-in-out infinite; +} + +@keyframes autoFillBlink { + 0%, 100% { + opacity: 1; + text-shadow: 0 0 10px rgba(255, 152, 0, 0.7); + } + 50% { + opacity: 0.25; + text-shadow: 0 0 0 transparent; + } +} + @keyframes celebrateText { 0% { transform: scale(1); diff --git a/src/App.jsx b/src/App.jsx index fd53d49..3ee73f7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ import WinModal from './components/WinModal' import Confetti from './components/Confetti' import SettingsModal from './components/SettingsModal' import { useGame } from './hooks/useGame' +import { findCertainMove } from './utils/sudoku' function App() { const { @@ -23,6 +24,7 @@ function App() { toggleDarkMode, undo, redo, + autoFill, } = useGame() const [showSettings, setShowSettings] = useState(false) @@ -30,6 +32,10 @@ function App() { const saved = parseInt(localStorage.getItem('sudoku-hue'), 10) return isNaN(saved) ? 211 : saved }) + const [solveUnlocked, setSolveUnlocked] = useState(false) + const [autoSolving, setAutoSolving] = useState(false) + const [autoBlinkCell, setAutoBlinkCell] = useState(null) + const [autoCooldown, setAutoCooldown] = useState(false) useEffect(() => { document.body.style.background = state.darkMode ? '#1a1a2e' : '#f8f9fa' @@ -50,6 +56,71 @@ function App() { return () => window.removeEventListener('keydown', handleKeyDown) }, []) + useEffect(() => { + if (solveUnlocked) return + const sequence = [ + 'ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', + 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', + 'b', 'a', + ] + let progress = 0 + function onKeyDown(e) { + const expected = sequence[progress] + const key = expected.length === 1 ? e.key.toLowerCase() : e.key + if (key === expected) { + progress++ + if (progress === sequence.length) { + setSolveUnlocked(true) + } + } else { + progress = key === sequence[0] ? 1 : 0 + } + } + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, [solveUnlocked]) + + useEffect(() => { + if (!autoSolving) { + setAutoBlinkCell(null) + setAutoCooldown(false) + } + }, [autoSolving]) + + useEffect(() => { + if (!autoSolving) return + if (state.isComplete) { + setAutoSolving(false) + return + } + if (autoBlinkCell || autoCooldown) return + + const move = findCertainMove(state.board) + if (!move) { + setAutoSolving(false) + return + } + autoFill(move.row, move.col, move.value) + setAutoBlinkCell({ row: move.row, col: move.col }) + }, [autoSolving, state.board, state.isComplete, autoBlinkCell, autoCooldown, autoFill]) + + useEffect(() => { + if (!autoBlinkCell) return + const blinkMs = 1000 + Math.random() * 1000 + const t = setTimeout(() => { + setAutoBlinkCell(null) + setAutoCooldown(true) + }, blinkMs) + return () => clearTimeout(t) + }, [autoBlinkCell]) + + useEffect(() => { + if (!autoCooldown) return + const cooldownMs = 1000 + Math.random() * 1000 + const t = setTimeout(() => setAutoCooldown(false), cooldownMs) + return () => clearTimeout(t) + }, [autoCooldown]) + return (