I finished creating a binary puzzle solver. The game can be found here: http://binarypuzzle.com/.
The rules for the game are:
- Each box should contain a zero or a one.
- No more than two similar numbers next to or below each other are allowed.
- Each row and each column should contain an equal number of zeros and ones.
- Each row is unique and each column is unique.
Each binary puzzle does only have one solution. You can always find this solution without guessing.
I solved it by just generating all possibilities and putting it against the rules.
I still have a couple of question about the quality of my code and what is preferred and not. Here are my questions about my BinaryPuzzle class:
- Should
BLANK
be private static inside theBinaryPuzzle
class? Or would it be better if I had anEnum
for BLANK, ONE, and ZERO? - Is the name
values[][]
a good name? Why notboardValues
orboard
? - Say I make a jar of this and someone would use this class, he will always have to extend this class if he wants to get the
values[][]
is that too strict?
public class BinaryPuzzle {
private static final int BLANK = -1;
private final int size;
private final int[][] values;
public BinaryPuzzle(int size) {
if (size < 6 || size > 14 || size % 2 != 0) {
throw new IllegalArgumentException("Invalid size. Only size of 6, 8, 10, 12, 14 allowed.");
}
this.size = size;
values = new int[size][size];
fillValuesWithBlank();
}
public BinaryPuzzle setValue(int x, int y, int value) {
if (value != 0 && value != 1) {
throw new IllegalArgumentException("Only 0 or 1 allowed.");
}
if ((x < 0 || x >= size) || (y < 0 || y >= size)) {
throw new IllegalArgumentException(
"x or y is outside the bounds. Only values between 0 and " + (size - 1) + " allowed.");
}
values[x][y] = value;
return this;
}
protected int[][] getValues() {
return values;
}
protected void fillValuesWithBlank() {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
values[i][j] = BLANK;
}
}
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
stringBuilder
.append(values[i][j])
.append(" ");
}
stringBuilder.append("\n"); // newline
}
return stringBuilder.toString();
}
}
Questions about the BinaryPuzzleSolver
class:
- Should my
solve()
method be static? It is stateless, but if I make it static I can't override any methods. - Should
solve()
clone/copyBinaryPuzzle
and return a new one? Or is the fact that it returns aboolean
enough to imply that theBinaryPuzzle
given as param is modified? solve()
actually only uses thevalues[][]
fromBinaryPuzzle
, should that be the param instead?- Are my helper methods good with regards to naming and lenght? I tried to think about good names and make them smaller about 7 lines, but it is hard!
- As you can see some methods have the
default
modifier instead ofprivate
, because they are less trivial and I wanted to write tests for them. Performing TDD and testing only public methods (solve()
in this case) seems bad, so much functionality is insolve()
. But making my methods that I want to testdefault
seems weird, I have never seen someone do that. The classes that I left private are hard to test or are trivial. - I had a hard time thinking about a good name for my
HashMap
insidesolve()
method and themapAllMatchingBinaryNumbersToEachBinaryNumberInsideBinaryPuzzleBasedOnSetValues
is downright rediculous. Any suggestions?
public class BinaryPuzzleSolver {
// NOTE: each binary number is an int[]
public boolean solve(BinaryPuzzle binaryPuzzle) {
int[][] binaryPuzzleValues = binaryPuzzle.getValues();
List<int[]> binaryNumbers = generateAllBinaryNumbersWithBitSizeOf(
binaryPuzzleValues.length);
binaryNumbers.removeIf(this::binaryNumberInvalidBecauseOfRules);
HashMap<Integer, List<int[]>> matchingBinaryNumbersForBinaryPuzzleRows =
mapAllMatchingBinaryNumbersToEachBinaryNumberInsideBinaryPuzzleBasedOnSetValues(
binaryNumbers, binaryPuzzleValues);
return solveByTryingOutAllPossibilitiesAndCheckingAgainstBinaryPuzzleRules(
matchingBinaryNumbersForBinaryPuzzleRows, binaryPuzzleValues);
}
List<int[]> generateAllBinaryNumbersWithBitSizeOf(int numberOfBits) {
List<int[]> binaryNumbers = new ArrayList<>();
int largestBinaryNumber = (int) Math.pow(2, numberOfBits);
for (int i = 0; i < largestBinaryNumber; i++) {
int[] binaryNumber = convertDecimalToBinary(i, numberOfBits);
binaryNumbers.add(binaryNumber);
}
return binaryNumbers;
}
private int[] convertDecimalToBinary(int decimalValue, int numberOfBits) {
int[] binaryNumber = new int[numberOfBits];
for (int j = 1; decimalValue != 0; j++) {
binaryNumber[numberOfBits - j] = decimalValue % 2;
decimalValue /= 2;
}
return binaryNumber;
}
boolean binaryNumberInvalidBecauseOfRules(int[] binaryNumber) {
return numberContainsMoreThan2SimilarBitsAfterEachOther(binaryNumber)
|| numberContainsUnequalAmountOfOnesAndZeros(binaryNumber);
}
private HashMap<Integer, List<int[]>> mapAllMatchingBinaryNumbersToEachBinaryNumberInsideBinaryPuzzleBasedOnSetValues(
List<int[]> binaryNumbers, int[][] binaryPuzzleValues) {
HashMap<Integer, List<int[]>> matchingBinaryNumbersForBinaryPuzzleRows = new HashMap<>();
for (int i = 0; i < binaryPuzzleValues.length; i++) {
int[] binaryPuzzleRow = binaryPuzzleValues[i]; // <-- binaryPuzzleRow is also a binaryNumber.
// I think it is easier in this method to think about the binaryNumbers as a row inside the binary puzzle
List<int[]> matchingBinaryNumbersForCurrentBinaryPuzzleRow = findMatchesForBinaryPuzzleRowBasedOnSetValues(
binaryNumbers,
binaryPuzzleRow);
matchingBinaryNumbersForBinaryPuzzleRows
.put(i, matchingBinaryNumbersForCurrentBinaryPuzzleRow);
}
return matchingBinaryNumbersForBinaryPuzzleRows;
}
private List<int[]> findMatchesForBinaryPuzzleRowBasedOnSetValues(List<int[]> binaryNumbers,
int[] binaryPuzzleRow) {
List<int[]> matchingBinaryNumber = new ArrayList<>();
for (int[] array : binaryNumbers) {
boolean isMatch = true;
for (int i = 0; i < binaryPuzzleRow.length; i++) {
if (binaryPuzzleRow[i] != -1) {
if (binaryPuzzleRow[i] != array[i]) {
isMatch = false;
break;
}
}
}
if (isMatch) {
matchingBinaryNumber.add(array);
}
}
return matchingBinaryNumber;
}
private boolean solveByTryingOutAllPossibilitiesAndCheckingAgainstBinaryPuzzleRules(
HashMap<Integer, List<int[]>> matchingBinaryNumbersForBinaryPuzzleRows,
int[][] binaryPuzzleValues) {
int allPossibilities = 1;
for (int i = 0; i < binaryPuzzleValues.length; i++) {
allPossibilities *= matchingBinaryNumbersForBinaryPuzzleRows.get(i).size();
}
System.out.println(
"Amount of possible board configurations to be checked: " + allPossibilities + "\n");
return solve(matchingBinaryNumbersForBinaryPuzzleRows, 0, binaryPuzzleValues); }
// 8 months ago when I first created this I was kissing my fingers, now I'm sitting here thinking wtf is this...
private boolean solve(Map<Integer, List<int[]>> matchingBinaryNumbersForBinaryPuzzleRows,
int mapKey,
int[][] binaryPuzzleValues) {
if (mapKey == matchingBinaryNumbersForBinaryPuzzleRows.size()) {
return verticalBinariesNumberValid(binaryPuzzleValues) && binaryNumbersUnique(
binaryPuzzleValues); // base
}
for (int i = 0; i < matchingBinaryNumbersForBinaryPuzzleRows.get(mapKey).size(); i++) {
binaryPuzzleValues[mapKey] = matchingBinaryNumbersForBinaryPuzzleRows.get(mapKey).get(i);
if (solve(matchingBinaryNumbersForBinaryPuzzleRows, mapKey + 1,
binaryPuzzleValues)) { // recurse
return true;
}
}
return false;
}
private boolean verticalBinariesNumberValid(int[][] values) {
int size = values.length;
int[] verticalBinaryNumber = new int[size];
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
verticalBinaryNumber[j] = values[j][i];
}
if (binaryNumberInvalidBecauseOfRules(verticalBinaryNumber)) {
return false;
}
}
return true;
}
private boolean binaryNumbersUnique(int[][] values) {
return horizontalBinaryNumbersUnique(values) && verticalBinaryNumbersUnique(values);
}
private boolean horizontalBinaryNumbersUnique(int[][] values) {
int size = values.length;
int[] tempBinaryNumber = new int[size];
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
tempBinaryNumber[j] = values[i][j];
}
for (int k = i + 1; k < size; k++) {
if (Arrays.equals(values[k], tempBinaryNumber)) {
return false;
}
}
}
return true;
}
private boolean verticalBinaryNumbersUnique(int[][] values) {
int size = values.length;
int[] tempBinaryNumber = new int[size];
for (int i = 0; i < size; i++) {
int[] tempBinaryNumberVertical = new int[size];
for (int j = 0; j < size; j++) {
tempBinaryNumber[j] = values[j][i];
}
for (int k = i + 1; k < size; k++) {
for (int l = 0; l < size; l++) {
tempBinaryNumberVertical[l] = values[l][k];
}
if (Arrays.equals(tempBinaryNumberVertical, tempBinaryNumber)) {
return false;
}
}
}
return true;
}
boolean numberContainsMoreThan2SimilarBitsAfterEachOther(int[] binaryNumber) {
for (int i = 0; i < binaryNumber.length; i++) {
int currentBit = binaryNumber[i];
int threeInARowCounter = 1;
for (int j = -1; j < 2; j += 2) {
int previousAndNextIndex = i + j;
if (previousAndNextIndex < 0 || previousAndNextIndex > (binaryNumber.length - 1)) {
break;
}
if (currentBit == binaryNumber[previousAndNextIndex]) {
threeInARowCounter++;
}
if (threeInARowCounter == 3) {
return true;
}
}
}
return false;
}
boolean numberContainsUnequalAmountOfOnesAndZeros(int[] binaryNumber) {
int amountOfOnes = 0;
for (int i : binaryNumber) {
if (i == 1) {
amountOfOnes++;
}
}
return amountOfOnes != (binaryNumber.length / 2);
}
}
How to run:
public class BinaryPuzzleSolverTest {
private BinaryPuzzleSolver binaryPuzzleSolver = new BinaryPuzzleSolver();
@Test
public void test() {
// http://binarypuzzle.com/puzzles.php?size=6&level=4&nr=2
BinaryPuzzle binaryPuzzle = new BinaryPuzzle(6);
binaryPuzzle
.setValue(0, 1, 0)
.setValue(2, 0, 1)
.setValue(3, 1, 0)
.setValue(3, 4, 0)
.setValue(4, 0, 1)
.setValue(4, 3, 0)
.setValue(5, 1, 0)
.setValue(5, 5, 0);
System.out.println(binaryPuzzle.toString());
System.out.println(new BinaryPuzzleSolver().solve(binaryPuzzle));
System.out.println(binaryPuzzle.toString());
}
}
Note: Probably too much in 1 post, how should I otherwise split my question? Is it überhaupt a good question?