From 5c6156092999e1a2dd7fff86007f41318363baae Mon Sep 17 00:00:00 2001 From: ayan4m1 Date: Thu, 28 Dec 2023 00:08:15 -0500 Subject: [PATCH] add timed mode to sudoku --- src/components/sudokuBoard.js | 121 +++++++++++++++++++++++++++------- src/components/sudokuCell.js | 6 +- src/pages/sudoku.js | 30 ++++++++- src/utils/sudoku.js | 30 +++++++++ 4 files changed, 159 insertions(+), 28 deletions(-) diff --git a/src/components/sudokuBoard.js b/src/components/sudokuBoard.js index aefa384..cf97530 100644 --- a/src/components/sudokuBoard.js +++ b/src/components/sudokuBoard.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import { hsl } from 'd3-color'; import { chunk } from 'lodash-es'; import { getSudoku } from 'sudoku-gen'; +import { differenceInSeconds } from 'date-fns'; import useLocalStorageState from 'use-local-storage-state'; import { Alert, @@ -14,21 +15,41 @@ import { Dropdown, DropdownButton } from 'react-bootstrap'; -import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; +import { + Fragment, + useCallback, + useEffect, + useMemo, + useRef, + useState +} from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faRecycle, faFloppyDisk, faFolderOpen, - faTrash + faTrash, + faPause, + faPlay } from '@fortawesome/free-solid-svg-icons'; import SudokuCell from 'components/sudokuCell'; -import { checkSolution, getInvalids, difficulties } from 'utils/sudoku'; +import { + formatTime, + checkSolution, + getInvalids, + getInvalidArray, + difficulties +} from 'utils/sudoku'; import useRainbow from 'hooks/useRainbow'; -export default function SudokuBoard() { +export default function SudokuBoard({ mode }) { + const intervalRef = useRef(null); + const startTime = useMemo(() => Date.now(), []); + const [currentTime, setCurrentTime] = useState(Date.now()); + const [paused, setPaused] = useState(false); const [solved, setSolved] = useState(false); + const [solveRate, setSolveRate] = useState(null); const [activeCell, setActiveCell] = useState([-1, -1]); const [puzzle, setPuzzle] = useState(getSudoku('easy')); const [savedState, setSavedState] = useLocalStorageState('savedState', { @@ -41,7 +62,7 @@ export default function SudokuBoard() { ), [puzzle] ); - const [values, setValues] = useState(Array(9).fill(Array(9).fill(-1))); + const [values, setValues] = useState(getInvalidArray()); const invalids = useMemo(() => getInvalids(values, cells), [values, cells]); const handleClick = useCallback( (row, column) => @@ -67,17 +88,19 @@ export default function SudokuBoard() { }), [] ); - const handleNew = useCallback( - (difficulty) => setPuzzle(getSudoku(difficulty)), - [] - ); + const handleNew = useCallback((difficulty) => { + setCurrentTime(Date.now()); + setPuzzle(getSudoku(difficulty)); + setValues(getInvalidArray()); + }, []); const handleSave = useCallback( () => setSavedState({ puzzle, - values + values, + elapsedTime: differenceInSeconds(currentTime, startTime) }), - [puzzle, values] + [puzzle, values, currentTime, startTime] ); const handleLoad = useCallback(() => { if (!savedState) { @@ -86,8 +109,14 @@ export default function SudokuBoard() { setPuzzle(savedState.puzzle); setValues(savedState.values); + setCurrentTime(startTime + savedState.elapsedTime * 1000); }, [savedState]); const handleClear = useCallback(() => setSavedState(null), []); + const handlePause = useCallback(() => setPaused((prevVal) => !prevVal), []); + const handleDocumentVisibilityChange = useCallback( + () => setPaused(document.hidden), + [] + ); const { color: animationColor, start, stop } = useRainbow(false, false); useEffect(() => { @@ -95,19 +124,67 @@ export default function SudokuBoard() { if (solved) { setSolved(solved); + setSolveRate( + differenceInSeconds(currentTime, startTime) / + cells.reduce( + (result, row) => + result + + row.reduce( + (rowResult, cell) => rowResult + (cell === null ? 1 : 0), + 0 + ), + 0 + ) + ); start(); } else { stop(); } }, [cells, values, puzzle]); + useEffect(() => { + if (!paused) { + intervalRef.current = setInterval( + () => setCurrentTime((prevVal) => prevVal + 1000), + 1000 + ); + + return () => clearInterval(intervalRef.current); + } else if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }, [paused]); + + useEffect(() => { + // if we remove visibilitychange on unmount we lose it, so only set it up once + document.addEventListener( + 'visibilitychange', + handleDocumentVisibilityChange + ); + }); + return ( - +

Sudoku

+ {mode === 'timed' && ( + + + {formatTime(currentTime, startTime)} + + + + )}
@@ -141,15 +218,16 @@ export default function SudokuBoard() { - - - {solved && ( + {solved && ( + + - You solved it! + You solved it, averaging {solveRate.toFixed(1)} seconds per + cell! - )} - - + + + )} {cells.map((row, rowIdx) => ( {row.map((value, colIdx) => { @@ -162,9 +240,9 @@ export default function SudokuBoard() { return ( { if (active && ['Enter', 'Tab'].includes(key)) { event.preventDefault(); - onClick(-1, -1); + handleBlur(); } else if (active && ['Backspace', 'Delete'].includes(key)) { event.preventDefault(); onChange(row, column, -1); - onClick(-1, -1); + handleBlur(); } }, [active, onClick] diff --git a/src/pages/sudoku.js b/src/pages/sudoku.js index 214592e..ebad4a3 100644 --- a/src/pages/sudoku.js +++ b/src/pages/sudoku.js @@ -1,17 +1,41 @@ -import { Container, Row, Col } from 'react-bootstrap'; +import { useState } from 'react'; +import { Container, Row, Col, ButtonGroup, Button } from 'react-bootstrap'; import Layout from 'components/layout'; import SEO from 'components/seo'; import SudokuBoard from 'components/sudokuBoard'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faClock, faDoorOpen } from '@fortawesome/free-solid-svg-icons'; export default function SudokuPage() { + const [gameMode, setGameMode] = useState(null); + return ( - + - + {gameMode ? ( + + ) : ( + + + + + )} diff --git a/src/utils/sudoku.js b/src/utils/sudoku.js index d36c31e..07857cb 100644 --- a/src/utils/sudoku.js +++ b/src/utils/sudoku.js @@ -1,5 +1,35 @@ +import { + differenceInHours, + differenceInMinutes, + differenceInSeconds, + subHours, + subMinutes +} from 'date-fns'; + export const difficulties = ['Easy', 'Medium', 'Hard', 'Expert']; +function padTime(input) { + return input.toString().padStart(2, '0'); +} + +export function getInvalidArray() { + return Array(9).fill(Array(9).fill(-1)); +} + +export function formatTime(start, current) { + const hours = differenceInHours(start, current); + + start = subHours(start, hours); + + const minutes = differenceInMinutes(start, current); + + start = subMinutes(start, minutes); + + const seconds = differenceInSeconds(start, current); + + return `${padTime(hours)}:${padTime(minutes)}:${padTime(seconds)}`; +} + export function checkSolution(puzzle, values, solution) { if (!puzzle.length) { return false;