From b2cc21a35457badc293a2ff209fc8d660b754c89 Mon Sep 17 00:00:00 2001 From: Eduard Duran Date: Sun, 26 Apr 2026 13:50:55 +0200 Subject: [PATCH] Add secret auto-solver unlocked via Konami code Logical-only solver (naked + hidden singles, no backtracking) fills one cell every ~2-3s with a blink animation, so progress is only made when the deduction is certain. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/App.css | 16 +++++++ src/App.jsx | 75 +++++++++++++++++++++++++++++++ src/components/Board.jsx | 5 ++- src/components/Cell.jsx | 3 +- src/components/GameControls.jsx | 7 ++- src/hooks/useGame.js | 39 ++++++++++++++++ src/utils/sudoku.js | 80 +++++++++++++++++++++++++++++++++ 7 files changed, 222 insertions(+), 3 deletions(-) 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 (
@@ -73,6 +144,7 @@ function App() { highlightedNumber={state.highlightedNumber} showErrors={state.showErrors} celebrating={state.showEffects ? state.celebrating : new Set()} + autoBlinkCell={autoBlinkCell} selectCell={selectCell} deselect={deselect} /> @@ -92,6 +164,9 @@ function App() { canUndo={state.history.length > 0} canRedo={state.future.length > 0} onOpenSettings={() => setShowSettings(true)} + solveUnlocked={solveUnlocked} + autoSolving={autoSolving} + onToggleAutoSolve={() => setAutoSolving(s => !s)} /> diff --git a/src/components/Board.jsx b/src/components/Board.jsx index 348e4a5..db30183 100644 --- a/src/components/Board.jsx +++ b/src/components/Board.jsx @@ -2,7 +2,7 @@ import React from 'react' import Cell from './Cell' import { checkValue } from '../utils/sudoku' -function Board({ board, puzzle, solution, notes, selected, highlightedNumber, showErrors, celebrating, selectCell, deselect }) { +function Board({ board, puzzle, solution, notes, selected, highlightedNumber, showErrors, celebrating, autoBlinkCell, selectCell, deselect }) { const selectedValue = selected && board[selected.row][selected.col] const activeNumber = selected ? selectedValue : highlightedNumber @@ -31,6 +31,8 @@ function Board({ board, puzzle, solution, notes, selected, highlightedNumber, sh value !== 0 && !checkValue(solution, r, c, value) const isCelebrating = celebrating.has(`${r},${c}`) + const isAutoBlinking = + autoBlinkCell && autoBlinkCell.row === r && autoBlinkCell.col === c return ( e.ctrlKey || e.metaKey ? deselect() : selectCell(r, c)} /> ) diff --git a/src/components/Cell.jsx b/src/components/Cell.jsx index b2f051b..428c561 100644 --- a/src/components/Cell.jsx +++ b/src/components/Cell.jsx @@ -1,6 +1,6 @@ import React from 'react' -function Cell({ value, notes, isGiven, isSelected, isHighlightedRow, isHighlightedCol, isHighlightedBox, isSameNumber, isError, isCelebrating, onClick }) { +function Cell({ value, notes, isGiven, isSelected, isHighlightedRow, isHighlightedCol, isHighlightedBox, isSameNumber, isError, isCelebrating, isAutoBlinking, onClick }) { const classes = ['cell'] if (isGiven) classes.push('given') if (isSelected) classes.push('selected') @@ -10,6 +10,7 @@ function Cell({ value, notes, isGiven, isSelected, isHighlightedRow, isHighlight if (isSameNumber) classes.push('same-number') if (isError) classes.push('error') if (isCelebrating) classes.push('celebrating') + if (isAutoBlinking) classes.push('auto-blinking') return (
diff --git a/src/components/GameControls.jsx b/src/components/GameControls.jsx index 979a2d1..4bc812b 100644 --- a/src/components/GameControls.jsx +++ b/src/components/GameControls.jsx @@ -8,7 +8,7 @@ const DIFFICULTIES = [ { value: 'expert', label: 'Expert' }, ] -function GameControls({ currentDifficulty, onNewGame, onClear, onUndo, onRedo, canUndo, canRedo, onOpenSettings }) { +function GameControls({ currentDifficulty, onNewGame, onClear, onUndo, onRedo, canUndo, canRedo, onOpenSettings, solveUnlocked, autoSolving, onToggleAutoSolve }) { const [difficulty, setDifficulty] = useState(currentDifficulty) const [showConfirm, setShowConfirm] = useState(false) @@ -32,6 +32,11 @@ function GameControls({ currentDifficulty, onNewGame, onClear, onUndo, onRedo, c + {solveUnlocked && ( + + )}