The Program
I wrote this program as an answer to a question I thought of for Programming Puzzles and Code Golf. To avoid cross-posting, I will only summarize the requirements:
- Solve a game in a 3x4 grid that is similar to the fifteen puzzle, except there are colors instead of numbers, and some colors are duplicated; 4 red, 4 yellow, 2 blue, 1 gray, 1 empty
- The goal is to make the top row exactly match the bottom row.
- Input is in the other thread; basically, input the 12 cells (a permutation of
RRRRYYYYBBG_
in one line - Output the moves (a string of
LRUD
), and then the resulting board.
Why Code Review?
Once I had it working, I decided this would be a really good sort of program for me to get some review on. I cleaned it up and turned it into something that I would be willing to turn into production (there would be some extra error handling, but that part isn't that hard to add in and is boring). There is nothing in particular that I'm looking for; I've worked really hard to polish my programming skills from books like Go4, Refactoring, and Clean Code, and I want to make sure I understand most of those concepts and am applying them correctly. Note: This program is too short for me to bother with a DI framework, but normally I do all my programming beyond a certain complexity level with Guice. Also; this code depends on Guava, but not for all that much, just Preconditions
and the java.util
factory methods.
FluxSolver.java (main class)
package org.durron597.codereview;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.*;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
public class FluxSolver implements Runnable {
public static final String DEFAULT_TEMPLATE = "RRRRYYYYBBG_";
private final Map<String, Board> allBoards = Maps.newHashMap();
private final String template;
private Set<Board> scoredBoards = Sets.newHashSet();
public FluxSolver() {
this(DEFAULT_TEMPLATE);
}
public FluxSolver(String template) {
this.template = template;
}
@Override
public void run() {
computeAllBoards(template);
solveAllBoards();
Scanner c = new Scanner(System.in);
while (c.hasNext()) {
String input = c.nextLine();
printSolution(input);
}
c.close();
}
private void printSolution(String boardString) {
Board currentBoard = allBoards.get(boardString);
while (!currentBoard.isSolved()) {
System.out.print(currentBoard.getDirection().getSymbol());
currentBoard = currentBoard.getParent();
}
System.out.println();
System.out.print(currentBoard);
}
private void computeAllBoards(String template) {
checkArgument(template.length() == Board.X_SIZE * Board.Y_SIZE,
"Board size is the wrong length: %s", template.length());
permuteHelper("", template);
}
private void solveAllBoards() {
for (int score = 0; score < 20; score++) {
Set<Board> nextBoardIteration = Sets.newHashSet(scoredBoards);
for (Board currentBoard : scoredBoards) {
if (currentBoard.getScore() == score) {
for (Direction direction : Direction.values()) {
Board boardAfterMove =
direction.moveSquare(this, currentBoard);
if (discoveredNewBoard(nextBoardIteration,
boardAfterMove)) {
boardAfterMove.setParent(currentBoard, direction);
nextBoardIteration.add(boardAfterMove);
}
}
}
}
scoredBoards = nextBoardIteration;
}
}
private void permuteHelper(String prefix, String remaining) {
int charsLeft = remaining.length();
if (charsLeft == 0) {
Board newBoard = new Board(prefix);
allBoards.put(prefix, newBoard);
if (newBoard.isSolved())
scoredBoards.add(newBoard);
} else {
Set<Character> seenCharacters = new HashSet<>();
for (int i = 0; i < charsLeft; i++) {
char c = remaining.charAt(i);
if (!seenCharacters.contains(c)) {
permuteHelper(prefix + c, remaining.substring(0, i)
+ remaining.substring(i + 1, charsLeft));
seenCharacters.add(c);
}
}
}
}
private boolean discoveredNewBoard(Set<Board> nextBoardIteration,
Board boardAfterMove) {
return boardAfterMove != null
&& !nextBoardIteration.contains(boardAfterMove);
}
public Board getBoardFromRawString(String key) {
return allBoards.get(key);
}
public static void main(String[] a) {
new FluxSolver().run();
}
}
Board.java
package org.durron597.codereview;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class Board {
public static final int X_SIZE = 4;
public static final int Y_SIZE = 3;
private final Board.CellType[][] boardCells = new Board.CellType[Y_SIZE][X_SIZE];
private final Position emptyPosition;
private final String rawString;
private String prettyString = null;
private Board parent;
private Direction direction;
private int score;
Board(String rawString) {
int y, x, raw;
int emptyX = -1, emptyY = -1, emptyRaw = -1;
for (y = 0; y < Y_SIZE; y++) {
for (x = 0; x < X_SIZE; x++) {
raw = calculateRawPosition(y, x);
boardCells[y][x] =
CellType.getTypeFromSymbol(rawString.charAt(raw));
if (boardCells[y][x] == CellType.EMPTY) {
if (emptyX > -1) {
throw new IllegalArgumentException(
"Multiple empty cells detected at position "
+ raw + ": " + rawString);
}
emptyX = x;
emptyY = y;
emptyRaw = raw;
}
}
}
if (emptyX == -1) {
throw new IllegalArgumentException("No empty cell detected!");
} else {
emptyPosition = new Position(emptyX, emptyY, emptyRaw);
}
this.rawString = rawString;
}
public static int calculateRawPosition(int y, int x) {
return X_SIZE * y + x;
}
public Position getEmptyPosition() {
return emptyPosition;
}
public boolean isSolved() {
return Arrays.equals(boardCells[0], boardCells[2]);
}
public void setParent(Board parent, Direction direction) {
this.parent = parent;
this.direction = direction;
this.score = parent.getScore() + 1;
}
@Override
public String toString() {
if (prettyString == null) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < Y_SIZE; i++) {
for (int j = 0; j < X_SIZE; j++) {
builder.append(boardCells[i][j].getSymbol());
}
builder.append(System.lineSeparator());
}
prettyString = builder.toString();
}
return prettyString;
}
public String getRawString() {
return rawString;
}
public int getScore() {
return score;
}
public Board getParent() {
return parent;
}
public Direction getDirection() {
return direction;
}
public static class Position {
public final int x;
public final int y;
public final int raw;
private Position(int x, int y, int raw) {
this.x = x;
this.y = y;
this.raw = raw;
}
}
public static enum CellType {
EMPTY('_'), GREEN('G'), BLUE('B'), RED('R'), YELLOW('Y');
private static Map<Character, CellType> reverseMap = new HashMap<>();
static {
for (CellType type : values()) {
reverseMap.put(type.symbol, type);
}
}
private char symbol;
private CellType(char symbol) {
this.symbol = symbol;
}
public char getSymbol() {
return symbol;
}
public static CellType getTypeFromSymbol(char symbol) {
return reverseMap.get(symbol);
}
}
}
Direction.java
package org.durron597.codereview;
public enum Direction {
RIGHT("R") {
public Board moveSquare(FluxSolver solver, Board input) {
return input.getEmptyPosition().x == Board.X_SIZE - 1 ? null
: moveToEmpty(solver, input, 1);
}
},
LEFT("L") {
public Board moveSquare(FluxSolver solver, Board input) {
return input.getEmptyPosition().x == 0 ? null : moveToEmpty(solver, input,
-1);
}
},
DOWN("D") {
public Board moveSquare(FluxSolver solver, Board input) {
return input.getEmptyPosition().y == Board.Y_SIZE - 1 ? null
: moveToEmpty(solver, input, Board.X_SIZE);
}
},
UP("U") {
public Board moveSquare(FluxSolver solver, Board input) {
return input.getEmptyPosition().y == 0 ? null : moveToEmpty(solver, input,
-Board.X_SIZE);
}
};
private String symbol;
private Direction(String symbol) {
this.symbol = symbol;
}
public abstract Board moveSquare(FluxSolver solver, Board input);
Board moveToEmpty(FluxSolver solver, Board i, int emptyOffset) {
int newPosition = emptyOffset + i.getEmptyPosition().raw;
char[] s = i.getRawString().toCharArray();
s[i.getEmptyPosition().raw] = s[newPosition];
s[newPosition] = Board.CellType.EMPTY.getSymbol();
return solver.getBoardFromRawString(new String(s));
}
public String getSymbol() {
return symbol;
}
}