Skip to content

Commit

Permalink
refactor sudoku code to get buttons looking right and working, implem…
Browse files Browse the repository at this point in the history
…ent save/load
  • Loading branch information
ayan4m1 committed Dec 25, 2023
1 parent 87e168a commit f8e0283
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 125 deletions.
18 changes: 17 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
"react-snowfall": "^1.2.1",
"sass": "^1.69.5",
"sharp": "^0.33.0",
"sudoku-gen": "^1.0.2"
"sudoku-gen": "^1.0.2",
"use-local-storage-state": "^19.1.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.23.3",
Expand Down
163 changes: 75 additions & 88 deletions src/components/sudokuBoard.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,44 @@
import PropTypes from 'prop-types';
import { chunk } from 'lodash-es';
import { Alert, Card, Container, Row } from 'react-bootstrap';
import { getSudoku } from 'sudoku-gen';
import useLocalStorageState from 'use-local-storage-state';
import {
Alert,
Card,
Container,
Row,
Col,
ButtonGroup,
Button
} from 'react-bootstrap';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faRecycle,
faFloppyDisk,
faFolderOpen,
faTrash
} from '@fortawesome/free-solid-svg-icons';

import SudokuCell from 'components/sudokuCell';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { checkSolution, getInvalids } from 'utils/sudoku';

export default function SudokuBoard({ puzzle, solution }) {
export default function SudokuBoard() {
const [solved, setSolved] = useState(false);
const [activeCell, setActiveCell] = useState([-1, -1]);
const [puzzle, setPuzzle] = useState(getSudoku('easy'));
const [savedState, setSavedState] = useLocalStorageState('savedState', {
defaultValue: null
});
const rows = useMemo(
() =>
chunk(puzzle.split(''), 9).map((row) =>
chunk(puzzle.puzzle.split(''), 9).map((row) =>
row.map((value) => (value === '-' ? null : parseInt(value, 10)))
),
[puzzle]
);
const [values, setValues] = useState(Array(9).fill(Array(9).fill(-1)));
const invalids = useMemo(
() =>
values.map((row, rowIdx) =>
row.map((value, colIdx) => {
if (value === -1) {
return true;
}

const targetRow = rows[rowIdx];

if (
((targetRow &&
targetRow.indexOf(value) === colIdx &&
targetRow.lastIndexOf(value) === colIdx) ||
(targetRow.indexOf(value) === -1 &&
targetRow.lastIndexOf(value) === -1) ||
row.indexOf(value) !== colIdx ||
row.lastIndexOf(value) !== colIdx) &&
rows.every((searchRow) => searchRow[colIdx] !== value)
) {
const cellRow = Math.floor(rowIdx / 3);
const cellCol = Math.floor(colIdx / 3);

for (
let searchRowIdx = cellRow * 3;
searchRowIdx < (cellRow + 1) * 3;
searchRowIdx++
) {
for (
let searchColIdx = cellCol * 3;
searchColIdx < (cellCol + 1) * 3;
searchColIdx++
) {
if (
searchRowIdx !== rowIdx &&
searchColIdx !== colIdx &&
(rows[searchRowIdx][searchColIdx] === value ||
values[searchRowIdx][searchColIdx] === value)
) {
return false;
}
}
}

return true;
} else {
return false;
}
})
),
[rows, values]
);
const invalids = useMemo(() => getInvalids(values, rows), [values, rows]);
const handleClick = useCallback(
(row, column) =>
setActiveCell(([prevRow, prevCol]) => {
Expand All @@ -79,48 +50,64 @@ export default function SudokuBoard({ puzzle, solution }) {
}),
[]
);
const handleChange = useCallback((row, column, value) => {
setValues((prevVal) => {
const newVal = [...prevVal];
const newRow = [...newVal[row]];
const handleChange = useCallback(
(row, column, value) =>
setValues((prevVal) => {
const newVal = [...prevVal];
const newRow = [...newVal[row]];

newRow[column] = value;
newVal.splice(row, 1, newRow);
newRow[column] = value;
newVal.splice(row, 1, newRow);

return newVal;
});
}, []);

useEffect(() => {
if (!rows.length) {
return newVal;
}),
[]
);
const handleNew = useCallback(() => setPuzzle(getSudoku('easy')), []);
const handleSave = useCallback(
() =>
setSavedState({
puzzle,
values
}),
[puzzle, values]
);
const handleLoad = useCallback(() => {
if (!savedState) {
return;
}

const currentBoard = rows
.map((boardRow, rowIdx) =>
boardRow
.map((value, colIdx) => {
if (value !== null) {
return value.toString();
}

const liveValue = values[rowIdx][colIdx];

return liveValue === -1 ? '-' : liveValue;
})
.join('')
)
.join('');
setPuzzle(savedState.puzzle);
setValues(savedState.values);
}, [savedState]);
const handleClear = useCallback(() => setSavedState(null), []);

if (currentBoard === solution) {
setSolved(true);
}
}, [rows, values]);
useEffect(() => {
setSolved(checkSolution(rows, values, puzzle.solution));
}, [rows, values, puzzle]);

return (
<Card body>
{solved && <Alert variant="success">You did it!</Alert>}
{solved && <Alert variant="success">You solved it!</Alert>}
<Container fluid>
<Row className="mb-2">
<Col className="d-flex justify-content-center g-0">
<ButtonGroup className="w-100">
<Button onClick={handleNew}>
<FontAwesomeIcon icon={faRecycle} /> New
</Button>
<Button onClick={handleSave}>
<FontAwesomeIcon icon={faFloppyDisk} /> Save
</Button>
<Button onClick={handleClear} disabled={!savedState}>
<FontAwesomeIcon icon={faTrash} /> Clear
</Button>
<Button onClick={handleLoad} disabled={!savedState}>
<FontAwesomeIcon icon={faFolderOpen} /> Load
</Button>
</ButtonGroup>
</Col>
</Row>
{rows.map((row, rowIdx) => (
<Row key={rowIdx}>
{row.map((value, colIdx) => (
Expand Down
37 changes: 2 additions & 35 deletions src/pages/sudoku.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
import { useState } from 'react';
import { Container, Row, Col, Button, ButtonGroup } from 'react-bootstrap';
import { getSudoku } from 'sudoku-gen';
import { Container, Row, Col } 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 {
faCheckCircle,
faFloppyDisk,
faFolderOpen,
faQuestionCircle,
faRecycle
} from '@fortawesome/free-solid-svg-icons';

export default function SudokuPage() {
const [puzzle, setPuzzle] = useState(getSudoku('easy'));

return (
<Layout>
<SEO title="Sudoku" />
Expand All @@ -26,30 +14,9 @@ export default function SudokuPage() {
<h1>Sudoku</h1>
</Col>
</Row>
<Row className="mb-2">
<Col xs={12}>
<ButtonGroup>
<Button onClick={() => setPuzzle(getSudoku('easy'))}>
<FontAwesomeIcon icon={faRecycle} /> New Board
</Button>
<Button disabled>
<FontAwesomeIcon icon={faFloppyDisk} /> Save
</Button>
<Button disabled>
<FontAwesomeIcon icon={faFolderOpen} /> Load
</Button>
<Button disabled>
<FontAwesomeIcon icon={faQuestionCircle} /> Get Hint
</Button>
<Button variant="success">
<FontAwesomeIcon icon={faCheckCircle} /> Check Answer
</Button>
</ButtonGroup>
</Col>
</Row>
<Row>
<Col className="d-flex justify-content-center">
<SudokuBoard {...puzzle} />
<SudokuBoard />
</Col>
</Row>
</Container>
Expand Down
74 changes: 74 additions & 0 deletions src/utils/sudoku.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
export function checkSolution(puzzle, values, solution) {
if (!puzzle.length) {
return false;
}

const currentBoard = puzzle
.map((boardRow, rowIdx) =>
boardRow
.map((value, colIdx) => {
if (value !== null) {
return value.toString();
}

const liveValue = values[rowIdx][colIdx];

return liveValue === -1 ? '-' : liveValue;
})
.join('')
)
.join('');

return currentBoard === solution;
}

export function getInvalids(values, rows) {
return values.map((row, rowIdx) =>
row.map((value, colIdx) => {
if (value === -1) {
return true;
}

const targetRow = rows[rowIdx];

if (
((targetRow &&
targetRow.indexOf(value) === colIdx &&
targetRow.lastIndexOf(value) === colIdx) ||
(targetRow.indexOf(value) === -1 &&
targetRow.lastIndexOf(value) === -1) ||
row.indexOf(value) !== colIdx ||
row.lastIndexOf(value) !== colIdx) &&
rows.every((searchRow) => searchRow[colIdx] !== value)
) {
const cellRow = Math.floor(rowIdx / 3);
const cellCol = Math.floor(colIdx / 3);

for (
let searchRowIdx = cellRow * 3;
searchRowIdx < (cellRow + 1) * 3;
searchRowIdx++
) {
for (
let searchColIdx = cellCol * 3;
searchColIdx < (cellCol + 1) * 3;
searchColIdx++
) {
if (
searchRowIdx !== rowIdx &&
searchColIdx !== colIdx &&
(rows[searchRowIdx][searchColIdx] === value ||
values[searchRowIdx][searchColIdx] === value)
) {
return false;
}
}
}

return true;
} else {
return false;
}
})
);
}

0 comments on commit f8e0283

Please sign in to comment.