package puzzle; import static net.gnehzr.tnoodle.utils.GwtSafeUtils.azzert; import static net.gnehzr.tnoodle.utils.GwtSafeUtils.azzertEquals; import net.gnehzr.tnoodle.svglite.Color; import net.gnehzr.tnoodle.svglite.Dimension; import net.gnehzr.tnoodle.svglite.Svg; import net.gnehzr.tnoodle.svglite.Path; import net.gnehzr.tnoodle.svglite.PathIterator; import net.gnehzr.tnoodle.svglite.Point2D; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Random; import java.util.logging.Logger; import puzzle.PyraminxSolver.PyraminxSolverState; import net.gnehzr.tnoodle.scrambles.InvalidScrambleException; import net.gnehzr.tnoodle.scrambles.Puzzle; import net.gnehzr.tnoodle.scrambles.PuzzleStateAndGenerator; import net.gnehzr.tnoodle.utils.GwtSafeUtils; import org.timepedia.exporter.client.Export; @Export public class PyraminxPuzzle extends Puzzle { private static final Logger l = Logger.getLogger(PyraminxPuzzle.class.getName()); private static final int MIN_SCRAMBLE_LENGTH = 11; private static final boolean SCRAMBLE_LENGTH_INCLUDES_TIPS = true; private PyraminxSolver pyraminxSolver = null; public PyraminxPuzzle() { pyraminxSolver = new PyraminxSolver(); wcaMinScrambleDistance = 6; } @Override public PuzzleStateAndGenerator generateRandomMoves(Random r) { PyraminxSolverState state = pyraminxSolver.randomState(r); String scramble = pyraminxSolver.generateExactly(state, MIN_SCRAMBLE_LENGTH, false); PuzzleState pState; try { pState = getSolvedState().applyAlgorithm(scramble); } catch (InvalidScrambleException e) { azzert(false, e); return null; } return new PuzzleStateAndGenerator(pState, scramble); } /************************************************************* * Functions to display the puzzle */ private static final int pieceSize = 30; private static final int gap = 5; private static final HashMap<String, Color> defaultColorScheme = new HashMap<String, Color>(); static { defaultColorScheme.put("F", new Color(0x00FF00)); defaultColorScheme.put("D", new Color(0xFFFF00)); defaultColorScheme.put("L", new Color(0xFF0000)); defaultColorScheme.put("R", new Color(0x0000FF)); } @Override public HashMap<String, Color> getDefaultColorScheme() { return new HashMap<String, Color>(defaultColorScheme); } @Override public Dimension getPreferredSize() { return getImageSize(gap, pieceSize); } private static Dimension getImageSize(int gap, int pieceSize) { return new Dimension(getPyraminxViewWidth(gap, pieceSize), getPyraminxViewHeight(gap, pieceSize)); } private void drawMinx(Svg g, int gap, int pieceSize, Color[] colorScheme, int[][] image) { drawTriangle(g, 2*gap+3*pieceSize, gap+Math.sqrt(3)*pieceSize, true, image[0], pieceSize, colorScheme); drawTriangle(g, 2*gap+3*pieceSize, 2*gap+2*Math.sqrt(3)*pieceSize, false, image[1], pieceSize, colorScheme); drawTriangle(g, gap+1.5*pieceSize, gap+Math.sqrt(3)/2*pieceSize, false, image[2], pieceSize, colorScheme); drawTriangle(g, 3*gap+4.5*pieceSize, gap+Math.sqrt(3)/2*pieceSize, false, image[3], pieceSize, colorScheme); } private void drawTriangle(Svg g, double x, double y, boolean up, int[] state, int pieceSize, Color[] colorScheme) { Path p = triangle(up, pieceSize); p.translate(x, y); double[] xpoints = new double[3]; double[] ypoints = new double[3]; PathIterator iter = p.getPathIterator(); for(int ch = 0; ch < 3; ch++) { double[] coords = new double[6]; int type = iter.currentSegment(coords); if(type == PathIterator.SEG_MOVETO || type == PathIterator.SEG_LINETO) { xpoints[ch] = coords[0]; ypoints[ch] = coords[1]; } iter.next(); } double[] xs = new double[6]; double[] ys = new double[6]; for(int i = 0; i < 3; i++) { xs[i]=1/3.*xpoints[(i+1)%3]+2/3.*xpoints[i]; ys[i]=1/3.*ypoints[(i+1)%3]+2/3.*ypoints[i]; xs[i+3]=2/3.*xpoints[(i+1)%3]+1/3.*xpoints[i]; ys[i+3]=2/3.*ypoints[(i+1)%3]+1/3.*ypoints[i]; } Path[] ps = new Path[9]; for(int i = 0; i < ps.length; i++) { ps[i] = new Path(); } Point2D.Double center = getLineIntersection(xs[0], ys[0], xs[4], ys[4], xs[2], ys[2], xs[3], ys[3]); for(int i = 0; i < 3; i++) { ps[3*i].moveTo(xpoints[i], ypoints[i]); ps[3*i].lineTo(xs[i], ys[i]); ps[3*i].lineTo(xs[3+(2+i)%3], ys[3+(2+i)%3]); ps[3*i].closePath(); ps[3*i+1].moveTo(xs[i], ys[i]); ps[3*i+1].lineTo(xs[3+(i+2)%3], ys[3+(i+2)%3]); ps[3*i+1].lineTo(center.x, center.y); ps[3*i+1].closePath(); ps[3*i+2].moveTo(xs[i], ys[i]); ps[3*i+2].lineTo(xs[i+3], ys[i+3]); ps[3*i+2].lineTo(center.x, center.y); ps[3*i+2].closePath(); } for(int i = 0; i < ps.length; i++) { Path sticker = ps[i]; sticker.setFill(colorScheme[state[i]]); sticker.setStroke(Color.BLACK); g.appendChild(sticker); } } private static Path triangle(boolean pointup, int pieceSize) { int rad = (int)(Math.sqrt(3) * pieceSize); double[] angs = { 7/6., 11/6., .5 }; for(int i = 0; i < angs.length; i++) { if(pointup) { angs[i] += 1/3.; } angs[i] *= Math.PI; } double[] x = new double[angs.length]; double[] y = new double[angs.length]; for(int i = 0; i < x.length; i++) { x[i] = rad * Math.cos(angs[i]); y[i] = rad * Math.sin(angs[i]); } Path p = new Path(); p.moveTo(x[0], y[0]); for(int ch = 1; ch < x.length; ch++) { p.lineTo(x[ch], y[ch]); } p.closePath(); return p; } private static Point2D.Double getLineIntersection(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) { return new Point2D.Double( det(det(x1, y1, x2, y2), x1 - x2, det(x3, y3, x4, y4), x3 - x4)/ det(x1 - x2, y1 - y2, x3 - x4, y3 - y4), det(det(x1, y1, x2, y2), y1 - y2, det(x3, y3, x4, y4), y3 - y4)/ det(x1 - x2, y1 - y2, x3 - x4, y3 - y4)); } private static double det(double a, double b, double c, double d) { return a * d - b * c; } private static int getPyraminxViewWidth(int gap, int pieceSize) { return (2 * 3 * pieceSize + 4 * gap); } private static int getPyraminxViewHeight(int gap, int pieceSize) { return (int)(2 * 1.5 * Math.sqrt(3) * pieceSize + 3 * gap); } private static int getNewUnitSize(int width, int height, int gap, String variation) { return (int) Math.round(Math.min((width - 4*gap) / (3 * 2), (height - 3*gap) / (3 * Math.sqrt(3)))); } private static Path getTriangle(double x, double y, int pieceSize, boolean up) { Path p = triangle(up, pieceSize); p.translate(x, y); return p; } @Override public String getLongName() { return "Pyraminx"; } @Override public String getShortName() { return "pyram"; } @Override public PuzzleState getSolvedState() { return new PyraminxState(); } @Override protected int getRandomMoveCount() { return 15; } public class PyraminxState extends PuzzleState { private int[][] image; /** Trying to make an ascii art of the pyraminx stickers position... * * U * ____ ____ ____ ____ ____ ____ * \ /\ /\ / /\ \ /\ /\ / * \0 /1 \2 /4 \3 / /0 \ \0 /1 \2 /4 \3 / * \/____\/____\/ /____\ \/____\/____\/ * \ /\ / /\ /\ \ /\ / * face 2 \8 /7 \5 / /8 \1 /2 \ \8 /7 \5 / face 3 * \/____\/ /____\/____\ \/____\/ * \ / /\ /\ /\ \ / * \6 / /6 \7 /5 \4 /3 \ \6 / * \/ /____\/____\/____\ \/ * face 0 * L ____ ____ ____ R * \ /\ /\ / * \0 /1 \2 /4 \3 / * \/____\/____\/ * \ /\ / * \8 /7 \5 / * face 1 \/____\/ * \ / * \6 / * \/ * * B */ public PyraminxState() { image = new int[4][9]; for(int i = 0; i < image.length; i++) { for(int j = 0; j < image[0].length; j++) { image[i][j] = i; } } } public PyraminxState(int[][] image) { this.image = image; } private void turn(int side, int dir, int[][] image) { for(int i = 0; i < dir; i++) { turn(side, image); } } private void turnTip(int side, int dir, int[][] image) { for(int i = 0; i < dir; i++) { turnTip(side, image); } } private void turn(int s, int[][] image) { switch(s) { case 0: swap(0, 8, 3, 8, 2, 2, image); swap(0, 1, 3, 1, 2, 4, image); swap(0, 2, 3, 2, 2, 5, image); break; case 1: swap(2, 8, 1, 2, 0, 8, image); swap(2, 7, 1, 1, 0, 7, image); swap(2, 5, 1, 8, 0, 5, image); break; case 2: swap(3, 8, 0, 5, 1, 5, image); swap(3, 7, 0, 4, 1, 4, image); swap(3, 5, 0, 2, 1, 2, image); break; case 3: swap(1, 8, 2, 2, 3, 5, image); swap(1, 7, 2, 1, 3, 4, image); swap(1, 5, 2, 8, 3, 2, image); break; default: azzert(false); } turnTip(s, image); } private void turnTip(int s, int[][] image) { switch(s) { case 0: swap(0, 0, 3, 0, 2, 3, image); break; case 1: swap(0, 6, 2, 6, 1, 0, image); break; case 2: swap(0, 3, 1, 3, 3, 6, image); break; case 3: swap(1, 6, 2, 0, 3, 3, image); break; default: azzert(false); } } private void swap(int f1, int s1, int f2, int s2, int f3, int s3, int[][] image) { int temp = image[f1][s1]; image[f1][s1] = image[f2][s2]; image[f2][s2] = image[f3][s3]; image[f3][s3] = temp; } public PyraminxSolverState toPyraminxSolverState() { PyraminxSolverState state = new PyraminxSolverState(); /** Each face color is assigned a value so that the sum of the color (minus 1) of each edge gives a unique integer. * These edge values match the edge numbering in the PyraminxSolver class, making the following code simpler. * U * ____ ____ ____ ____ ____ ____ * \ /\ /\ / /\ \ /\ /\ / * \ / \5 / \ / / \ \ / \5 / \ / * \/____\/____\/ /____\ \/____\/____\/ * \ /\ / /\ /\ \ /\ / * face +2 \2 / \1 / /1 \ /3 \ \3 / \4 / face +4 * \/____\/ /____\/____\ \/____\/ * \ / /\ /\ /\ \ / * \ / / \ /0 \ / \ \ / * \/ /____\/____\/____\ \/ * face +0 * L ____ ____ ____ R * \ /\ /\ / * \ / \0 / \ / * \/____\/____\/ * \ /\ / * \2 / \4 / * face +1 \/____\/ * \ / * \ / * \/ * * B */ int[][] stickersToEdges = new int[][] { { image[0][5], image[1][2] }, { image[0][8], image[2][5] }, { image[1][8], image[2][8] }, { image[0][2], image[3][8] }, { image[1][5], image[3][5] }, { image[2][2], image[3][2] } }; int[] colorToValue = new int[] {0, 1, 2, 4}; int[] edges = new int[6]; for (int i = 0; i < edges.length; i++){ edges[i] = colorToValue[stickersToEdges[i][0]] + colorToValue[stickersToEdges[i][1]] - 1; // In the PyraminxSolver class, the primary facelet of each edge correspond to the lowest face number. if( stickersToEdges[i][0] > stickersToEdges[i][1] ) { edges[i] += 8; } } state.edgePerm = PyraminxSolver.packEdgePerm(edges); state.edgeOrient = PyraminxSolver.packEdgeOrient(edges); int[][] stickersToCorners = new int[][] { { image[0][1], image[2][4], image[3][1] }, { image[0][7], image[1][1], image[2][7] }, { image[0][4], image[3][7], image[1][4] }, { image[1][7], image[3][4], image[2][1] } }; /* The corners are supposed to be fixed, so we are also checking if they are in the right place. * We can use the sum trick, but here, no need for transition table :) */ int[] correctSum = new int[] {5, 3, 4, 6}; int[] corners = new int[4]; for (int i = 0; i < corners.length; i++){ azzertEquals(stickersToCorners[i][0] + stickersToCorners[i][1] + stickersToCorners[i][2], correctSum[i]); // The following code is not pretty, sorry... if(( stickersToCorners[i][0] < stickersToCorners[i][1] ) && ( stickersToCorners[i][0] < stickersToCorners[i][2] )) { corners[i] = 0; } if(( stickersToCorners[i][1] < stickersToCorners[i][0] ) && ( stickersToCorners[i][1] < stickersToCorners[i][2] )) { corners[i] = 1; } if(( stickersToCorners[i][2] < stickersToCorners[i][1] ) && ( stickersToCorners[i][2] < stickersToCorners[i][0] )) { corners[i] = 2; } } state.cornerOrient = PyraminxSolver.packCornerOrient(corners); /* For the tips, we use the same numbering */ int[][] stickersToTips = new int[][] { { image[0][0], image[2][3], image[3][0] }, { image[0][6], image[1][0], image[2][6] }, { image[0][3], image[3][6], image[1][3] }, { image[1][6], image[3][3], image[2][0] } }; int[] tips = new int[4]; for (int i = 0; i < tips.length; i++){ int[] stickers = stickersToTips[i]; // We can use the same color check as for the corners. azzertEquals(stickers[0] + stickers[1] + stickers[2], correctSum[i]); // For the tips, we don't have to check colors against face, but against the attached corner. int cornerPrimaryColor = stickersToCorners[i][0]; int clockwiseTurnsToMatchCorner = 0; while(stickers[clockwiseTurnsToMatchCorner] != cornerPrimaryColor) { clockwiseTurnsToMatchCorner++; azzert(clockwiseTurnsToMatchCorner < 3); } tips[i] = clockwiseTurnsToMatchCorner; } state.tips = PyraminxSolver.packCornerOrient(tips); // Same function as for corners. return state; } @Override public String solveIn(int n) { return pyraminxSolver.solveIn(toPyraminxSolverState(), n, SCRAMBLE_LENGTH_INCLUDES_TIPS); } @Override public LinkedHashMap<String, PuzzleState> getSuccessorsByName() { LinkedHashMap<String, PuzzleState> successors = new LinkedHashMap<String, PuzzleState>(); String axes = "ulrb"; for(int axis = 0; axis < axes.length(); axis++) { for(boolean tip : new boolean[] { true, false }) { char face = axes.charAt(axis); face = tip ? Character.toLowerCase(face) : Character.toUpperCase(face); for(int dir = 1; dir <= 2; dir++) { String turn = "" + face; if(dir == 2) { turn += "'"; } int[][] imageCopy = new int[image.length][image[0].length]; GwtSafeUtils.deepCopy(image, imageCopy); if(tip) { turnTip(axis, dir, imageCopy); } else { turn(axis, dir, imageCopy); } successors.put(turn, new PyraminxState(imageCopy)); } } } return successors; } @Override public boolean equals(Object other) { // Sure this could blow up with a cast exception, but shouldn't it? =) return Arrays.deepEquals(image, ((PyraminxState) other).image); } @Override public int hashCode() { return Arrays.deepHashCode(image); } @Override protected Svg drawScramble(HashMap<String, Color> colorScheme) { Dimension preferredSize = getPreferredSize(); Svg svg = new Svg(preferredSize); svg.setStroke(2, 10, "round"); Color[] scheme = new Color[4]; for(int i = 0; i < scheme.length; i++) { scheme[i] = colorScheme.get("FDLR".charAt(i)+""); } drawMinx(svg, gap, pieceSize, scheme, image); return svg; } } }