feat(js): rewrite in JavaScript as a browser app

This commit is contained in:
tiennm99 committed 2026-08-31 17:08:26 +07:00
1 parent c6ad7ad414
commit 3643f75f30
18 files changed
+3575 -620

No files matched your search

+49
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
node_modules
dist
.vite
.DS_Store
*.log
.claude
+63 -24
View File
@@ -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
+28
View File
@@ -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
View File
@@ -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>
+2661
View File
File diff suppressed because it is too large. Load diff
+35
View File
@@ -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"
}
}
+6
View File
@@ -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
View File
@@ -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;
}
+104
View File
@@ -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];
}
+81
View File
@@ -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;
}
+140
View File
@@ -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();
});
});
+6
View File
@@ -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') });
+197
View File
@@ -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>
+118
View File
@@ -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
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
root: '.',
plugins: [svelte()],
server: { port: 8080 }
});
+10
View File
@@ -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 }
});