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
|
||||
|
||||
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:
|
||||
|
||||
```php
|
||||
$puzzle = new SudokuSolver("530070000600195000098000060800060003400803001700020006060000280000419005000080079");
|
||||
$puzzle->solve();
|
||||
```sh
|
||||
npm install
|
||||
npm run dev # http://localhost:8080
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Input:
|
||||
```
|
||||
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
|
||||
```sh
|
||||
npm run build # static site into dist/
|
||||
npm run preview
|
||||
```
|
||||
|
||||
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
|
||||
6 7 2 | 1 9 5 | 3 4 8
|
||||
1 9 8 | 3 4 2 | 5 6 7
|
||||
530070000600195000098000060800060003400803001700020006060000280000419005000080079
|
||||
```
|
||||
|
||||
## Usage
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
php sudoku.php
|
||||
```sh
|
||||
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
|
||||
|
||||
|
||||
@@ -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