Skip to content

Commit

Permalink
add WIP sudoku page
Browse files Browse the repository at this point in the history
  • Loading branch information
ayan4m1 committed Dec 25, 2023
1 parent 0cfbce6 commit 6f59736
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 2 deletions.
9 changes: 8 additions & 1 deletion package-lock.json

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

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"babel-plugin-prismjs": "^2.1.0",
"bootstrap": "^5.3.2",
"bootswatch": "^5.3.2",
"classnames": "^2.3.2",
"d3-color": "^3.1.0",
"d3-interpolate": "^3.0.1",
"date-fns": "^2.30.0",
Expand Down Expand Up @@ -67,7 +68,8 @@
"react-markdown": "^8.0.7",
"react-snowfall": "^1.2.1",
"sass": "^1.69.5",
"sharp": "^0.33.0"
"sharp": "^0.33.0",
"sudoku-gen": "^1.0.2"
},
"devDependencies": {
"@babel/eslint-parser": "^7.23.3",
Expand Down
149 changes: 149 additions & 0 deletions src/components/sudokuBoard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import PropTypes from 'prop-types';
import { chunk } from 'lodash-es';
import { Alert, Card, Container, Row } from 'react-bootstrap';

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

export default function SudokuBoard({ puzzle, solution }) {
const [solved, setSolved] = useState(false);
const [activeCell, setActiveCell] = useState([-1, -1]);
const rows = useMemo(
() =>
chunk(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 handleClick = useCallback(
(row, column) =>
setActiveCell(([prevRow, prevCol]) => {
if (prevRow === row && prevCol === column) {
return [-1, -1];
} else {
return [row, column];
}
}),
[]
);
const handleChange = useCallback((row, column, value) => {
setValues((prevVal) => {
const newVal = [...prevVal];
const newRow = [...newVal[row]];

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

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

useEffect(() => {
if (!rows.length) {
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('');

if (currentBoard === solution) {
setSolved(true);
}
}, [rows, values]);

return (
<Card body>
{solved && <Alert variant="success">You did it!</Alert>}
<Container fluid>
{rows.map((row, rowIdx) => (
<Row key={rowIdx}>
{row.map((value, colIdx) => (
<SudokuCell
row={rowIdx}
column={colIdx}
key={colIdx}
value={!value ? values[rowIdx][colIdx] : value}
unknown={!value}
active={activeCell[0] === rowIdx && activeCell[1] === colIdx}
valid={invalids[rowIdx][colIdx]}
onClick={handleClick}
onChange={handleChange}
/>
))}
</Row>
))}
</Container>
</Card>
);
}

SudokuBoard.propTypes = {
puzzle: PropTypes.string.isRequired,
solution: PropTypes.string.isRequired
};
98 changes: 98 additions & 0 deletions src/components/sudokuCell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Col, Form } from 'react-bootstrap';
import { useCallback, useEffect, useRef } from 'react';

export default function SudokuCell({
row,
column,
value,
unknown = false,
active = false,
valid = true,
onClick,
onChange
}) {
const textRef = useRef(null);
const handleChange = useCallback(
(event) => {
const newVal = parseInt(event.target.value, 10);

if (isNaN(newVal) || newVal < 1 || newVal > 9) {
return;
}

onChange(row, column, newVal);
onClick(-1, -1);
},
[onClick, onChange]
);
const handleKeyDown = useCallback(
({ key }) => {
if (!active || !['Enter', 'Tab'].includes(key)) {
return;
}

event.preventDefault();
onClick(-1, -1);
},
[active, onClick]
);
const handleBlur = useCallback(() => onClick(-1, -1), [onClick]);

useEffect(() => {
if (active && textRef.current) {
textRef.current.focus();
}
}, [active]);

const displayValue = value === -1 ? '' : value;
const text = unknown ? displayValue : <strong>{displayValue}</strong>;

return (
<Col
onClick={() => {
if (onClick) {
onClick(row, column);
}
}}
className={classNames(
'd-flex',
'justify-content-center',
'align-items-center',
'text-dark',
'p-0',
'border-2',
'border-dark',
column > 0 && column % 3 === 2 && 'border-end',
row > 0 && row % 3 === 2 && 'border-bottom',
valid ? 'bg-white' : 'bg-danger'
)}
style={{ height: 48, maxWidth: 48, minWidth: 48 }}
>
{unknown && active ? (
<Form.Control
ref={textRef}
type="text"
className={classNames('bg-warning', 'p-1', 'text-center', 'h-100')}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
/>
) : (
text
)}
</Col>
);
}

SudokuCell.propTypes = {
row: PropTypes.number.isRequired,
column: PropTypes.number.isRequired,
value: PropTypes.number,
unknown: PropTypes.bool,
active: PropTypes.bool,
valid: PropTypes.bool,
onClick: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired
};
58 changes: 58 additions & 0 deletions src/pages/sudoku.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState } from 'react';
import { Container, Row, Col, Button, ButtonGroup } from 'react-bootstrap';
import { getSudoku } from 'sudoku-gen';

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" />
<Container>
<Row>
<Col xs={12}>
<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} />
</Col>
</Row>
</Container>
</Layout>
);
}

0 comments on commit 6f59736

Please sign in to comment.