Sudoku solver: Finish project

This commit is contained in:
CherryKitten 2022-11-12 22:40:00 +01:00
parent 0d2e39e7ba
commit f1ce58405c
Signed by: sammy
GPG key ID: 0B696A86A853E955
5 changed files with 186 additions and 80 deletions

View file

@ -29,8 +29,8 @@ export const invalidStrings = [
]; ];
export const unsolvableStrings = [ export const unsolvableStrings = [
"115..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.420..8..1..16....926969.37.", "115..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.421..8..1..16....926969.37.",
"4206942069.......................................................................", "4216942169.......................................................................",
"111111111111111111111111111111111111111111111111111111111111111111111111111111111", "111111111111111111111111111111111111111111111111111111111111111111111111111111111",
]; ];

View file

@ -1,47 +1,127 @@
const grid = [ let currentPuzzle = "";
[1, 2, 3, 4, 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14, 15, 16, 17, 18],
[19, 20, 21, 22, 23, 24, 25, 26, 27],
[28, 29, 30, 31, 32, 33, 34, 35, 36],
[37, 38, 39, 40, 41, 42, 43, 44, 45],
[46, 47, 48, 49, 50, 51, 52, 53, 54],
[55, 56, 57, 58, 59, 60, 61, 62, 63],
[64, 65, 66, 67, 68, 69, 70, 71, 72],
[73, 74, 75, 76, 77, 78, 79, 80, 81],
];
class SudokuSolver { class SudokuSolver {
getPuzzleArray(puzzleString) { getPuzzleArray(puzzleString) {
const puzzleArray = []; const puzzleArray = [[], [], [], [], [], [], [], [], []];
for (let i = 1; i <= 81; i++) { for (let i = 0; i <= 80; i++) {
let value = puzzleString[i - 1]; let value = puzzleString[i];
value === "." || !value value === "." || !value
? (puzzleArray[Math.ceil(i / 9)][(i % 9) + 1] = " ") ? (puzzleArray[Math.floor(i / 9)][i % 9] = " ")
: (puzzleArray[Math.ceil(i / 9)][(i % 9) + 1] = value); : (puzzleArray[Math.floor(i / 9)][i % 9] = value);
} }
return puzzleArray;
}
getRow(puzzle, row) {
return puzzle[row];
}
getColumn(puzzle, row, column) {
let col = [];
for (let i in puzzle) {
col.push(puzzle[i][column]);
}
return col;
}
getRegion(puzzle, row, column) {
let region = [];
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
region.push(
puzzle[Math.floor(row / 3) * 3 + r][Math.floor(column / 3) * 3 + c]
);
}
}
return region;
} }
validate(puzzleString) { validate(puzzleString) {
if (puzzleString.length != 81) { if (puzzleString.length !== 81) {
return { return {
status: "error",
error: "Expected puzzle to be 81 characters long", error: "Expected puzzle to be 81 characters long",
}; };
} else if (!/^[1-9\.]+$/.test(puzzleString)) { } else if (!/^[1-9.]+$/.test(puzzleString)) {
return { status: "error", error: "Invalid characters in puzzle" }; return { error: "Invalid characters in puzzle" };
} else { } else {
for (let i in puzzleString) {
let val = puzzleString[i];
let row = Math.floor(i / 9);
let column = i % 9;
if (val !== ".") {
let temp = puzzleString.split("");
temp[i] = ".";
if (
this.checkRowPlacement(temp, row, column, val) &&
this.checkColPlacement(temp, row, column, val) &&
this.checkRegionPlacement(temp, row, column, val)
) {
temp[i] = val;
puzzleString = temp.join("");
} else {
return { error: "Puzzle cannot be solved" };
}
}
}
return true; return true;
} }
} }
checkRowPlacement(puzzleString, row, column, value) {} checkRowPlacement(puzzleString, row, column, value) {
let puzzle = this.getPuzzleArray(puzzleString);
let puzzleRow = this.getRow(puzzle, row, column);
return !(
puzzleRow.includes(value.toString()) &&
puzzleRow[column] !== value.toString()
);
}
checkColPlacement(puzzleString, row, column, value) {} checkColPlacement(puzzleString, row, column, value) {
let puzzle = this.getPuzzleArray(puzzleString);
let puzzleCol = this.getColumn(puzzle, row, column);
return !(
puzzleCol.includes(value.toString()) &&
puzzleCol[row] !== value.toString()
);
}
checkRegionPlacement(puzzleString, row, column, value) {} checkRegionPlacement(puzzleString, row, column, value) {
let puzzle = this.getPuzzleArray(puzzleString);
let puzzleRegion = this.getRegion(puzzle, row, column);
return !(
puzzleRegion.includes(value.toString()) &&
puzzle[row][column] !== value.toString()
);
}
solve(puzzleString) { solve(puzzleString) {
return { error: "test" }; if (puzzleString) currentPuzzle = puzzleString;
let valid = this.validate(currentPuzzle);
if (valid !== true) {
return valid;
}
let empty = currentPuzzle.indexOf(".");
let row = Math.floor(empty / 9);
let column = empty % 9;
if (empty !== -1) {
for (let i = 1; i <= 9; i++) {
if (
this.checkRowPlacement(currentPuzzle, row, column, i) &&
this.checkColPlacement(currentPuzzle, row, column, i) &&
this.checkRegionPlacement(currentPuzzle, row, column, i)
) {
let temp = currentPuzzle.split("");
temp[empty] = i;
currentPuzzle = temp.join("");
this.solve();
}
}
}
if (currentPuzzle.indexOf(".") !== -1) {
let temp = currentPuzzle.split("");
temp[empty] = ".";
currentPuzzle = temp.join("");
}
return { solution: currentPuzzle };
} }
} }

View file

@ -1,18 +1,47 @@
'use strict'; "use strict";
const SudokuSolver = require('../controllers/sudoku-solver.js'); const SudokuSolver = require("../controllers/sudoku-solver.js");
module.exports = function (app) { module.exports = function (app) {
let solver = new SudokuSolver(); let solver = new SudokuSolver();
app.route('/api/check') app.route("/api/check").post((req, res) => {
.post((req, res) => { if (!req.body.puzzle || !req.body.coordinate || !req.body.value) {
return res.json({ error: "Required field(s) missing" });
}
if (!(req.body.value > 0) || !(req.body.value < 10)) {
return res.json({ error: "Invalid value" });
}
if (!/^[A-I][1-9]$/.test(req.body.coordinate)) {
return res.json({ error: "Invalid coordinate" });
}
let validString = solver.validate(req.body.puzzle);
if (validString !== true) {
return res.json(validString);
}
let row = req.body.coordinate[0].charCodeAt(0) - 65;
let col = req.body.coordinate[1] - 1;
let conflict = [];
let valid;
if (!solver.checkRowPlacement(req.body.puzzle, row, col, req.body.value)) {
conflict.push("row");
}
if (!solver.checkColPlacement(req.body.puzzle, row, col, req.body.value)) {
conflict.push("column");
}
if (
!solver.checkRegionPlacement(req.body.puzzle, row, col, req.body.value)
) {
conflict.push("region");
}
conflict.length > 0 ? (valid = false) : (valid = true);
res.json({ valid: valid, conflict: conflict });
}); });
app.route('/api/solve') app.route("/api/solve").post((req, res) => {
.post((req, res) => { if (!req.body.puzzle) {
return res.json({ error: "Required field missing" });
}
res.json(solver.solve(req.body.puzzle));
}); });
}; };

View file

@ -3,7 +3,6 @@ const assert = chai.assert;
import { import {
puzzlesAndSolutions, puzzlesAndSolutions,
invalidStrings, invalidStrings,
unsolvableStrings,
wrongLengthStrings, wrongLengthStrings,
} from "../controllers/puzzle-strings"; } from "../controllers/puzzle-strings";
@ -36,11 +35,11 @@ suite("Unit Tests", () => {
}); });
}); });
suite("Placement Checks", () => { suite("Placement Checks", () => {
test("Login handles a valid row placement", () => { test("Logic handles a valid row placement", () => {
const checks = [ const checks = [
solver.checkRowPlacement(puzzlesAndSolutions[0][0], "A", 2, 3), solver.checkRowPlacement(puzzlesAndSolutions[0][0], 0, 3, 3),
solver.checkRowPlacement(puzzlesAndSolutions[0][1], "A", 2, 6), solver.checkRowPlacement(puzzlesAndSolutions[1][0], 0, 3, 6),
solver.checkRowPlacement(puzzlesAndSolutions[0][2], "A", 1, 2), solver.checkRowPlacement(puzzlesAndSolutions[2][0], 0, 2, 2),
]; ];
for (let i in checks) { for (let i in checks) {
assert.isTrue(checks[i]); assert.isTrue(checks[i]);
@ -48,9 +47,9 @@ suite("Unit Tests", () => {
}); });
test("Logic handles an invalid row placement", () => { test("Logic handles an invalid row placement", () => {
const checks = [ const checks = [
solver.checkRowPlacement(puzzlesAndSolutions[0][0], "A", 2, 8), solver.checkRowPlacement(puzzlesAndSolutions[0][0], 0, 3, 5),
solver.checkRowPlacement(puzzlesAndSolutions[0][1], "A", 2, 1), solver.checkRowPlacement(puzzlesAndSolutions[1][0], 0, 3, 1),
solver.checkRowPlacement(puzzlesAndSolutions[0][2], "A", 1, 3), solver.checkRowPlacement(puzzlesAndSolutions[2][0], 0, 2, 3),
]; ];
for (let i in checks) { for (let i in checks) {
assert.isFalse(checks[i]); assert.isFalse(checks[i]);
@ -58,9 +57,9 @@ suite("Unit Tests", () => {
}); });
test("Login handles a valid column placement", () => { test("Login handles a valid column placement", () => {
const checks = [ const checks = [
solver.checkColPlacement(puzzlesAndSolutions[0][0], "A", 2, 3), solver.checkColPlacement(puzzlesAndSolutions[0][0], 0, 1, 3),
solver.checkColPlacement(puzzlesAndSolutions[0][1], "A", 2, 6), solver.checkColPlacement(puzzlesAndSolutions[1][0], 0, 1, 6),
solver.checkColPlacement(puzzlesAndSolutions[0][2], "A", 1, 2), solver.checkColPlacement(puzzlesAndSolutions[2][0], 0, 0, 2),
]; ];
for (let i in checks) { for (let i in checks) {
assert.isTrue(checks[i]); assert.isTrue(checks[i]);
@ -68,9 +67,9 @@ suite("Unit Tests", () => {
}); });
test("Logic handles an invalid column placement", () => { test("Logic handles an invalid column placement", () => {
const checks = [ const checks = [
solver.checkColPlacement(puzzlesAndSolutions[0][0], "B", 2, 9), solver.checkColPlacement(puzzlesAndSolutions[0][0], 1, 1, 9),
solver.checkColPlacement(puzzlesAndSolutions[0][1], "B", 2, 8), solver.checkColPlacement(puzzlesAndSolutions[1][0], 1, 1, 8),
solver.checkColPlacement(puzzlesAndSolutions[0][2], "C", 3, 2), solver.checkColPlacement(puzzlesAndSolutions[2][0], 2, 2, 2),
]; ];
for (let i in checks) { for (let i in checks) {
assert.isFalse(checks[i]); assert.isFalse(checks[i]);
@ -78,9 +77,9 @@ suite("Unit Tests", () => {
}); });
test("Logic handles a valid region (3x3 grid) placement", () => { test("Logic handles a valid region (3x3 grid) placement", () => {
const checks = [ const checks = [
solver.checkRegionPlacement(puzzlesAndSolutions[0][0], "A", 2, 3), solver.checkRegionPlacement(puzzlesAndSolutions[0][0], 0, 1, 3),
solver.checkRegionPlacement(puzzlesAndSolutions[0][1], "A", 2, 6), solver.checkRegionPlacement(puzzlesAndSolutions[1][0], 0, 1, 6),
solver.checkRegionPlacement(puzzlesAndSolutions[0][2], "A", 1, 2), solver.checkRegionPlacement(puzzlesAndSolutions[2][0], 0, 0, 2),
]; ];
for (let i in checks) { for (let i in checks) {
assert.isTrue(checks[i]); assert.isTrue(checks[i]);
@ -88,9 +87,9 @@ suite("Unit Tests", () => {
}); });
test("Logic handles an invalid region (3x3 grid) placement", () => { test("Logic handles an invalid region (3x3 grid) placement", () => {
const checks = [ const checks = [
solver.checkRegionPlacement(puzzlesAndSolutions[0][0], "C", 3, 5), solver.checkRegionPlacement(puzzlesAndSolutions[0][0], 2, 2, 5),
solver.checkRegionPlacement(puzzlesAndSolutions[0][1], "C", 3, 5), solver.checkRegionPlacement(puzzlesAndSolutions[1][0], 2, 2, 5),
solver.checkRegionPlacement(puzzlesAndSolutions[0][2], "C", 3, 5), solver.checkRegionPlacement(puzzlesAndSolutions[2][0], 2, 2, 5),
]; ];
for (let i in checks) { for (let i in checks) {
assert.isFalse(checks[i]); assert.isFalse(checks[i]);
@ -101,7 +100,7 @@ suite("Unit Tests", () => {
test("Valid puzzle strings pass the solver", () => { test("Valid puzzle strings pass the solver", () => {
for (let i in puzzlesAndSolutions) { for (let i in puzzlesAndSolutions) {
assert.equal( assert.equal(
solver.solve(puzzlesAndSolutions[i][1]), solver.solve(puzzlesAndSolutions[i][0]).solution,
puzzlesAndSolutions[i][1] puzzlesAndSolutions[i][1]
); );
} }
@ -109,8 +108,8 @@ suite("Unit Tests", () => {
test("Invalid puzzle strings fail the solver", () => { test("Invalid puzzle strings fail the solver", () => {
for (let i in invalidStrings) { for (let i in invalidStrings) {
assert.equal( assert.equal(
solver.solve(invalidStrings[i]), solver.solve(invalidStrings[i]).error,
{ error: "Invalid characters in puzzle" }, "Invalid characters in puzzle",
"invalid characters" "invalid characters"
); );
} }
@ -118,9 +117,8 @@ suite("Unit Tests", () => {
test("Solver returns the expected solution for an incomplete puzzle", () => { test("Solver returns the expected solution for an incomplete puzzle", () => {
for (let i in puzzlesAndSolutions) { for (let i in puzzlesAndSolutions) {
assert.equal( assert.equal(
solver.solve(puzzlesAndSolutions[i][0]), solver.solve(puzzlesAndSolutions[i][0]).solution,
puzzlesAndSolutions[i][1], puzzlesAndSolutions[i][1]
puzzlesAndSolutions[i]
); );
} }
}); });

View file

@ -22,12 +22,11 @@ suite("Functional Tests", () => {
.end((err, res) => { .end((err, res) => {
assert.equal(res.status, 200); assert.equal(res.status, 200);
assert.equal(res.body.solution, puzzlesAndSolutions[i][1]); assert.equal(res.body.solution, puzzlesAndSolutions[i][1]);
done();
}); });
} }
done();
}); });
test("Solve a puzzle with missing puzzle string: POST request to /api/solve", (done) => { test("Solve a puzzle with missing puzzle string: POST request to /api/solve", (done) => {
for (let i in puzzlesAndSolutions) {
chai chai
.request(server) .request(server)
.post("/api/solve") .post("/api/solve")
@ -37,7 +36,6 @@ suite("Functional Tests", () => {
assert.equal(res.body.error, "Required field missing"); assert.equal(res.body.error, "Required field missing");
done(); done();
}); });
}
}); });
test("Solve a puzzle with invalid characters: POST request to /api/solve", (done) => { test("Solve a puzzle with invalid characters: POST request to /api/solve", (done) => {
for (let i in invalidStrings) { for (let i in invalidStrings) {
@ -48,9 +46,9 @@ suite("Functional Tests", () => {
.end((err, res) => { .end((err, res) => {
assert.equal(res.status, 200); assert.equal(res.status, 200);
assert.equal(res.body.error, "Invalid characters in puzzle"); assert.equal(res.body.error, "Invalid characters in puzzle");
done();
}); });
} }
done();
}); });
test("Solve a puzzle with incorrect length: POST request to /api/solve", (done) => { test("Solve a puzzle with incorrect length: POST request to /api/solve", (done) => {
for (let i in wrongLengthStrings) { for (let i in wrongLengthStrings) {
@ -62,11 +60,11 @@ suite("Functional Tests", () => {
assert.equal(res.status, 200); assert.equal(res.status, 200);
assert.equal( assert.equal(
res.body.error, res.body.error,
"Expected puzzle to be 81 charcters long" "Expected puzzle to be 81 characters long"
); );
done();
}); });
} }
done();
}); });
test("Solve a puzzle that cannot be solved: POST request to /api/solve", (done) => { test("Solve a puzzle that cannot be solved: POST request to /api/solve", (done) => {
for (let i in unsolvableStrings) { for (let i in unsolvableStrings) {
@ -77,11 +75,12 @@ suite("Functional Tests", () => {
.end((err, res) => { .end((err, res) => {
assert.equal(res.status, 200); assert.equal(res.status, 200);
assert.equal(res.body.error, "Puzzle cannot be solved"); assert.equal(res.body.error, "Puzzle cannot be solved");
done();
}); });
} }
done();
}); });
}); });
suite("Placement Checks", () => { suite("Placement Checks", () => {
test("Check a puzzle placement with all fields: POST request to /api/check", (done) => { test("Check a puzzle placement with all fields: POST request to /api/check", (done) => {
chai chai