package puzzle;
import static net.gnehzr.tnoodle.utils.GwtSafeUtils.azzert;
import net.gnehzr.tnoodle.svglite.Color;
import net.gnehzr.tnoodle.svglite.Dimension;
import net.gnehzr.tnoodle.svglite.Rectangle;
import net.gnehzr.tnoodle.svglite.Svg;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import net.gnehzr.tnoodle.scrambles.Puzzle;
import net.gnehzr.tnoodle.utils.GwtSafeUtils;
import puzzle.TwoByTwoSolver.TwoByTwoState;
import org.timepedia.exporter.client.Export;
@Export
public class CubePuzzle extends Puzzle {
public static enum Face {
R, U, F, L, D, B;
public Face oppositeFace() {
return values()[(ordinal() + 3) % 6];
}
}
private static final String[] DIR_TO_STR = new String[] { null, "", "2", "'" };
private static HashMap<Face, String> faceRotationsByName = new HashMap<Face, String>();
static {
faceRotationsByName.put(Face.R, "x");
faceRotationsByName.put(Face.U, "y");
faceRotationsByName.put(Face.F, "z");
}
public class CubeMove {
Face face;
int dir;
int innerSlice, outerSlice;
public CubeMove(Face face, int dir) {
this(face, dir, 0);
}
public CubeMove(Face face, int dir, int innerSlice) {
this(face, dir, innerSlice, 0);
}
public CubeMove(Face face, int dir, int innerSlice, int outerSlice) {
this.face = face;
this.dir = dir;
this.innerSlice = innerSlice;
this.outerSlice = outerSlice;
// We haven't come up with names for moves where outerSlice != 0
azzert(outerSlice == 0);
}
public String toString() {
String f = face.toString();
String move;
if(innerSlice == 0) {
move = f;
} else if (innerSlice == 1) {
move = f + "w";
} else if (innerSlice == size - 1) {
// Turning all the slices is a rotation
String rotationName = faceRotationsByName.get(face);
if(rotationName == null) {
// Not all rotations are actually named.
return null;
}
move = rotationName;
} else {
move = (innerSlice+1) + f + "w";
}
move += DIR_TO_STR[dir];
return move;
}
}
private static final int gap = 2;
private static final int cubieSize = 10;
private static final int[] DEFAULT_LENGTHS = { 0, 0, 25, 25, 40, 60, 80, 100, 120, 140, 160, 180 };
protected final int size;
protected CubeMove[][] getRandomOrientationMoves(int thickness) {
CubeMove[] randomUFaceMoves = new CubeMove[] {
null,
new CubeMove(Face.R, 1, thickness),
new CubeMove(Face.R, 2, thickness),
new CubeMove(Face.R, 3, thickness),
new CubeMove(Face.F, 1, thickness),
new CubeMove(Face.F, 3, thickness)
};
CubeMove[] randomFFaceMoves = new CubeMove[] {
null,
new CubeMove(Face.U, 1, thickness),
new CubeMove(Face.U, 2, thickness),
new CubeMove(Face.U, 3, thickness)
};
CubeMove[][] randomOrientationMoves = new CubeMove[randomUFaceMoves.length * randomFFaceMoves.length][];
int i = 0;
for(CubeMove randomUFaceMove : randomUFaceMoves) {
for(CubeMove randomFFaceMove : randomFFaceMoves) {
ArrayList<CubeMove> moves = new ArrayList<CubeMove>();
if(randomUFaceMove != null) {
moves.add(randomUFaceMove);
}
if(randomFFaceMove != null) {
moves.add(randomFFaceMove);
}
CubeMove[] movesArr = moves.toArray(new CubeMove[moves.size()]);
randomOrientationMoves[i++] = movesArr;
}
}
return randomOrientationMoves;
}
public CubePuzzle(int size) {
azzert(size >= 0 && size < DEFAULT_LENGTHS.length, "Invalid cube size");
this.size = size;
}
@Override
public String getLongName() {
return size + "x" + size + "x" + size;
}
@Override
public String getShortName() {
return size + "" + size + "" + size;
}
private static void swap(int[][][] image,
int f1, int x1, int y1,
int f2, int x2, int y2,
int f3, int x3, int y3,
int f4, int x4, int y4,
int dir) {
if (dir == 1) {
int temp = image[f1][x1][y1];
image[f1][x1][y1] = image[f2][x2][y2];
image[f2][x2][y2] = image[f3][x3][y3];
image[f3][x3][y3] = image[f4][x4][y4];
image[f4][x4][y4] = temp;
} else if (dir == 2) {
int temp = image[f1][x1][y1];
image[f1][x1][y1] = image[f3][x3][y3];
image[f3][x3][y3] = temp;
temp = image[f2][x2][y2];
image[f2][x2][y2] = image[f4][x4][y4];
image[f4][x4][y4] = temp;
} else if (dir == 3) {
int temp = image[f4][x4][y4];
image[f4][x4][y4] = image[f3][x3][y3];
image[f3][x3][y3] = image[f2][x2][y2];
image[f2][x2][y2] = image[f1][x1][y1];
image[f1][x1][y1] = temp;
} else {
azzert(false);
}
}
private static void slice(Face face, int slice, int dir, int[][][] image) {
int size = image[0].length;
azzert(slice >= 0 && slice < size);
Face sface = face;
int sslice = slice;
int sdir = dir;
if(face != Face.L && face != Face.D && face != Face.B) {
sface = face.oppositeFace();
sslice = size - 1 - slice;
sdir = 4 - dir;
}
for(int j = 0; j < size; j++) {
if(sface == Face.L) {
swap(image,
Face.U.ordinal(), j, sslice,
Face.B.ordinal(), size-1-j, size-1-sslice,
Face.D.ordinal(), j, sslice,
Face.F.ordinal(), j, sslice,
sdir);
} else if(sface == Face.D) {
swap(image,
Face.L.ordinal(), size-1-sslice, j,
Face.B.ordinal(), size-1-sslice, j,
Face.R.ordinal(), size-1-sslice, j,
Face.F.ordinal(), size-1-sslice, j,
sdir);
} else if(sface == Face.B) {
swap(image,
Face.U.ordinal(), sslice, j,
Face.R.ordinal(), j, size-1-sslice,
Face.D.ordinal(), size-1-sslice, size-1-j,
Face.L.ordinal(), size-1-j, sslice,
sdir);
} else {
azzert(false);
}
}
if(slice == 0 || slice == size - 1) {
int f;
if(slice == 0) {
f = face.ordinal();
sdir = 4 - dir;
} else if(slice == size - 1) {
f = face.oppositeFace().ordinal();
sdir = dir;
} else {
azzert(false);
return;
}
for(int j = 0; j < (size+1)/2; j++) {
for(int k = 0; k < size/2; k++) {
swap(image,
f, j, k,
f, k, size-1-j,
f, size-1-j, size-1-k,
f, size-1-k, j,
sdir);
}
}
}
}
private static HashMap<String, Color> defaultColorScheme = new HashMap<String, Color>();
static {
defaultColorScheme.put("B", Color.BLUE);
defaultColorScheme.put("D", Color.YELLOW);
defaultColorScheme.put("F", Color.GREEN);
defaultColorScheme.put("L", new Color(255, 128, 0)); //orange heraldic tincture
defaultColorScheme.put("R", Color.RED);
defaultColorScheme.put("U", Color.WHITE);
}
@Override
public HashMap<String, Color> getDefaultColorScheme() {
return new HashMap<String, Color>(defaultColorScheme);
}
@Override
public Dimension getPreferredSize() {
return getImageSize(gap, cubieSize, size);
}
private static int getCubeViewWidth(int cubie, int gap, int size) {
return (size*cubie + gap)*4 + gap;
}
private static int getCubeViewHeight(int cubie, int gap, int size) {
return (size*cubie + gap)*3 + gap;
}
private static Dimension getImageSize(int gap, int unitSize, int size) {
return new Dimension(getCubeViewWidth(unitSize, gap, size), getCubeViewHeight(unitSize, gap, size));
}
private void drawCube(Svg g, int[][][] state, int gap, int cubieSize, HashMap<String, Color> colorScheme) {
paintCubeFace(g, gap, 2*gap+size*cubieSize, size, cubieSize, state[Face.L.ordinal()], colorScheme);
paintCubeFace(g, 2*gap+size*cubieSize, 3*gap+2*size*cubieSize, size, cubieSize, state[Face.D.ordinal()], colorScheme);
paintCubeFace(g, 4*gap+3*size*cubieSize, 2*gap+size*cubieSize, size, cubieSize, state[Face.B.ordinal()], colorScheme);
paintCubeFace(g, 3*gap+2*size*cubieSize, 2*gap+size*cubieSize, size, cubieSize, state[Face.R.ordinal()], colorScheme);
paintCubeFace(g, 2*gap+size*cubieSize, gap, size, cubieSize, state[Face.U.ordinal()], colorScheme);
paintCubeFace(g, 2*gap+size*cubieSize, 2*gap+size*cubieSize, size, cubieSize, state[Face.F.ordinal()], colorScheme);
}
private void paintCubeFace(Svg g, int x, int y, int size, int cubieSize, int[][] faceColors, HashMap<String, Color> colorScheme) {
for(int row = 0; row < size; row++) {
for(int col = 0; col < size; col++) {
int tempx = x + col*cubieSize;
int tempy = y + row*cubieSize;
Rectangle rect = new Rectangle(tempx, tempy, cubieSize, cubieSize);
rect.setFill(colorScheme.get(Face.values()[faceColors[row][col]].toString()));
rect.setStroke(Color.BLACK);
g.appendChild(rect);
}
}
}
@Override
public CubeState getSolvedState() {
return new CubeState();
}
@Override
protected int getRandomMoveCount() {
return DEFAULT_LENGTHS[size];
}
private int[][][] cloneImage(int[][][] image) {
int[][][] imageCopy = new int[image.length][image[0].length][image[0][0].length];
GwtSafeUtils.deepCopy(image, imageCopy);
return imageCopy;
}
private void spinCube(int[][][] image, Face face, int dir) {
for(int slice = 0; slice < size; slice++) {
slice(face, slice, dir, image);
}
}
private int[][][] normalize(int[][][] image) {
image = cloneImage(image);
int spins = 0;
while (!isNormalized(image)) {
azzert(spins < 2);
int[][] stickersByPiece = getStickersByPiece(image);
int goal = 0;
goal |= 1 << Face.B.ordinal();
goal |= 1 << Face.L.ordinal();
goal |= 1 << Face.D.ordinal();
int idx = -1;
for (int i = 0; i < stickersByPiece.length; i++) {
int t = 0;
for (int j = 0; j < stickersByPiece[i].length; j++) {
t |= 1 << stickersByPiece[i][j];
}
if (t == goal) {
idx = i;
break;
}
}
azzert(idx >= 0);
Face f = null;
int dir = 1;
if (stickersByPiece[idx][0] == Face.D.ordinal()) {
if (idx < 4) {
// on U
f = Face.F;
dir = 2;
} else {
// on D
f = Face.U;
switch(idx) {
case 4:
dir = 2; break;
case 5:
dir = 1; break;
case 6:
dir = 3; break;
default:
azzert(false);
}
}
} else if (stickersByPiece[idx][1] == Face.D.ordinal()) {
switch (idx) {
case 0: case 6:
f = Face.F; break; // on R
case 1: case 4:
f = Face.L; break; // on F
case 2: case 7:
f = Face.R; break; // on B
case 3: case 5:
f = Face.B; break; // on L
default:
azzert(false);
}
} else {
switch (idx) {
case 2: case 4:
f = Face.F; break; // on R
case 0: case 5:
f = Face.L; break; // on F
case 3: case 6:
f = Face.R; break; // on B
case 1: case 7:
f = Face.B; break; // on L
default:
azzert(false);
}
}
spinCube(image, f, dir);
spins++;
}
return image;
}
private boolean isNormalized(int[][][] image) {
// A CubeState is normalized if the BLD piece is solved
return image[Face.B.ordinal()][size-1][size-1] == Face.B.ordinal() &&
image[Face.L.ordinal()][size-1][0] == Face.L.ordinal() &&
image[Face.D.ordinal()][size-1][0] == Face.D.ordinal();
}
protected static int[][] getStickersByPiece(int[][][] img) {
int s = img[0].length - 1;
return new int[][] {
{ img[Face.U.ordinal()][s][s], img[Face.R.ordinal()][0][0], img[Face.F.ordinal()][0][s] },
{ img[Face.U.ordinal()][s][0], img[Face.F.ordinal()][0][0], img[Face.L.ordinal()][0][s] },
{ img[Face.U.ordinal()][0][s], img[Face.B.ordinal()][0][0], img[Face.R.ordinal()][0][s] },
{ img[Face.U.ordinal()][0][0], img[Face.L.ordinal()][0][0], img[Face.B.ordinal()][0][s] },
{ img[Face.D.ordinal()][0][s], img[Face.F.ordinal()][s][s], img[Face.R.ordinal()][s][0] },
{ img[Face.D.ordinal()][0][0], img[Face.L.ordinal()][s][s], img[Face.F.ordinal()][s][0] },
{ img[Face.D.ordinal()][s][s], img[Face.R.ordinal()][s][s], img[Face.B.ordinal()][s][0] },
{ img[Face.D.ordinal()][s][0], img[Face.B.ordinal()][s][s], img[Face.L.ordinal()][s][0] }
};
}
public class CubeState extends PuzzleState {
private final int[][][] image;
private CubeState normalizedState = null;
public CubeState() {
image = new int[6][size][size];
for(int face = 0; face < image.length; face++) {
for(int j = 0; j < size; j++) {
for(int k = 0; k < size; k++) {
image[face][j][k] = face;
}
}
}
normalizedState = this;
}
public CubeState(int[][][] image) {
this.image = image;
}
public boolean isNormalized() {
return CubePuzzle.this.isNormalized(image);
}
public CubeState getNormalized() {
if(normalizedState == null) {
int[][][] normalizedImage = normalize(image);
normalizedState = new CubeState(normalizedImage);
}
return normalizedState;
}
public TwoByTwoState toTwoByTwoState() {
TwoByTwoState state = new TwoByTwoState();
int[][] stickersByPiece = getStickersByPiece(image);
// Here's a clever color value assigning system that gives each piece
// a unique id just by summing up the values of its stickers.
//
// +----------+
// |*3* *2*|
// | U (0) |
// |*1* *0*|
// +----------+----------+----------+----------+
// | 3 1 | 1 0 | 0 2 | 2 3 |
// | L (1) | F (0) | R (0) | B (2) |
// | 7 5 | 5 4 | 4 6 | 6 7 |
// +----------+----------+----------+----------+
// |*5* *4*|
// | D (4) |
// |*7* *6*|
// +----------+
//
int dColor = stickersByPiece[7][0];
int bColor = stickersByPiece[7][1];
int lColor = stickersByPiece[7][2];
int uColor = Face.values()[dColor].oppositeFace().ordinal();
int fColor = Face.values()[bColor].oppositeFace().ordinal();
int rColor = Face.values()[lColor].oppositeFace().ordinal();
int[] colorToVal = new int[8];
colorToVal[uColor] = 0;
colorToVal[fColor] = 0;
colorToVal[rColor] = 0;
colorToVal[lColor] = 1;
colorToVal[bColor] = 2;
colorToVal[dColor] = 4;
int[] pieces = new int[7];
for(int i = 0; i < pieces.length; i++) {
int[] stickers = stickersByPiece[i];
int pieceVal = colorToVal[stickers[0]] + colorToVal[stickers[1]] + colorToVal[stickers[2]];
int clockwiseTurnsToGetToPrimaryColor = 0;
while(stickers[clockwiseTurnsToGetToPrimaryColor] != uColor && stickers[clockwiseTurnsToGetToPrimaryColor] != dColor) {
clockwiseTurnsToGetToPrimaryColor++;
azzert(clockwiseTurnsToGetToPrimaryColor < 3);
}
int piece = (clockwiseTurnsToGetToPrimaryColor << 3) + pieceVal;
pieces[i] = piece;
}
state.permutation = TwoByTwoSolver.packPerm(pieces);
state.orientation = TwoByTwoSolver.packOrient(pieces);
return state;
}
public String toFaceCube() {
azzert(size == 3);
String state = "";
for(char f : "URFDLB".toCharArray()) {
Face face = Face.valueOf("" + f);
int[][] faceArr = image[face.ordinal()];
for(int i = 0; i < faceArr.length; i++) {
for(int j = 0; j < faceArr[i].length; j++) {
state += Face.values()[faceArr[i][j]].toString();
}
}
}
return state;
}
@Override
public LinkedHashMap<String, CubeState> getSuccessorsByName() {
return getSuccessorsWithinSlice(size - 1, true);
}
@Override
public HashMap<String, CubeState> getScrambleSuccessors() {
return getSuccessorsWithinSlice((int) (size / 2) - 1, false);
}
@Override
public HashMap<? extends PuzzleState, String> getCanonicalMovesByState() {
return GwtSafeUtils.reverseHashMap(getScrambleSuccessors());
}
private LinkedHashMap<String, CubeState> getSuccessorsWithinSlice(int maxSlice, boolean includeRedundant) {
LinkedHashMap<String, CubeState> successors = new LinkedHashMap<String, CubeState>();
for(int innerSlice = 0; innerSlice <= maxSlice; innerSlice++) {
for(Face face : Face.values()) {
boolean halfOfEvenCube = size % 2 == 0 && (innerSlice == (size / 2) - 1);
if(!includeRedundant && face.ordinal() >= 3 && halfOfEvenCube) {
// Skip turning the other halves of even sized cubes
continue;
}
int outerSlice = 0;
for(int dir = 1; dir <= 3; dir++) {
CubeMove move = new CubeMove(face, dir, innerSlice, outerSlice);
String moveStr = move.toString();
if(moveStr == null) {
// Skip unnamed rotations.
continue;
}
int[][][] imageCopy = cloneImage(image);
for(int slice = outerSlice; slice <= innerSlice; slice++) {
slice(face, slice, dir, imageCopy);
}
successors.put(moveStr, new CubeState(imageCopy));
}
}
}
return successors;
}
@Override
public boolean equals(Object other) {
return Arrays.deepEquals(image, ((CubeState) other).image);
}
@Override
public int hashCode() {
return Arrays.deepHashCode(image);
}
protected Svg drawScramble(HashMap<String, Color> colorScheme) {
Svg svg = new Svg(getPreferredSize());
drawCube(svg, image, gap, cubieSize, colorScheme);
return svg;
}
}
}