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.Svg; import net.gnehzr.tnoodle.svglite.PathIterator; import net.gnehzr.tnoodle.svglite.Path; import net.gnehzr.tnoodle.svglite.Point2D; import net.gnehzr.tnoodle.svglite.Text; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Random; 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 MegaminxPuzzle extends Puzzle { private static enum Face { U, BL, BR, R, F, L, D, DR, DBR, B, DBL, DL; // TODO We could rename faces so we can just do +6 mod 12 here instead. public Face oppositeFace() { switch(this) { case U: return D; case BL: return DR; case BR: return DL; case R: return DBL; case F: return B; case L: return DBR; case D: return U; case DR: return BL; case DBR: return L; case B: return F; case DBL: return R; case DL: return BR; default: azzert(false); return null; } } } private static final int gap = 2; private static final int minxRad = 30; public MegaminxPuzzle() {} @Override public String getLongName() { return "Megaminx"; } @Override public String getShortName() { return "minx"; } @Override public Dimension getPreferredSize() { return getImageSize(gap, minxRad, null); } private static final double UNFOLDHEIGHT = 2 + 3 * Math.sin(.3 * Math.PI) + Math.sin(.1 * Math.PI); private static final double UNFOLDWIDTH = 4 * Math.cos(.1 * Math.PI) + 2 * Math.cos(.3 * Math.PI); private static void turn(int[][] image, Face side, int dir) { dir = GwtSafeUtils.modulo(dir, 5); for(int i = 0; i < dir; i++) { turn(image, side); } } private static void turn(int[][] image, Face face) { int s = face.ordinal(); int b = (s >= 6 ? 6 : 0); switch(s % 6) { case 0: swapOnSide(image, b, 1, 6, 5, 4, 4, 2, 3, 0, 2, 8); break; case 1: swapOnSide(image, b, 0, 0, 2, 0, 9, 6, 10, 6, 5, 2); break; case 2: swapOnSide(image, b, 0, 2, 3, 2, 8, 4, 9, 4, 1, 4); break; case 3: swapOnSide(image, b, 0, 4, 4, 4, 7, 2, 8, 2, 2, 6); break; case 4: swapOnSide(image, b, 0, 6, 5, 6, 11, 0, 7, 0, 3, 8); break; case 5: swapOnSide(image, b, 0, 8, 1, 8, 10, 8, 11, 8, 4, 0); break; default: azzert(false); } rotateFace(image, face); } private static void swapOnSide(int[][] image, int b, int f1, int s1, int f2, int s2, int f3, int s3, int f4, int s4, int f5, int s5) { for(int i = 0; i < 3; i++) { int temp = image[(f1+b)%12][(s1+i)%10]; image[(f1+b)%12][(s1+i)%10] = image[(f2+b)%12][(s2+i)%10]; image[(f2+b)%12][(s2+i)%10] = image[(f3+b)%12][(s3+i)%10]; image[(f3+b)%12][(s3+i)%10] = image[(f4+b)%12][(s4+i)%10]; image[(f4+b)%12][(s4+i)%10] = image[(f5+b)%12][(s5+i)%10]; image[(f5+b)%12][(s5+i)%10] = temp; } } private static void swapOnFace(int[][] image, Face face, int s1, int s2, int s3, int s4, int s5) { int f = face.ordinal(); int temp = image[f][s1]; image[f][s1] = image[f][s2]; image[f][s2] = image[f][s3]; image[f][s3] = image[f][s4]; image[f][s4] = image[f][s5]; image[f][s5] = temp; } private static void rotateFace(int[][] image, Face f) { swapOnFace(image, f, 0, 8, 6, 4, 2); swapOnFace(image, f, 1, 9, 7, 5, 3); } private static void bigTurn(int[][] image, Face side, int dir) { dir = GwtSafeUtils.modulo(dir, 5); for(int i = 0; i < dir; i++) { bigTurn(image, side); } } private static void bigTurn(int[][] image, Face f) { if(f == Face.DBR) { for(int i = 0; i < 7; i++) { swap(image, 0, (1+i)%10, 4, (3+i)%10, 11, (1+i)%10, 10, (1+i)%10, 1, (1+i)%10); } swapCenters(image, 0, 4, 11, 10, 1); swapWholeFace(image, 2, 0, 3, 0, 7, 0, 6, 8, 9, 8); rotateFace(image, Face.DBR); } else { azzert(f == Face.D); for(int i = 0; i < 7; i++) { swap(image, 1, (9+i)%10, 2, (1+i)%10, 3, (3+i)%10, 4, (5+i)%10, 5, (7+i)%10); } swapCenters(image, 1, 2, 3, 4, 5); swapWholeFace(image, 11, 0, 10, 8, 9, 6, 8, 4, 7, 2); rotateFace(image, Face.D); } } private static void swap(int[][] image, int f1, int s1, int f2, int s2, int f3, int s3, int f4, int s4, int f5, int s5) { int temp = image[f1][s1]; image[f1][s1] = image[f2][s2]; image[f2][s2] = image[f3][s3]; image[f3][s3] = image[f4][s4]; image[f4][s4] = image[f5][s5]; image[f5][s5] = temp; } private static void swapCenters(int[][] image, int f1, int f2, int f3, int f4, int f5) { swap(image, f1, 10, f2, 10, f3, 10, f4, 10, f5, 10); } private static void swapWholeFace(int[][] image, int f1, int s1, int f2, int s2, int f3, int s3, int f4, int s4, int f5, int s5) { for(int i = 0; i < 10; i++) { int temp = image[(f1)%12][(s1+i)%10]; image[(f1)%12][(s1+i)%10] = image[(f2)%12][(s2+i)%10]; image[(f2)%12][(s2+i)%10] = image[(f3)%12][(s3+i)%10]; image[(f3)%12][(s3+i)%10] = image[(f4)%12][(s4+i)%10]; image[(f4)%12][(s4+i)%10] = image[(f5)%12][(s5+i)%10]; image[(f5)%12][(s5+i)%10] = temp; } swapCenters(image, f1, f2, f3, f4, f5); } @Override public HashMap<String, Color> getDefaultColorScheme() { HashMap<String, Color> colors = new HashMap<String, Color>(); colors.put("U", new Color(0xffffff)); colors.put("BL", new Color(0xffcc00)); colors.put("BR", new Color(0x0000b3)); colors.put("R", new Color(0xdd0000)); colors.put("F", new Color(0x006600)); colors.put("L", new Color(0x8a1aff)); colors.put("D", new Color(0x999999)); colors.put("DR", new Color(0xffffb3)); colors.put("DBR", new Color(0xff99ff)); colors.put("B", new Color(0x71e600)); colors.put("DBL", new Color(0xff8433)); colors.put("DL", new Color(0x88ddff)); return colors; } private static Dimension getImageSize(int gap, int minxRad, String variation) { return new Dimension(getMegaminxViewWidth(gap, minxRad), getMegaminxViewHeight(gap, minxRad)); } private static int getMegaminxViewWidth(int gap, int minxRad) { return (int)(UNFOLDWIDTH * 2 * minxRad + 3 * gap); } private static int getMegaminxViewHeight(int gap, int minxRad) { return (int)(UNFOLDHEIGHT * minxRad + 2 * gap); } private static Path pentagon(boolean pointup, int minxRad) { double[] angs = { 1.3, 1.7, .1, .5, .9 }; for(int i = 0; i < angs.length; i++) { if(pointup) { angs[i] -= .2; } 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] = minxRad * Math.cos(angs[i]); y[i] = minxRad * 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.lineTo(x[0], y[0]); // TODO - this is retarded, why do i need to do this? it would appear that closePath() isn't doing it's job 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 Path getPentagon(double x, double y, boolean up, int minxRad) { Path p = pentagon(up, minxRad); p.translate(x, y); return p; } double x = minxRad*Math.sqrt(2*(1-Math.cos(.6*Math.PI))); double a = minxRad*Math.cos(.1*Math.PI); double b = x*Math.cos(.1*Math.PI); double c = x*Math.cos(.3*Math.PI); double d = x*Math.sin(.1*Math.PI); double e = x*Math.sin(.3*Math.PI); double leftCenterX = gap + a + b + d/2; double leftCenterY = gap + x + minxRad - d; double f = Math.cos(.1*Math.PI); double gg = Math.cos(.2*Math.PI); double magicShiftNumber = d*0.6+minxRad*(f+gg); double shift = leftCenterX+magicShiftNumber; public HashMap<Face, Path> getFaceBoundaries() { HashMap<Face, Path> faces = new HashMap<Face, Path>(); faces.put(Face.U, getPentagon(leftCenterX , leftCenterY , true , minxRad)); faces.put(Face.BL, getPentagon(leftCenterX-c, leftCenterY-e, false, minxRad)); faces.put(Face.BR, getPentagon(leftCenterX+c, leftCenterY-e, false, minxRad)); faces.put(Face.R, getPentagon(leftCenterX+b, leftCenterY+d, false, minxRad)); faces.put(Face.F, getPentagon(leftCenterX , leftCenterY+x, false, minxRad)); faces.put(Face.L, getPentagon(leftCenterX-b, leftCenterY+d, false, minxRad)); faces.put(Face.D, getPentagon(shift+gap+a+b , gap+x+minxRad , false, minxRad)); faces.put(Face.DR, getPentagon(shift+gap+a+b-c, gap+x+e+minxRad, true , minxRad)); faces.put(Face.DBR, getPentagon(shift+gap+a , gap+x-d+minxRad, true , minxRad)); faces.put(Face.B, getPentagon(shift+gap+a+b , gap+minxRad , true , minxRad)); faces.put(Face.DBL, getPentagon(shift+gap+a+2*b, gap+x-d+minxRad, true , minxRad)); faces.put(Face.DL, getPentagon(shift+gap+a+b+c, gap+x+e+minxRad, true , minxRad)); return faces; } @Override public PuzzleState getSolvedState() { return new MegaminxState(); } @Override protected int getRandomMoveCount() { return 11*7; } @Override public PuzzleStateAndGenerator generateRandomMoves(Random r) { StringBuilder scramble = new StringBuilder(); int width = 10, height = 7; for(int i = 0; i < height; i++) { if(i > 0) { scramble.append("\n"); } int dir = 0; for(int j = 0; j < width; j++) { if(j > 0) { scramble.append(" "); } char side = (j % 2 == 0) ? 'R' : 'D'; dir = r.nextInt(2); scramble.append(side + ((dir == 0) ? "++" : "--")); } scramble.append(" U"); if(dir != 0) { scramble.append("'"); } } String scrambleStr = scramble.toString(); PuzzleState state = getSolvedState(); try { state = state.applyAlgorithm(scrambleStr); } catch(InvalidScrambleException e) { azzert(false, e); return null; } return new PuzzleStateAndGenerator(state, scrambleStr); } private int centerIndex = 10; private boolean isNormalized(int[][] image) { return image[Face.U.ordinal()][centerIndex] == Face.U.ordinal() && image[Face.F.ordinal()][centerIndex] == Face.F.ordinal(); } private int[][] cloneImage(int[][] image) { int[][] imageCopy = new int[image.length][image[0].length]; GwtSafeUtils.deepCopy(image, imageCopy); return imageCopy; } private void spinMinx(int[][] image, Face face, int dir) { turn(image, face, dir); bigTurn(image, face.oppositeFace(), 5 - dir); } private void spinToTop(int[][] image, Face face) { switch(face) { case U: break; case BL: spinMinx(image, Face.L, 1); break; case BR: spinMinx(image, Face.U, 1); spinToTop(image, Face.R); break; case R: spinMinx(image, Face.U, 1); spinToTop(image, Face.F); break; case F: spinMinx(image, Face.L, -1); break; case L: spinMinx(image, Face.U, 1); spinToTop(image, Face.BL); break; case D: spinMinx(image, Face.L, -2); spinToTop(image, Face.R); break; case DR: spinMinx(image, Face.L, -1); spinToTop(image, Face.R); break; case DBR: spinMinx(image, Face.U, 1); spinMinx(image, Face.L, -1); spinToTop(image, Face.R); break; case B: spinMinx(image, Face.L, -3); spinToTop(image, Face.R); break; case DBL: spinMinx(image, Face.L, 2); break; case DL: spinMinx(image, Face.L, -2); break; default: azzert(false); } } private int[][] normalize(int[][] image) { if(isNormalized(image)) { return image; } image = cloneImage(image); for(Face face : Face.values()) { if(image[face.ordinal()][centerIndex] == Face.U.ordinal()) { spinToTop(image, face); azzert(image[Face.U.ordinal()][centerIndex] == Face.U.ordinal()); for(int chooseF = 0; chooseF < 5; chooseF++) { spinMinx(image, Face.U, 1); if(isNormalized(image)) { return image; } } azzert(false); } } azzert(false); return null; } class MegaminxState extends PuzzleState { private final int[][] image; private MegaminxState normalizedState; public MegaminxState() { image = new int[12][11]; for(int i = 0; i < image.length; i++) { for(int j = 0; j < image[0].length; j++) { image[i][j] = i; } } normalizedState = this; } public MegaminxState(int[][] image) { this.image = image; } public PuzzleState getNormalized() { if(normalizedState == null) { int[][] normalizedImage = normalize(image); normalizedState = new MegaminxState(normalize(image)); } return normalizedState; } public boolean isNormalized() { return MegaminxPuzzle.this.isNormalized(image); } @Override public LinkedHashMap<String, MegaminxState> getSuccessorsByName() { LinkedHashMap<String, MegaminxState> successors = new LinkedHashMap<String, MegaminxState>(); String[] prettyDir = new String[] { null, "", "2", "2'", "'" }; for(Face face : Face.values()) { for(int dir = 1; dir <= 4; dir++) { String move = face.toString(); move += prettyDir[dir]; int[][] imageCopy = cloneImage(image); turn(imageCopy, face, dir); successors.put(move, new MegaminxState(imageCopy)); } } HashMap<String, Face> pochmannFaceNames = new HashMap<String, Face>(); pochmannFaceNames.put("R", Face.DBR); pochmannFaceNames.put("D", Face.D); String[] prettyPochmannDir = new String[] { null, "+", "++", "--" , "-"}; for(String pochmannFaceName : pochmannFaceNames.keySet()) { for(int dir = 1; dir < 5; dir++) { String move = pochmannFaceName + prettyPochmannDir[dir]; int[][] imageCopy = cloneImage(image); bigTurn(imageCopy, pochmannFaceNames.get(pochmannFaceName), dir); successors.put(move, new MegaminxState(imageCopy)); } } return successors; } @Override public HashMap<String, MegaminxState> getScrambleSuccessors() { HashMap<String, MegaminxState> successors = getSuccessorsByName(); HashMap<String, MegaminxState> scrambleSuccessors = new HashMap<String, MegaminxState>(); for(String turn : new String[] { "R++", "R--", "D++", "D--", "U", "U2", "U2'", "U'" }) { scrambleSuccessors.put(turn, successors.get(turn)); } return scrambleSuccessors; } @Override public boolean equals(Object other) { MegaminxState o = ((MegaminxState) other); return Arrays.deepEquals(image, o.image); } @Override public int hashCode() { return Arrays.deepHashCode(image); } @Override protected Svg drawScramble(HashMap<String, Color> colorScheme) { Svg svg = new Svg(getPreferredSize()); drawMinx(svg, gap, minxRad, colorScheme); return svg; } private void drawMinx(Svg g, int gap, int minxRad, HashMap<String, Color> colorScheme) { HashMap<Face, Path> pentagons = getFaceBoundaries(); for(Face face : pentagons.keySet()) { int f = face.ordinal(); int rotateCounterClockwise; if(face == Face.U) { rotateCounterClockwise = 0; } else if(f >= 1 && f <= 5) { rotateCounterClockwise = 1; } else if(f >= 6 && f <= 11) { rotateCounterClockwise = 2; } else { azzert(false); return; } String label = null; if(face == Face.U || face == Face.F) { label = face.toString(); } drawPentagon(g, pentagons.get(face), image[f], rotateCounterClockwise, label, colorScheme); } } private void drawPentagon(Svg g, Path p, int[] state, int rotateCounterClockwise, String label, HashMap<String, Color> colorScheme) { double[] xpoints = new double[5]; double[] ypoints = new double[5]; PathIterator iter = p.getPathIterator(); for(int ch = 0; ch < 5; 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[10]; double[] ys = new double[10]; for(int i = 0; i < 5; i++) { xs[i]=.4*xpoints[(i+1)%5]+.6*xpoints[i]; ys[i]=.4*ypoints[(i+1)%5]+.6*ypoints[i]; xs[i+5]=.6*xpoints[(i+1)%5]+.4*xpoints[i]; ys[i+5]=.6*ypoints[(i+1)%5]+.4*ypoints[i]; } Path[] ps = new Path[11]; for(int i = 0 ; i < ps.length; i++) { ps[i] = new Path(); } Point2D.Double[] intpent = new Point2D.Double[5]; for(int i = 0; i < intpent.length; i++) { intpent[i] = getLineIntersection(xs[i], ys[i], xs[5+(3+i)%5], ys[5+(3+i)%5], xs[(i+1)%5], ys[(i+1)%5], xs[5+(4+i)%5], ys[5+(4+i)%5]); if(i == 0) { ps[10].moveTo(intpent[i].x, intpent[i].y); } else { ps[10].lineTo(intpent[i].x, intpent[i].y); } } ps[10].closePath(); for(int i = 0; i < 5; i++) { ps[2*i].moveTo(xpoints[i], ypoints[i]); ps[2*i].lineTo(xs[i], ys[i]); ps[2*i].lineTo(intpent[i].x, intpent[i].y); ps[2*i].lineTo(xs[5+(4+i)%5], ys[5+(4+i)%5]); ps[2*i].closePath(); ps[2*i+1].moveTo(xs[i], ys[i]); ps[2*i+1].lineTo(xs[i+5], ys[i+5]); ps[2*i+1].lineTo(intpent[(i+1)%5].x, intpent[(i+1)%5].y); ps[2*i+1].lineTo(intpent[i].x, intpent[i].y); ps[2*i+1].closePath(); } for(int i = 0; i < ps.length; i++) { int j = i; if(j < 10) { // This is a bit convoluted, but tries to keep the intuitive derivation clear. j = (j + 2*rotateCounterClockwise) % 10; } ps[i].setStroke(Color.BLACK); ps[i].setFill(colorScheme.get("" + Face.values()[state[j]])); g.appendChild(ps[i]); } if(label != null) { double centerX = 0; double centerY = 0; for(Point2D.Double pt : intpent) { centerX += pt.x; centerY += pt.y; } centerX /= intpent.length; centerY /= intpent.length; Text labelText = new Text(label, centerX, centerY); // Vertically and horizontally center text labelText.setAttribute("text-anchor", "middle"); // dominant-baseline works great on Chrome, but // unfortunately isn't supported by androidsvg. // See http://stackoverflow.com/q/56402 for workaround. //labelText.setStyle("dominant-baseline", "central"); labelText.setAttribute("dy", "0.7ex"); g.appendChild(labelText); } } } }