Skip to content

Commit

Permalink
Improved to solve "world's hardest" sudoku
Browse files Browse the repository at this point in the history
  • Loading branch information
mlohbihler committed Nov 27, 2017
1 parent 8a923ea commit 166be49
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 107 deletions.
2 changes: 2 additions & 0 deletions Sudoku/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/.gradle/
/.settings/
/bin/
/build/
/gradle/
28 changes: 0 additions & 28 deletions Sudoku/bin/lohbihler/sudoku/.gitignore

This file was deleted.

7 changes: 7 additions & 0 deletions Sudoku/src/main/java/lohbihler/sudoku/PossibleCellValues.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,11 @@ public PossibleCellValues copy() {
public boolean contains(final Character c) {
return remaining.contains(c);
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
remaining.forEach(c -> sb.append(c));
return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,41 @@ public void process(final PuzzleModel<?> model) {
PuzzleModel<?> modelClone;

// Iterate through the cells looking for those with multiple potential values.
for (int y = 0; y < model.getSize(); y++) {
for (int x = 0; x < model.getSize(); x++) {
if (!model.isSolved(x, y)) {
final PossibleCellValues pcv = model.getCell(x, y).copy();
try {
for (int y = 0; y < model.getSize(); y++) {
for (int x = 0; x < model.getSize(); x++) {
if (!model.isSolved(x, y)) {
final PossibleCellValues pcv = model.getCell(x, y).copy();

// Test the values in the clone.
for (int i = 0; i < pcv.size(); i++) {
modelClone = model.copy();
modelClone.setValue(x, y, pcv.get(i));
// Test the values in the clone.
for (int i = 0; i < pcv.size(); i++) {
modelClone = model.copy();
modelClone.setValue(x, y, pcv.get(i));

try {
SolverFactory.solveFirstLevel(modelClone);
} catch (final RuntimeException e) {
// The value didn't work, so remove it as a potential value.
model.removeValue(x, y, pcv.get(i));
try {
// SolverFactory.solveFirstLevel(modelClone);
SolverFactory.solve(modelClone);
if (modelClone.isSolved()) {
model.set(modelClone);
throw new SolvedException();
}
} catch (final RuntimeException e) {
// The value didn't work, so remove it as a potential value.
model.removeValue(x, y, pcv.get(i));
if (model.isEmpty(x, y)) {
throw new RuntimeException("No values remain at " + x + ", " + y);
}
}
}
}
}
}
} catch (final SolvedException e) {
// no op
}
}

static class SolvedException extends Exception {
private static final long serialVersionUID = 1L;
}
}
28 changes: 22 additions & 6 deletions Sudoku/src/main/java/lohbihler/sudoku/PuzzleModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public int getSize() {
return getBoxSize() * getBoxSize();
}

public void set(final PuzzleModel<?> that) {
possibleCellValues = that.possibleCellValues;
changed = that.changed;
}

public boolean hasChanged() {
return changed;
}
Expand Down Expand Up @@ -93,34 +98,45 @@ public void setValue(final int x, final int y, final Character value) {
}
}

public boolean isEmpty(final int x, final int y) {
return possibleCellValues[x][y].size() == 0;
}

public boolean isSolved() {
return !XYStreams.toStream(possibleCellValues).anyMatch(pcv -> !pcv.isSolved());
}

public void dump() {
@Override
public String toString() {
final int maxSize = XYStreams.toStream(possibleCellValues) //
.max((pcv1, pcv2) -> pcv1.size() - pcv2.size()) //
.get().size();

final StringBuilder sb = new StringBuilder();

Arrays.stream(possibleCellValues) //
.forEach(arr -> {
Arrays.stream(arr) //
.forEach(pcv -> {
int counter = maxSize;
// Write out the remaining values
for (int i = 0; i < pcv.size(); i++) {
System.out.print(pcv.get(i));
sb.append(pcv.get(i));
counter--;
}
// Fill in with spaces.
while (counter > 0) {
System.out.print(' ');
sb.append(' ');
counter--;
}
System.out.print(" | ");
sb.append(" | ");
});
System.out.println();
sb.append("\r\n");
});
System.out.println();
return sb.toString();
}

public void dump() {
System.out.println(toString());
}
}
33 changes: 20 additions & 13 deletions Sudoku/src/main/java/lohbihler/sudoku/SolverFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@
* @author mlohbihler
*/
public class SolverFactory {
public static Solver[] firstLevelSolvers() {
return new Solver[] { new BoxEliminator(), new HorizontalEliminator(), new VerticalEliminator(),
new BoxSingleValueSeeker(), new HorizontalSingleValueSeeker(), new VerticalSingleValueSeeker(), };
}
private static Solver[] firstLevelSolvers = { new BoxEliminator(), new HorizontalEliminator(),
new VerticalEliminator(), new BoxSingleValueSeeker(), new HorizontalSingleValueSeeker(),
new VerticalSingleValueSeeker(), };

public static Solver[] secondLevelSolvers() {
return new Solver[] { new PotentialValueElimination(), };
}
private static Solver[] secondLevelSolvers = { new PotentialValueElimination(), };

private static Validator[] validators = { new HorizontalValidator(), new VerticalValidator(), new BoxValidator(), };

public static boolean solveFirstLevel(final PuzzleModel<?> model) {
boolean changed = false;
model.reset();
final Solver[] solvers = firstLevelSolvers();
while (true) {
for (int i = 0; i < solvers.length; i++)
solvers[i].process(model);
for (int i = 0; i < firstLevelSolvers.length; i++)
firstLevelSolvers[i].process(model);
if (!model.hasChanged())
break;
changed = true;
Expand All @@ -31,10 +29,9 @@ public static boolean solveFirstLevel(final PuzzleModel<?> model) {
public static boolean solveSecondLevel(final PuzzleModel<?> model) {
boolean changed = false;
model.reset();
final Solver[] solvers = secondLevelSolvers();
while (true) {
for (int i = 0; i < solvers.length; i++)
solvers[i].process(model);
for (int i = 0; i < secondLevelSolvers.length; i++)
secondLevelSolvers[i].process(model);
if (!model.hasChanged())
break;
changed = true;
Expand All @@ -44,6 +41,16 @@ public static boolean solveSecondLevel(final PuzzleModel<?> model) {
}

public static void solve(final PuzzleModel<?> model) {
// First validate the model.
for (int i = 0; i < validators.length; i++) {
validators[i].validate(model);
}

// Then try to solve it.
solveValidated(model);
}

static void solveValidated(final PuzzleModel<?> model) {
boolean changed = false;
while (true) {
System.out.println("First level...");
Expand Down
118 changes: 71 additions & 47 deletions Sudoku/src/test/java/lohbihler/sudoku/SudokuTest.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package lohbihler.sudoku;

import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -260,29 +258,6 @@ public void custom1() {
});
}

@Test
public void custom2() {
try {
test(new Puzzle9x9(),
new char[][] { //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', '?', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', '?', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
});
fail("Should have throws RTE");
} catch (final RuntimeException e) {
Assert.assertEquals(
"Attempt to set cell to a value that has already been eliminated: cell:[1, 2, 3, 4, 5, 6, 7, 8, 9], value=?",
e.getMessage());
}
}

@Test
public void custom3() {
final Puzzle9x9 model = test(new Puzzle9x9(),
Expand Down Expand Up @@ -599,28 +574,77 @@ public void worldsHardest() {

assertEquals(model,
new char[][] { //
{ '9', '3', '1', '4', '5', '8', '2', '6', '7', }, //
{ '2', '7', '4', '6', '3', '9', '1', '5', '8', }, //
{ '6', '8', '5', '7', '1', '2', '3', '4', '9', }, //
{ '5', '4', '8', '3', '6', '1', '7', '9', '2', }, //
{ '1', '2', '3', '9', '4', '7', '5', '8', '6', }, //
{ '7', '9', '6', '8', '2', '5', '4', '1', '3', }, //
{ '3', '6', '2', '5', '9', '4', '8', '7', '1', }, //
{ '4', '1', '7', '2', '8', '6', '9', '3', '5', }, //
{ '8', '5', '9', '1', '7', '3', '6', '2', '4', }, //
{ '8', '1', '2', '7', '5', '3', '6', '4', '9', }, //
{ '9', '4', '3', '6', '8', '2', '1', '7', '5', }, //
{ '6', '7', '5', '4', '9', '1', '2', '8', '3', }, //
{ '1', '5', '4', '2', '3', '7', '8', '9', '6', }, //
{ '3', '6', '9', '8', '4', '5', '7', '2', '1', }, //
{ '2', '8', '7', '1', '6', '9', '5', '3', '4', }, //
{ '5', '2', '1', '9', '7', '4', '3', '6', '8', }, //
{ '4', '3', '8', '5', '2', '6', '9', '1', '7', }, //
{ '7', '9', '6', '3', '1', '8', '4', '5', '2', }, //
});
}

@Test
public void empty() {
final Puzzle9x9 model = test(new Puzzle9x9(),
new char[][] { //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
});

assertEquals(model,
new char[][] { //
{ '1', '4', '7', '2', '3', '8', '5', '6', '9', }, //
{ '2', '5', '8', '1', '6', '9', '3', '4', '7', }, //
{ '3', '6', '9', '4', '5', '7', '1', '2', '8', }, //
{ '4', '7', '1', '3', '8', '2', '6', '9', '5', }, //
{ '5', '8', '2', '6', '9', '1', '4', '7', '3', }, //
{ '6', '9', '3', '5', '7', '4', '2', '8', '1', }, //
{ '7', '1', '4', '8', '2', '3', '9', '5', '6', }, //
{ '8', '2', '5', '9', '1', '6', '7', '3', '4', }, //
{ '9', '3', '6', '7', '4', '5', '8', '1', '2', }, //
});
}

@Test
public void bad() {
try {
test(new Puzzle9x9(),
new char[][] { //
{ '1', '1', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
{ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, //
});
} catch (final RuntimeException e) {
Assert.assertEquals("Duplicate column value at x=0, y=1", e.getMessage());
}
}

private static <T extends PuzzleModel<?>> T test(final T model, final char[][] data) {
final long startTime = System.currentTimeMillis();

model.init();
for (int y = 0; y < data.length; y++) {
for (int x = 0; x < data[0].length; x++) {
if (data[y][x] == '-')
data[y][x] = ' ';
if (data[y][x] != ' ')
model.setValue(x, y, data[y][x]);
for (int x = 0; x < data.length; x++) {
for (int y = 0; y < data[0].length; y++) {
if (data[x][y] == '-')
data[x][y] = ' ';
if (data[x][y] != ' ')
model.setValue(x, y, data[x][y]);
}
}

Expand All @@ -634,21 +658,21 @@ private static <T extends PuzzleModel<?>> T test(final T model, final char[][] d
validators.get(i).validate(model);
}

final String time = Long.toString((System.currentTimeMillis() - startTime) / 1000);
final String time = Long.toString(System.currentTimeMillis() - startTime);
model.dump();
if (model.isSolved())
System.out.println("Puzzle was solved in " + time + " s");
System.out.println("Puzzle was solved in " + time + " ms");
else
System.out.println("Puzzle was not solved in " + time + " s");
System.out.println("Puzzle was not solved in " + time + " ms");

return model;
}

private static void assertEquals(final PuzzleModel<?> model, final char[][] solution) {
for (int y = 0; y < solution.length; y++) {
for (int x = 0; x < solution[0].length; x++) {
if (solution[y][x] != model.getSolvedValue(x, y).charValue()) {
throw new AssertionError("Expected " + solution[y][x] + " but found "
for (int x = 0; x < solution[0].length; x++) {
for (int y = 0; y < solution.length; y++) {
if (solution[x][y] != model.getSolvedValue(x, y).charValue()) {
throw new AssertionError("Expected " + solution[x][y] + " but found "
+ model.getSolvedValue(x, y).charValue() + " at " + x + "," + y);
}
}
Expand Down

0 comments on commit 166be49

Please sign in to comment.