mirror of
https://github.com/tiennm99/sudoku-solver.git
synced 2026-10-11 03:13:52 +00:00
feat(js): rewrite in JavaScript as a browser app
This commit is contained in:
1 parent
c6ad7ad414
commit
3643f75f30
18 files changed
+3575
-620
No files matched your search
@@ -0,0 +1,49 @@
|
|||||||
|
name: Deploy to GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run lint
|
||||||
|
- run: npm test
|
||||||
|
|
||||||
|
build-deploy:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
- uses: actions/configure-pages@v6
|
||||||
|
- uses: actions/upload-pages-artifact@v5
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
- id: deployment
|
||||||
|
uses: actions/deploy-pages@v5
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.vite
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.claude
|
||||||
@@ -1,43 +1,82 @@
|
|||||||
# sudoku-solver
|
# sudoku-solver
|
||||||
|
|
||||||
9×9 Sudoku solver in PHP using recursive backtracking — runs from the browser or CLI, prints solution to stdout.
|
Solves a 9×9 Sudoku by recursive backtracking, entirely in the browser.
|
||||||
|
|
||||||
## Algorithm
|
Live: <https://tiennm99.github.io/sudoku-solver/>
|
||||||
|
|
||||||
Recursive backtracking: iterates through empty cells, tries digits 1–9, and backtracks on constraint violations (row, column, and 3×3 box checks).
|
*Originally written in PHP. In 2026 it was rewritten in JavaScript; the PHP
|
||||||
|
version is on the [feature/php](https://github.com/tiennm99/sudoku-solver/tree/feature/php)
|
||||||
|
branch.*
|
||||||
|
|
||||||
## Input format
|
## Running it
|
||||||
|
|
||||||
Edit `sudoku.php` to define the puzzle as an 81-character string — digits 1–9 for known cells, `0` for empty cells:
|
```sh
|
||||||
|
npm install
|
||||||
```php
|
npm run dev # http://localhost:8080
|
||||||
$puzzle = new SudokuSolver("530070000600195000098000060800060003400803001700020006060000280000419005000080079");
|
|
||||||
$puzzle->solve();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example
|
```sh
|
||||||
|
npm run build # static site into dist/
|
||||||
Input:
|
npm run preview
|
||||||
```
|
|
||||||
5 3 0 | 0 7 0 | 0 0 0
|
|
||||||
6 0 0 | 1 9 5 | 0 0 0
|
|
||||||
0 9 8 | 0 0 0 | 0 6 0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Output (stdout):
|
## How it works
|
||||||
|
|
||||||
|
The solver walks the grid cell by cell. On a blank it tries 1–9, keeps any digit
|
||||||
|
no peer already holds, and recurses; if the rest of the grid cannot be completed
|
||||||
|
it puts the cell back and tries the next digit. A cell's *peers* are the other
|
||||||
|
cells in its row, column, and 3×3 box — one definition in
|
||||||
|
[`src/lib/sudoku/grid.js`](src/lib/sudoku/grid.js) that both the solver and the
|
||||||
|
conflict highlighter read, so the two can never disagree about what a clash is.
|
||||||
|
|
||||||
|
### The givens are checked before the search starts
|
||||||
|
|
||||||
|
Backtracking only validates digits it places itself; it skips over cells that
|
||||||
|
were already filled. So a puzzle whose givens contradict each other — the same
|
||||||
|
digit twice in one row, say — could otherwise come back reported as *solved*,
|
||||||
|
with the contradiction still sitting in it. `solve()` therefore rejects a grid
|
||||||
|
that already has conflicts rather than searching it. The PHP version had this
|
||||||
|
hole; there is a test pinning it shut.
|
||||||
|
|
||||||
|
## Puzzle text format
|
||||||
|
|
||||||
|
The grid round-trips through the 81-character form the original command-line
|
||||||
|
version read: one character per cell, row by row, `0` for a blank. It is under
|
||||||
|
"Puzzle as text" in the UI.
|
||||||
|
|
||||||
```
|
```
|
||||||
5 3 4 | 6 7 8 | 9 1 2
|
530070000600195000098000060800060003400803001700020006060000280000419005000080079
|
||||||
6 7 2 | 1 9 5 | 3 4 8
|
|
||||||
1 9 8 | 3 4 2 | 5 6 7
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Tests
|
||||||
|
|
||||||
```bash
|
```sh
|
||||||
php sudoku.php
|
npm test
|
||||||
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
Or open `sudoku.php` in a browser for the interactive grid UI.
|
21 tests cover parsing and formatting, peer computation, conflict detection, and
|
||||||
|
the solver — including that it leaves the caller's grid untouched, preserves
|
||||||
|
every given, refuses contradictory givens, and returns `null` when a puzzle
|
||||||
|
genuinely has no completion.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/lib/sudoku/ grid model and solver — no DOM, directly testable
|
||||||
|
src/views/ Svelte components
|
||||||
|
vite/ dev and production configs
|
||||||
|
```
|
||||||
|
|
||||||
|
Plain JavaScript with JSDoc types, Svelte 5, Vite. Deployed to GitHub Pages by
|
||||||
|
[`.github/workflows/deploy.yml`](.github/workflows/deploy.yml) on every push to
|
||||||
|
`main`, gated on lint and tests.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
The backtracking approach follows the worked examples the PHP version cited:
|
||||||
|
[Rosetta Code's Sudoku task](https://rosettacode.org/wiki/Sudoku) and
|
||||||
|
[a gist by thebinarypenguin](https://gist.github.com/thebinarypenguin/4d45ffe87096e508800b5d11544bf2fa).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ ignores: ['dist/**', 'node_modules/**'] },
|
||||||
|
|
||||||
|
js.configs.recommended,
|
||||||
|
...svelte.configs['flat/recommended'],
|
||||||
|
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: { ...globals.browser }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
files: ['**/*.test.js'],
|
||||||
|
languageOptions: { globals: { ...globals.node } }
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
files: ['vite/**', 'eslint.config.js'],
|
||||||
|
languageOptions: { globals: { ...globals.node } }
|
||||||
|
}
|
||||||
|
];
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="./favicon.svg" />
|
||||||
|
<title>Sudoku Solver</title>
|
||||||
|
<meta name="description" content="Solve a 9x9 Sudoku in the browser by recursive backtracking." />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2661
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "sudoku-solver",
|
||||||
|
"description": "9x9 Sudoku solver in the browser using recursive backtracking (Svelte + Vite)",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tiennm99/sudoku-solver.git"
|
||||||
|
},
|
||||||
|
"author": "tiennm99",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"licenseUrl": "https://www.apache.org/licenses/LICENSE-2.0",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tiennm99/sudoku-solver/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://tiennm99.github.io/sudoku-solver/",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --config vite/config.dev.mjs",
|
||||||
|
"build": "vite build --config vite/config.prod.mjs",
|
||||||
|
"preview": "vite preview --config vite/config.prod.mjs",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
||||||
|
"eslint": "^10.9.1",
|
||||||
|
"eslint-plugin-svelte": "^3.23.0",
|
||||||
|
"globals": "^17.11.0",
|
||||||
|
"svelte": "^5.57.0",
|
||||||
|
"vite": "^8.2.2",
|
||||||
|
"vitest": "^4.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<rect width="32" height="32" rx="4" fill="#2563eb" />
|
||||||
|
<g stroke="#fff" stroke-width="2" stroke-linecap="round">
|
||||||
|
<path d="M4 14h24M4 20h24M14 4v24M20 4v24" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 236 B |
+49
@@ -0,0 +1,49 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
|
||||||
|
--page-bg: #fafafa;
|
||||||
|
--cell-bg: #ffffff;
|
||||||
|
--cell-hover: #f0f0f0;
|
||||||
|
--cell-conflict: #fde8e8;
|
||||||
|
--rule-strong: #1a1a1a;
|
||||||
|
--ink: #1a1a1a;
|
||||||
|
--ink-muted: #666666;
|
||||||
|
--ink-given: #1a1a1a;
|
||||||
|
--ink-solved: #2563eb;
|
||||||
|
--ink-conflict: #b91c1c;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--accent-ink: #ffffff;
|
||||||
|
--focus: #2563eb;
|
||||||
|
--ok: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--page-bg: #14161a;
|
||||||
|
--cell-bg: #1e2127;
|
||||||
|
--cell-hover: #272b33;
|
||||||
|
--cell-conflict: #4a1f1f;
|
||||||
|
--rule-strong: #8a8f98;
|
||||||
|
--ink: #e6e6e6;
|
||||||
|
--ink-muted: #9aa0a8;
|
||||||
|
--ink-given: #e6e6e6;
|
||||||
|
--ink-solved: #7aa2f7;
|
||||||
|
--ink-conflict: #f87171;
|
||||||
|
--accent: #3b82f6;
|
||||||
|
--accent-ink: #0b0d10;
|
||||||
|
--focus: #7aa2f7;
|
||||||
|
--ok: #4ade80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--page-bg);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/** Side length of the puzzle. */
|
||||||
|
export const SIZE = 9;
|
||||||
|
|
||||||
|
/** Side length of one box. */
|
||||||
|
export const BOX = 3;
|
||||||
|
|
||||||
|
/** Total number of cells. */
|
||||||
|
export const CELLS = SIZE * SIZE;
|
||||||
|
|
||||||
|
/** Value stored in a cell that has no digit yet. */
|
||||||
|
export const EMPTY = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A puzzle as a flat, row-major array of 81 digits, 0 meaning empty.
|
||||||
|
* @typedef {number[]} Grid
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an empty puzzle.
|
||||||
|
* @returns {Grid}
|
||||||
|
*/
|
||||||
|
export function emptyGrid() {
|
||||||
|
return new Array(CELLS).fill(EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the 81-character text form used by the original CLI: one character per
|
||||||
|
* cell, row-major, with `emptyChar` marking a blank.
|
||||||
|
*
|
||||||
|
* @param {string} text
|
||||||
|
* @param {string} [emptyChar] character standing for a blank cell
|
||||||
|
* @returns {Grid}
|
||||||
|
* @throws {Error} if the length is wrong or a character is not a digit or blank
|
||||||
|
*/
|
||||||
|
export function parseGrid(text, emptyChar = '0') {
|
||||||
|
const trimmed = text.replace(/\s/g, '');
|
||||||
|
if (trimmed.length !== CELLS) {
|
||||||
|
throw new Error(`expected ${CELLS} characters, got ${trimmed.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...trimmed].map((char, index) => {
|
||||||
|
if (char === emptyChar) return EMPTY;
|
||||||
|
if (char >= '1' && char <= '9') return Number(char);
|
||||||
|
throw new Error(`cell ${index} has invalid character ${JSON.stringify(char)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a puzzle back to its 81-character text form.
|
||||||
|
* @param {Grid} grid
|
||||||
|
* @param {string} [emptyChar]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatGrid(grid, emptyChar = '0') {
|
||||||
|
return grid.map((value) => (value === EMPTY ? emptyChar : String(value))).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Row index of a cell.
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function rowOf(index) {
|
||||||
|
return Math.floor(index / SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Column index of a cell.
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function columnOf(index) {
|
||||||
|
return index % SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indices sharing a row, column, or box with the given cell, excluding the cell
|
||||||
|
* itself. Every Sudoku constraint is "no peer repeats my value", so the solver
|
||||||
|
* and the conflict check share this one definition.
|
||||||
|
*
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
export function peersOf(index) {
|
||||||
|
const row = rowOf(index);
|
||||||
|
const column = columnOf(index);
|
||||||
|
const peers = new Set();
|
||||||
|
|
||||||
|
for (let i = 0; i < SIZE; i++) {
|
||||||
|
peers.add(row * SIZE + i);
|
||||||
|
peers.add(i * SIZE + column);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boxRow = Math.floor(row / BOX) * BOX;
|
||||||
|
const boxColumn = Math.floor(column / BOX) * BOX;
|
||||||
|
for (let r = boxRow; r < boxRow + BOX; r++) {
|
||||||
|
for (let c = boxColumn; c < boxColumn + BOX; c++) {
|
||||||
|
peers.add(r * SIZE + c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peers.delete(index);
|
||||||
|
return [...peers];
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { CELLS, EMPTY, SIZE, peersOf } from './grid.js';
|
||||||
|
|
||||||
|
/** @typedef {import('./grid.js').Grid} Grid */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether `value` may go in `index` without repeating in that cell's row,
|
||||||
|
* column, or box. The cell's own current value is ignored.
|
||||||
|
*
|
||||||
|
* @param {Grid} grid
|
||||||
|
* @param {number} index
|
||||||
|
* @param {number} value
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isValidPlacement(grid, index, value) {
|
||||||
|
return peersOf(index).every((peer) => grid[peer] !== value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indices of filled cells that repeat a value already held by a peer.
|
||||||
|
*
|
||||||
|
* Both offending cells are reported, since neither is more wrong than the
|
||||||
|
* other, and the UI highlights them as a pair.
|
||||||
|
*
|
||||||
|
* @param {Grid} grid
|
||||||
|
* @returns {Set<number>}
|
||||||
|
*/
|
||||||
|
export function findConflicts(grid) {
|
||||||
|
const conflicts = new Set();
|
||||||
|
|
||||||
|
for (let index = 0; index < CELLS; index++) {
|
||||||
|
const value = grid[index];
|
||||||
|
if (value === EMPTY) continue;
|
||||||
|
|
||||||
|
for (const peer of peersOf(index)) {
|
||||||
|
if (grid[peer] === value) {
|
||||||
|
conflicts.add(index);
|
||||||
|
conflicts.add(peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills every blank by recursive backtracking, leaving the caller's grid
|
||||||
|
* untouched.
|
||||||
|
*
|
||||||
|
* The givens are checked first. Backtracking only ever validates the digits it
|
||||||
|
* places, so a puzzle whose givens already contradict each other could
|
||||||
|
* otherwise come back "solved" with that contradiction still in it.
|
||||||
|
*
|
||||||
|
* @param {Grid} grid
|
||||||
|
* @returns {Grid|null} the completed grid, or null if it cannot be solved
|
||||||
|
*/
|
||||||
|
export function solve(grid) {
|
||||||
|
if (findConflicts(grid).size > 0) return null;
|
||||||
|
|
||||||
|
const work = [...grid];
|
||||||
|
return fill(work, 0) ? work : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Grid} grid mutated in place while searching
|
||||||
|
* @param {number} index first cell to consider
|
||||||
|
* @returns {boolean} whether the grid could be completed from here
|
||||||
|
*/
|
||||||
|
function fill(grid, index) {
|
||||||
|
if (index === CELLS) return true;
|
||||||
|
if (grid[index] !== EMPTY) return fill(grid, index + 1);
|
||||||
|
|
||||||
|
for (let value = 1; value <= SIZE; value++) {
|
||||||
|
if (isValidPlacement(grid, index, value)) {
|
||||||
|
grid[index] = value;
|
||||||
|
if (fill(grid, index + 1)) return true;
|
||||||
|
grid[index] = EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { CELLS, EMPTY, emptyGrid, formatGrid, parseGrid, peersOf } from './grid.js';
|
||||||
|
import { findConflicts, isValidPlacement, solve } from './solver.js';
|
||||||
|
|
||||||
|
/** The puzzle the original PHP README used as its worked example. */
|
||||||
|
const EXAMPLE = '530070000600195000098000060800060003400803001700020006060000280000419005000080079';
|
||||||
|
|
||||||
|
describe('parseGrid', () => {
|
||||||
|
it('reads digits and blanks row-major', () => {
|
||||||
|
const grid = parseGrid(EXAMPLE);
|
||||||
|
expect(grid).toHaveLength(CELLS);
|
||||||
|
expect(grid.slice(0, 9)).toEqual([5, 3, 0, 0, 7, 0, 0, 0, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores whitespace so pasted grids work', () => {
|
||||||
|
const spaced = `${EXAMPLE.slice(0, 9)}\n${EXAMPLE.slice(9)}`;
|
||||||
|
expect(parseGrid(spaced)).toEqual(parseGrid(EXAMPLE));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a custom blank character', () => {
|
||||||
|
expect(parseGrid('.'.repeat(CELLS), '.')).toEqual(emptyGrid());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects the wrong number of cells', () => {
|
||||||
|
expect(() => parseGrid('123')).toThrow(/expected 81/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects characters that are neither digit nor blank', () => {
|
||||||
|
expect(() => parseGrid(`x${EXAMPLE.slice(1)}`)).toThrow(/invalid character/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatGrid', () => {
|
||||||
|
it('round-trips through parseGrid', () => {
|
||||||
|
expect(formatGrid(parseGrid(EXAMPLE))).toBe(EXAMPLE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('peersOf', () => {
|
||||||
|
it('covers the row, column and box without the cell itself', () => {
|
||||||
|
const peers = peersOf(0);
|
||||||
|
// 8 in the row + 8 in the column + 4 more in the box.
|
||||||
|
expect(peers).toHaveLength(20);
|
||||||
|
expect(peers).not.toContain(0);
|
||||||
|
expect(peers).toContain(1); // same row
|
||||||
|
expect(peers).toContain(9); // same column
|
||||||
|
expect(peers).toContain(10); // same box
|
||||||
|
expect(peers).not.toContain(80); // opposite corner shares nothing
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isValidPlacement', () => {
|
||||||
|
const grid = parseGrid(EXAMPLE);
|
||||||
|
|
||||||
|
it('rejects a digit already in the row', () => {
|
||||||
|
expect(isValidPlacement(grid, 2, 5)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a digit already in the column', () => {
|
||||||
|
expect(isValidPlacement(grid, 9, 5)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a digit with no peer holding it', () => {
|
||||||
|
expect(isValidPlacement(grid, 2, 4)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findConflicts', () => {
|
||||||
|
it('finds none in a well-formed puzzle', () => {
|
||||||
|
expect(findConflicts(parseGrid(EXAMPLE)).size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports both cells of a repeated digit', () => {
|
||||||
|
const grid = emptyGrid();
|
||||||
|
grid[0] = 5;
|
||||||
|
grid[1] = 5;
|
||||||
|
expect(findConflicts(grid)).toEqual(new Set([0, 1]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sees a repeat inside a box that shares no row or column', () => {
|
||||||
|
const grid = emptyGrid();
|
||||||
|
grid[0] = 7;
|
||||||
|
grid[10] = 7;
|
||||||
|
expect(findConflicts(grid)).toEqual(new Set([0, 10]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores blanks', () => {
|
||||||
|
expect(findConflicts(emptyGrid()).size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('solve', () => {
|
||||||
|
it('solves the example puzzle', () => {
|
||||||
|
const solved = solve(parseGrid(EXAMPLE));
|
||||||
|
expect(solved).not.toBeNull();
|
||||||
|
expect(formatGrid(solved).slice(0, 9)).toBe('534678912');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a grid that is complete and self-consistent', () => {
|
||||||
|
const solved = solve(parseGrid(EXAMPLE));
|
||||||
|
expect(solved.every((value) => value >= 1 && value <= 9)).toBe(true);
|
||||||
|
expect(findConflicts(solved).size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps every given exactly where it was', () => {
|
||||||
|
const puzzle = parseGrid(EXAMPLE);
|
||||||
|
const solved = solve(puzzle);
|
||||||
|
puzzle.forEach((value, index) => {
|
||||||
|
if (value !== EMPTY) expect(solved[index]).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves the caller grid untouched', () => {
|
||||||
|
const puzzle = parseGrid(EXAMPLE);
|
||||||
|
solve(puzzle);
|
||||||
|
expect(formatGrid(puzzle)).toBe(EXAMPLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fills a blank grid', () => {
|
||||||
|
const solved = solve(emptyGrid());
|
||||||
|
expect(findConflicts(solved).size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backtracking validates only the digits it places, so contradictory givens
|
||||||
|
// would otherwise survive into a grid reported as solved.
|
||||||
|
it('refuses givens that already contradict each other', () => {
|
||||||
|
const grid = emptyGrid();
|
||||||
|
grid[0] = 5;
|
||||||
|
grid[1] = 5;
|
||||||
|
expect(solve(grid)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when no completion exists', () => {
|
||||||
|
// Row 0 holds 1-8 with a blank; the 9 that must go there is blocked by
|
||||||
|
// the 9 sitting in the same column.
|
||||||
|
const grid = parseGrid('123456780' + '000000009' + '0'.repeat(63));
|
||||||
|
expect(solve(grid)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
|
|
||||||
|
import './app.css';
|
||||||
|
import App from './views/App.svelte';
|
||||||
|
|
||||||
|
export default mount(App, { target: document.getElementById('app') });
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<script>
|
||||||
|
import { CELLS, EMPTY, emptyGrid, formatGrid, parseGrid } from '../lib/sudoku/grid.js';
|
||||||
|
import { findConflicts, solve } from '../lib/sudoku/solver.js';
|
||||||
|
import SudokuGrid from './SudokuGrid.svelte';
|
||||||
|
|
||||||
|
/** The puzzle the original PHP README used as its worked example. */
|
||||||
|
const EXAMPLE = '530070000600195000098000060800060003400803001700020006060000280000419005000080079';
|
||||||
|
|
||||||
|
let values = $state(emptyGrid());
|
||||||
|
/** Cells that were filled before solving, so the solution reads differently. */
|
||||||
|
let givens = $state(new Set());
|
||||||
|
let solved = $state(false);
|
||||||
|
let status = $state({ kind: 'idle', text: 'Type the puzzle, then solve.' });
|
||||||
|
|
||||||
|
const conflicts = $derived(solved ? new Set() : findConflicts(values));
|
||||||
|
const filledCount = $derived(values.filter((value) => value !== EMPTY).length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} index
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
|
function setCell(index, value) {
|
||||||
|
if (solved) return;
|
||||||
|
values[index] = value;
|
||||||
|
status = { kind: 'idle', text: 'Type the puzzle, then solve.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSolve() {
|
||||||
|
if (filledCount === 0) {
|
||||||
|
status = { kind: 'error', text: 'Enter at least one digit first.' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (conflicts.size > 0) {
|
||||||
|
status = { kind: 'error', text: 'Two highlighted cells repeat a digit — fix them first.' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const started = performance.now();
|
||||||
|
const result = solve(values);
|
||||||
|
const elapsed = Math.round(performance.now() - started);
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
status = { kind: 'error', text: 'This puzzle has no solution.' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
givens = new Set(values.flatMap((value, index) => (value === EMPTY ? [] : [index])));
|
||||||
|
values = result;
|
||||||
|
solved = true;
|
||||||
|
status = { kind: 'ok', text: `Solved in ${elapsed} ms.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
values = emptyGrid();
|
||||||
|
givens = new Set();
|
||||||
|
solved = false;
|
||||||
|
status = { kind: 'idle', text: 'Type the puzzle, then solve.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadExample() {
|
||||||
|
values = parseGrid(EXAMPLE);
|
||||||
|
givens = new Set();
|
||||||
|
solved = false;
|
||||||
|
status = { kind: 'idle', text: 'Example loaded — now solve it.' };
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>Sudoku Solver</h1>
|
||||||
|
<p class="lede">
|
||||||
|
Fill in the digits you know and the rest is worked out by recursive backtracking, entirely in
|
||||||
|
your browser.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<SudokuGrid {values} {givens} {conflicts} readonly={solved} onchange={setCell} />
|
||||||
|
|
||||||
|
<p class="status {status.kind}" role="status">{status.text}</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="primary" onclick={handleSolve} disabled={solved}>Solve</button>
|
||||||
|
<button onclick={loadExample}>Load example</button>
|
||||||
|
<button onclick={handleClear}>Clear</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="count">{filledCount} of {CELLS} cells filled</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Puzzle as text</summary>
|
||||||
|
<p class="hint">
|
||||||
|
81 characters, row by row, <code>0</code> for a blank — the format the original command-line version
|
||||||
|
read.
|
||||||
|
</p>
|
||||||
|
<code class="text-form">{formatGrid(values)}</code>
|
||||||
|
</details>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
max-width: 34rem;
|
||||||
|
margin-inline: auto;
|
||||||
|
padding: 2rem 1rem 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
font-size: clamp(1.5rem, 6vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lede {
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
min-height: 1.5em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.idle {
|
||||||
|
color: var(--ink-muted);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.ok {
|
||||||
|
color: var(--ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
color: var(--ink-conflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
border: 1px solid var(--rule-strong);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: var(--cell-bg);
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
background: var(--cell-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent-ink);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-form {
|
||||||
|
display: block;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
background: var(--cell-hover);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<script>
|
||||||
|
import { EMPTY, SIZE, columnOf, rowOf } from '../lib/sudoku/grid.js';
|
||||||
|
|
||||||
|
let { values, givens, conflicts, readonly = false, onchange } = $props();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps only a single digit 1-9, so a pasted or held key cannot leave the
|
||||||
|
* cell holding something the solver would reject.
|
||||||
|
* @param {Event} event
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
|
function handleInput(event, index) {
|
||||||
|
const typed = event.target.value.replace(/[^1-9]/g, '').slice(-1);
|
||||||
|
event.target.value = typed;
|
||||||
|
onchange?.(index, typed === '' ? EMPTY : Number(typed));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backspace and delete clear a cell; arrows move between cells.
|
||||||
|
* @param {KeyboardEvent} event
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
|
function handleKeydown(event, index) {
|
||||||
|
const moves = {
|
||||||
|
ArrowRight: 1,
|
||||||
|
ArrowLeft: -1,
|
||||||
|
ArrowDown: SIZE,
|
||||||
|
ArrowUp: -SIZE
|
||||||
|
};
|
||||||
|
|
||||||
|
const step = moves[event.key];
|
||||||
|
if (step === undefined) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const target = index + step;
|
||||||
|
if (target >= 0 && target < values.length) {
|
||||||
|
document.getElementById(`cell-${target}`)?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid" role="group" aria-label="Sudoku grid">
|
||||||
|
{#each values as value, index (index)}
|
||||||
|
<input
|
||||||
|
id="cell-{index}"
|
||||||
|
class="cell"
|
||||||
|
class:given={givens.has(index)}
|
||||||
|
class:conflict={conflicts.has(index)}
|
||||||
|
class:box-right={columnOf(index) % 3 === 2 && columnOf(index) !== SIZE - 1}
|
||||||
|
class:box-bottom={rowOf(index) % 3 === 2 && rowOf(index) !== SIZE - 1}
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
autocomplete="off"
|
||||||
|
maxlength="1"
|
||||||
|
{readonly}
|
||||||
|
aria-label="Row {rowOf(index) + 1}, column {columnOf(index) + 1}"
|
||||||
|
aria-invalid={conflicts.has(index)}
|
||||||
|
value={value === EMPTY ? '' : value}
|
||||||
|
oninput={(event) => handleInput(event, index)}
|
||||||
|
onkeydown={(event) => handleKeydown(event, index)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(9, 1fr);
|
||||||
|
width: min(90vw, 30rem);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-inline: auto;
|
||||||
|
border: 3px solid var(--rule-strong);
|
||||||
|
background: var(--rule-strong);
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: clamp(1rem, 5vw, 1.6rem);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--ink-solved);
|
||||||
|
background: var(--cell-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thicker rules mark the 3x3 boxes; the outer edge is the grid's own border. */
|
||||||
|
.box-right {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-bottom {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:focus {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.given {
|
||||||
|
color: var(--ink-given);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict {
|
||||||
|
background: var(--cell-conflict);
|
||||||
|
color: var(--ink-conflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:read-only {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
-596
@@ -1,596 +0,0 @@
|
|||||||
<?php
|
|
||||||
class SudokuSolver
|
|
||||||
{
|
|
||||||
protected $grid = [];
|
|
||||||
protected $emptySymbol;
|
|
||||||
public static function parseString($str, $emptySymbol = "0")
|
|
||||||
{
|
|
||||||
$grid = str_split($str);
|
|
||||||
foreach ($grid as &$v) {
|
|
||||||
if ($v == $emptySymbol) {
|
|
||||||
$v = 0;
|
|
||||||
} else {
|
|
||||||
$v = (int) $v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __construct($str, $emptySymbol = "0")
|
|
||||||
{
|
|
||||||
if (strlen($str) !== 81) {
|
|
||||||
throw new \Exception("Error sudoku");
|
|
||||||
}
|
|
||||||
$this->grid = static::parseString($str, $emptySymbol);
|
|
||||||
$this->emptySymbol = $emptySymbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function solve()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$this->placeNumber(0);
|
|
||||||
return false;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function placeNumber($pos)
|
|
||||||
{
|
|
||||||
if ($pos == 81) {
|
|
||||||
throw new \Exception("Finish");
|
|
||||||
}
|
|
||||||
if ($this->grid[$pos] > 0) {
|
|
||||||
$this->placeNumber($pos + 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for ($n = 1; $n <= 9; $n++) {
|
|
||||||
if ($this->checkValidity($n, $pos % 9, floor($pos / 9))) {
|
|
||||||
$this->grid[$pos] = $n;
|
|
||||||
$this->placeNumber($pos + 1);
|
|
||||||
$this->grid[$pos] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function checkValidity($val, $x, $y)
|
|
||||||
{
|
|
||||||
for ($i = 0; $i < 9; $i++) {
|
|
||||||
if (($this->grid[$y * 9 + $i] == $val) || ($this->grid[$i * 9 + $x] == $val)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$startX = (int) ((int) ($x / 3) * 3);
|
|
||||||
$startY = (int) ((int) ($y / 3) * 3);
|
|
||||||
|
|
||||||
for ($i = $startY; $i < $startY + 3; $i++) {
|
|
||||||
for ($j = $startX; $j < $startX + 3; $j++) {
|
|
||||||
if ($this->grid[$i * 9 + $j] == $val) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function display()
|
|
||||||
{
|
|
||||||
$str = "";
|
|
||||||
for ($i = 0; $i < 9; $i++) {
|
|
||||||
for ($j = 0; $j < 9; $j++) {
|
|
||||||
$str .= $this->grid[$i * 9 + $j];
|
|
||||||
$str .= " ";
|
|
||||||
if ($j == 2 || $j == 5) {
|
|
||||||
$str .= "| ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$str .= PHP_EOL;
|
|
||||||
if ($i == 2 || $i == 5) {
|
|
||||||
$str .= "------+-------+------" . PHP_EOL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
echo $str;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString()
|
|
||||||
{
|
|
||||||
foreach ($this->grid as &$item) {
|
|
||||||
if ($item == 0) {
|
|
||||||
$item = $this->emptySymbol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return implode("", $this->grid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
|
|
||||||
<title>Solve Sudoku (Made by MiTiX)</title>
|
|
||||||
|
|
||||||
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
background-color: #FAFAFA
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
border: 1px solid #000000;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
color: #000000;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
text-align: center;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
font-size: 24px;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:disabled {
|
|
||||||
background-color: #EEEEEE;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=number]::-webkit-inner-spin-button,
|
|
||||||
input[type=number]::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-0,
|
|
||||||
#cell-1,
|
|
||||||
#cell-2 {
|
|
||||||
border-top: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-2,
|
|
||||||
#cell-11,
|
|
||||||
#cell-20 {
|
|
||||||
border-right: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-18,
|
|
||||||
#cell-19,
|
|
||||||
#cell-20 {
|
|
||||||
border-bottom: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-0,
|
|
||||||
#cell-9,
|
|
||||||
#cell-18 {
|
|
||||||
border-left: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-3,
|
|
||||||
#cell-4,
|
|
||||||
#cell-5 {
|
|
||||||
border-top: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-5,
|
|
||||||
#cell-14,
|
|
||||||
#cell-23 {
|
|
||||||
border-right: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-21,
|
|
||||||
#cell-22,
|
|
||||||
#cell-23 {
|
|
||||||
border-bottom: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-3,
|
|
||||||
#cell-12,
|
|
||||||
#cell-21 {
|
|
||||||
border-left: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-6,
|
|
||||||
#cell-7,
|
|
||||||
#cell-8 {
|
|
||||||
border-top: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-8,
|
|
||||||
#cell-17,
|
|
||||||
#cell-26 {
|
|
||||||
border-right: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-24,
|
|
||||||
#cell-25,
|
|
||||||
#cell-26 {
|
|
||||||
border-bottom: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-6,
|
|
||||||
#cell-15,
|
|
||||||
#cell-24 {
|
|
||||||
border-left: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-27,
|
|
||||||
#cell-28,
|
|
||||||
#cell-29 {
|
|
||||||
border-top: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-29,
|
|
||||||
#cell-38,
|
|
||||||
#cell-47 {
|
|
||||||
border-right: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-45,
|
|
||||||
#cell-46,
|
|
||||||
#cell-47 {
|
|
||||||
border-bottom: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-27,
|
|
||||||
#cell-36,
|
|
||||||
#cell-45 {
|
|
||||||
border-left: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-30,
|
|
||||||
#cell-31,
|
|
||||||
#cell-32 {
|
|
||||||
border-top: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-32,
|
|
||||||
#cell-41,
|
|
||||||
#cell-50 {
|
|
||||||
border-right: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-48,
|
|
||||||
#cell-49,
|
|
||||||
#cell-50 {
|
|
||||||
border-bottom: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-30,
|
|
||||||
#cell-39,
|
|
||||||
#cell-48 {
|
|
||||||
border-left: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-33,
|
|
||||||
#cell-34,
|
|
||||||
#cell-35 {
|
|
||||||
border-top: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-35,
|
|
||||||
#cell-44,
|
|
||||||
#cell-53 {
|
|
||||||
border-right: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-51,
|
|
||||||
#cell-52,
|
|
||||||
#cell-53 {
|
|
||||||
border-bottom: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-33,
|
|
||||||
#cell-42,
|
|
||||||
#cell-51 {
|
|
||||||
border-left: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-54,
|
|
||||||
#cell-55,
|
|
||||||
#cell-56 {
|
|
||||||
border-top: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-56,
|
|
||||||
#cell-65,
|
|
||||||
#cell-74 {
|
|
||||||
border-right: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-72,
|
|
||||||
#cell-73,
|
|
||||||
#cell-74 {
|
|
||||||
border-bottom: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-54,
|
|
||||||
#cell-63,
|
|
||||||
#cell-72 {
|
|
||||||
border-left: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-57,
|
|
||||||
#cell-58,
|
|
||||||
#cell-59 {
|
|
||||||
border-top: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-59,
|
|
||||||
#cell-68,
|
|
||||||
#cell-77 {
|
|
||||||
border-right: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-75,
|
|
||||||
#cell-76,
|
|
||||||
#cell-77 {
|
|
||||||
border-bottom: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-57,
|
|
||||||
#cell-66,
|
|
||||||
#cell-75 {
|
|
||||||
border-left: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-60,
|
|
||||||
#cell-61,
|
|
||||||
#cell-62 {
|
|
||||||
border-top: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-62,
|
|
||||||
#cell-71,
|
|
||||||
#cell-80 {
|
|
||||||
border-right: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-78,
|
|
||||||
#cell-79,
|
|
||||||
#cell-80 {
|
|
||||||
border-bottom: 2px solid #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cell-60,
|
|
||||||
#cell-69,
|
|
||||||
#cell-78 {
|
|
||||||
border-left: 2px solid #000000;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<form action="sudoku.php" method="post" style="text-align: center;">
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<h1>Solve Sudoku (Made by MiTiX)</h1>
|
|
||||||
|
|
||||||
<table id="grid" style="margin-left:auto;margin-right:auto;">
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><input id="cell-0" name="cell-0" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-1" name="cell-1" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-2" name="cell-2" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-3" name="cell-3" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-4" name="cell-4" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-5" name="cell-5" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-6" name="cell-6" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-7" name="cell-7" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-8" name="cell-8" type="number" min="1" max="9"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><input id="cell-9" name="cell-9" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-10" name="cell-10" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-11" name="cell-11" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-12" name="cell-12" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-13" name="cell-13" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-14" name="cell-14" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-15" name="cell-15" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-16" name="cell-16" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-17" name="cell-17" type="number" min="1" max="9"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><input id="cell-18" name="cell-18" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-19" name="cell-19" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-20" name="cell-20" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-21" name="cell-21" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-22" name="cell-22" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-23" name="cell-23" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-24" name="cell-24" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-25" name="cell-25" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-26" name="cell-26" type="number" min="1" max="9"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><input id="cell-27" name="cell-27" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-28" name="cell-28" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-29" name="cell-29" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-30" name="cell-30" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-31" name="cell-31" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-32" name="cell-32" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-33" name="cell-33" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-34" name="cell-34" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-35" name="cell-35" type="number" min="1" max="9"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><input id="cell-36" name="cell-36" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-37" name="cell-37" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-38" name="cell-38" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-39" name="cell-39" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-40" name="cell-40" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-41" name="cell-41" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-42" name="cell-42" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-43" name="cell-43" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-44" name="cell-44" type="number" min="1" max="9"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><input id="cell-45" name="cell-45" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-46" name="cell-46" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-47" name="cell-47" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-48" name="cell-48" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-49" name="cell-49" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-50" name="cell-50" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-51" name="cell-51" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-52" name="cell-52" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-53" name="cell-53" type="number" min="1" max="9"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><input id="cell-54" name="cell-54" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-55" name="cell-55" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-56" name="cell-56" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-57" name="cell-57" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-58" name="cell-58" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-59" name="cell-59" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-60" name="cell-60" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-61" name="cell-61" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-62" name="cell-62" type="number" min="1" max="9"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><input id="cell-63" name="cell-63" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-64" name="cell-64" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-65" name="cell-65" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-66" name="cell-66" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-67" name="cell-67" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-68" name="cell-68" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-69" name="cell-69" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-70" name="cell-70" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-71" name="cell-71" type="number" min="1" max="9"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><input id="cell-72" name="cell-72" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-73" name="cell-73" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-74" name="cell-74" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-75" name="cell-75" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-76" name="cell-76" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-77" name="cell-77" type="number" min="1" max="9"></td>
|
|
||||||
|
|
||||||
<td><input id="cell-78" name="cell-78" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-79" name="cell-79" type="number" min="1" max="9"></td>
|
|
||||||
<td><input id="cell-80" name="cell-80" type="number" min="1" max="9"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<input type="submit" style="width: 100%;" value="Solve" name="solve"><br>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<?php
|
|
||||||
if (isset($_POST["solve"])) {
|
|
||||||
$input = "000000000000000000000000000000000000000000000000000000000000000000000000000000000";
|
|
||||||
for ($i = 0; $i < 81; $i++) {
|
|
||||||
if (isset($_POST["cell-{$i}"])) {
|
|
||||||
$input[$i] = $_POST["cell-{$i}"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<form style="text-align: center;">
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<h1>Input</h1>
|
|
||||||
|
|
||||||
<table id="grid" style="margin-left:auto;margin-right:auto;">
|
|
||||||
<?php
|
|
||||||
$i = 0;
|
|
||||||
while ($i < 81) {
|
|
||||||
if ($i % 9 == 0) {
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
$id = "cell-{$i}";
|
|
||||||
?>
|
|
||||||
<td><input id="<?php echo $id; ?>" type="number" min="1" max="9" <?php if ($input[$i] != 0) { ?> value="<?php echo $input[$i]; ?>" disabled <?php } ?>></td>
|
|
||||||
<?php
|
|
||||||
if ($i % 9 == 8) {
|
|
||||||
?>
|
|
||||||
</tr>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
$i++;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<?php
|
|
||||||
$solver = new SudokuSolver($input);
|
|
||||||
$solver->solve();
|
|
||||||
$output = (string) $solver;
|
|
||||||
?>
|
|
||||||
|
|
||||||
<form style="text-align: center;">
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<h1>Output</h1>
|
|
||||||
|
|
||||||
<table id="grid" style="margin-left:auto;margin-right:auto;">
|
|
||||||
<?php
|
|
||||||
$i = 0;
|
|
||||||
while ($i < 81) {
|
|
||||||
if ($i % 9 == 0) {
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
$id = "cell-{$i}";
|
|
||||||
?>
|
|
||||||
<td><input id="<?php echo $id; ?>" type="number" min="1" max="9" value="<?php echo $output[$i]; ?>" disabled></td>
|
|
||||||
<?php
|
|
||||||
if ($i % 9 == 8) {
|
|
||||||
?>
|
|
||||||
</tr>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
$i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<br>Made by @MiTiX
|
|
||||||
<br>source: https://rosettacode.org/wiki/Sudoku
|
|
||||||
<br>source: https://gist.github.com/thebinarypenguin/4d45ffe87096e508800b5d11544bf2fa
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: '.',
|
||||||
|
plugins: [svelte()],
|
||||||
|
server: { port: 8080 }
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: '.',
|
||||||
|
// Pages serves the site from /<repo>/, so assets resolve under that prefix.
|
||||||
|
base: '/sudoku-solver/',
|
||||||
|
plugins: [svelte()],
|
||||||
|
build: { outDir: 'dist', emptyOutDir: true }
|
||||||
|
});
|
||||||
Reference in new issue
Block a user