package puzzle;
import static net.gnehzr.tnoodle.utils.GwtSafeUtils.modulo;
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.Transform;
import net.gnehzr.tnoodle.svglite.Path;
import net.gnehzr.tnoodle.svglite.Rectangle;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Iterator;
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 cs.sq12phase.FullCube;
import cs.sq12phase.Search;
import org.timepedia.exporter.client.Export;
@Export
public class SquareOnePuzzle extends Puzzle {
private static final int radius = 32;
public SquareOnePuzzle() {
// TODO - we can't filter super aggresively until
// Chen Shuang's optimal solver is fixed.
//wcaMinScrambleDistance = 20;
wcaMinScrambleDistance = 11;
}
@Override
public PuzzleStateAndGenerator generateRandomMoves(Random r) {
Search s = new Search();
String scramble = s.solution(FullCube.randomCube(r)).trim();
PuzzleState state;
try {
state = getSolvedState().applyAlgorithm(scramble);
} catch (InvalidScrambleException e) {
azzert(false, e);
return null;
}
return new PuzzleStateAndGenerator(state, scramble);
}
private static HashMap<String, Color> defaultColorScheme = new HashMap<String, Color>();
static {
defaultColorScheme.put("B", new Color(255, 128, 0)); //orange heraldic tincture
defaultColorScheme.put("D", Color.WHITE);
defaultColorScheme.put("F", Color.RED);
defaultColorScheme.put("L", Color.BLUE);
defaultColorScheme.put("R", Color.GREEN);
defaultColorScheme.put("U", Color.YELLOW);
}
@Override
public HashMap<String, Color> getDefaultColorScheme() {
return new HashMap<String, Color>(defaultColorScheme);
}
@Override
public Dimension getPreferredSize() {
return getImageSize(radius);
}
private static Dimension getImageSize(int radius) {
return new Dimension(getWidth(radius), getHeight(radius));
}
private static final double RADIUS_MULTIPLIER = Math.sqrt(2) * Math.cos(Math.toRadians(15));
private static final double multiplier = 1.4;
private static int getWidth(int radius) {
return (int) (2 * RADIUS_MULTIPLIER * multiplier * radius);
}
private static int getHeight(int radius) {
return (int) (4 * RADIUS_MULTIPLIER * multiplier * radius);
}
private void drawFace(Svg g, Transform transform, int[] face, double x, double y, int radius, Color[] colorScheme) {
for(int ch = 0; ch < 12; ch++) {
if(ch < 11 && face[ch] == face[ch+1]) {
ch++;
}
drawPiece(g, transform, face[ch], x, y, radius, colorScheme);
}
}
private int drawPiece(Svg g, Transform transform, int piece, double x, double y, int radius, Color[] colorScheme) {
boolean corner = isCornerPiece(piece);
int degree = 30 * (corner ? 2 : 1);
Path[] p = corner ? getCornerPoly(x, y, radius) : getWedgePoly(x, y, radius);
Color[] cls = getPieceColors(piece, colorScheme);
for(int ch = cls.length - 1; ch >= 0; ch--) {
p[ch].setFill(cls[ch]);
p[ch].setStroke(Color.BLACK);
p[ch].setTransform(transform);
g.appendChild(p[ch]);
}
transform.rotate(Math.toRadians(degree), x, y);
return degree;
}
private boolean isCornerPiece(int piece) {
return ((piece + (piece <= 7 ? 0 : 1)) % 2) == 0;
}
private Color[] getPieceColors(int piece, Color[] colorScheme) {
boolean up = piece <= 7;
Color top = up ? colorScheme[4] : colorScheme[5];
if(isCornerPiece(piece)) { //corner piece
if(!up) {
piece = 15 - piece;
}
Color a = colorScheme[(piece/2+3) % 4];
Color b = colorScheme[piece/2];
if(!up) { //mirror for bottom
Color t = a;
a = b;
b = t;
}
return new Color[] { top, a, b }; //ordered counter-clockwise
} else { //wedge piece
if(!up) {
piece = 14 - piece;
}
return new Color[] { top, colorScheme[piece/2] };
}
}
private Path[] getWedgePoly(double x, double y, int radius) {
Path p = new Path();
p.moveTo(0, 0);
p.lineTo(radius, 0);
double tempx = Math.sqrt(3) * radius / 2.0;
double tempy = radius / 2.0;
p.lineTo(tempx, tempy);
p.closePath();
p.translate(x, y);
Path side = new Path();
side.moveTo(radius, 0);
side.lineTo(multiplier * radius, 0);
side.lineTo(multiplier * tempx, multiplier * tempy);
side.lineTo(tempx, tempy);
side.closePath();
side.translate(x, y);
return new Path[]{ p, side };
}
private Path[] getCornerPoly(double x, double y, int radius) {
Path p = new Path();
p.moveTo(0, 0);
p.lineTo(radius, 0);
double tempx = radius*(1 + Math.cos(Math.toRadians(75))/Math.sqrt(2));
double tempy = radius*Math.sin(Math.toRadians(75))/Math.sqrt(2);
p.lineTo(tempx, tempy);
double tempX = radius / 2.0;
double tempY = Math.sqrt(3) * radius / 2.0;
p.lineTo(tempX, tempY);
p.closePath();
p.translate(x, y);
Path side1 = new Path();
side1.moveTo(radius, 0);
side1.lineTo(multiplier * radius, 0);
side1.lineTo(multiplier * tempx, multiplier * tempy);
side1.lineTo(tempx, tempy);
side1.closePath();
side1.translate(x, y);
Path side2 = new Path();
side2.moveTo(multiplier * tempx, multiplier * tempy);
side2.lineTo(tempx, tempy);
side2.lineTo(tempX, tempY);
side2.lineTo(multiplier * tempX, multiplier * tempY);
side2.closePath();
side2.translate(x, y);
return new Path[]{ p, side1, side2 };
}
@Override
public String getLongName() {
return "Square-1";
}
@Override
public String getShortName() {
return "sq1";
}
@Override
public PuzzleState getSolvedState() {
return new SquareOneState();
}
@Override
protected int getRandomMoveCount() {
return 40;
}
/*
// TODO - we can't filter super aggresively until
// Chen Shuang's optimal solver is fixed.
@Override
protected String solveIn(PuzzleState ps, int n) {
FullCube f = ((SquareOneState)ps).toFullCube();
Search s = new Search();
String scramble = s.solutionOpt(f, n);
return scramble == null ? null : scramble.trim();
}
*/
static HashMap<String, Integer> costsByMove = new HashMap<String, Integer>();
static {
for(int top = -5; top <= 6; top++) {
for(int bottom = -5; bottom <= 6; bottom++) {
if(top == 0 && bottom == 0) {
// No use doing nothing =)
continue;
}
//int topCost = top % 12 == 0 ? 0 : 1;
//int bottomCost = bottom % 12 == 0 ? 0 : 1;
//int cost = topCost + bottomCost;
int cost = 1;
String turn = "(" + top + "," + bottom + ")";
costsByMove.put(turn, cost);
}
}
costsByMove.put("/", 1);
}
public class SquareOneState extends PuzzleState {
boolean sliceSolved;
int[] pieces;
public SquareOneState() {
sliceSolved = true;
pieces = new int[]{ 0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 9, 9, 10, 11, 11, 12, 13, 13, 14, 15, 15 }; //piece array
}
public SquareOneState(boolean sliceSolved, int[] pieces) {
this.sliceSolved = sliceSolved;
this.pieces = pieces;
}
FullCube toFullCube() {
int[] map1 = new int[]{3, 2, 1, 0, 7, 6, 5, 4, 0xa, 0xb, 8, 9, 0xe, 0xf, 0xc, 0xd};
int[] map2 = new int[]{5,4,3,2,1,0,11,10,9,8,7,6,17,16,15,14,13,12,23,22,21,20,19,18};
FullCube f = FullCube.randomCube();
for (int i=0; i<24; i++) {
f.setPiece(map2[i], map1[pieces[i]]);
}
f.setPiece(24, sliceSolved ? 0 : 1);
return f;
}
private int[] doSlash() {
int[] newPieces = GwtSafeUtils.clone(pieces);
for(int i = 0; i < 6; i++) {
int c = newPieces[i+12];
newPieces[i+12] = newPieces[i+6];
newPieces[i+6] = c;
}
return newPieces;
}
private boolean canSlash() {
if(pieces[0] == pieces[11]) {
return false;
}
if(pieces[6] == pieces[5]) {
return false;
}
if(pieces[12] == pieces[23]) {
return false;
}
if(pieces[12+6] == pieces[(12+6)-1]) {
return false;
}
return true;
}
/**
*
* @param top Amount to rotate top
* @param bottom Amount to rotate bottom
* @return A copy of pieces with (top, bottom) applied to it
*/
private int[] doRotateTopAndBottom(int top, int bottom) {
top = modulo(-top, 12);
int[] newPieces = GwtSafeUtils.clone(pieces);
int[] t = new int[12];
for(int i = 0; i < 12; i++) {
t[i] = newPieces[i];
}
for(int i = 0; i < 12; i++) {
newPieces[i] = t[(top + i) % 12];
}
bottom = modulo(-bottom, 12);
for(int i = 0; i < 12; i++) {
t[i] = newPieces[i+12];
}
for(int i = 0; i < 12; i++) {
newPieces[i+12] = t[(bottom + i) % 12];
}
return newPieces;
}
public int getMoveCost(String move) {
// TODO - We do a lookup here rather than string parsing because
// this is a very performance critical section of code.
// I believe the best thing to do would be to change the puzzle
// api to return move costs as part of the object returned by
// getScrambleSuccessors(), then subclasses wouldn't have to do
// weird stuff like this for speed.
return costsByMove.get(move);
}
@Override
public HashMap<String, SquareOneState> getScrambleSuccessors() {
HashMap<String, SquareOneState> successors = getSuccessorsByName();
Iterator<String> iter = successors.keySet().iterator();
while(iter.hasNext()) {
String key = iter.next();
SquareOneState state = successors.get(key);
if(!state.canSlash()) {
iter.remove();
}
}
return successors;
}
@Override
public LinkedHashMap<String, SquareOneState> getSuccessorsByName() {
LinkedHashMap<String, SquareOneState> successors = new LinkedHashMap<String, SquareOneState>();
for(int top = -5; top <= 6; top++) {
for(int bottom = -5; bottom <= 6; bottom++) {
if(top == 0 && bottom == 0) {
// No use doing nothing =)
continue;
}
int[] newPieces = doRotateTopAndBottom(top, bottom);
String turn = "(" + top + "," + bottom + ")";
successors.put(turn, new SquareOneState(sliceSolved, newPieces));
}
}
if(canSlash()) {
successors.put("/", new SquareOneState(!sliceSolved, doSlash()));
}
return successors;
}
@Override
public boolean equals(Object other) {
SquareOneState o = ((SquareOneState) other);
return Arrays.equals(pieces, o.pieces) && sliceSolved == o.sliceSolved;
}
@Override
public int hashCode() {
return Arrays.hashCode(pieces) ^ (sliceSolved ? 1 : 0);
}
@Override
protected Svg drawScramble(HashMap<String, Color> colorSchemeMap) {
Svg g = new Svg(getPreferredSize());
g.setStroke(2, 10, "round");
String faces = "LBRFUD";
Color[] colorScheme = new Color[faces.length()];
for(int i = 0; i < colorScheme.length; i++) {
colorScheme[i] = colorSchemeMap.get(faces.charAt(i)+"");
}
Dimension dim = getImageSize(radius);
int width = dim.width;
int height = dim.height;
double half_square_width = (radius * RADIUS_MULTIPLIER * multiplier) / Math.sqrt(2);
double edge_width = 2 * radius * multiplier * Math.sin(Math.toRadians(15));
double corner_width = half_square_width - edge_width / 2.;
Rectangle left_mid = new Rectangle(width / 2. - half_square_width, height / 2. - radius * (multiplier - 1) / 2., corner_width, radius * (multiplier - 1));
left_mid.setFill(colorScheme[3]); //front
Rectangle right_mid;
if(sliceSolved) {
right_mid = new Rectangle(width / 2. - half_square_width, height / 2. - radius * (multiplier - 1) / 2., 2*corner_width + edge_width, radius * (multiplier - 1));
right_mid.setFill(colorScheme[3]); //front
} else {
right_mid = new Rectangle(width / 2. - half_square_width, height / 2. - radius * (multiplier - 1) / 2., corner_width + edge_width, radius * (multiplier - 1));
right_mid.setFill(colorScheme[1]); //back
}
g.appendChild(right_mid);
g.appendChild(left_mid); //this will clobber part of the other guy
right_mid = new Rectangle(right_mid);
right_mid.setStroke(Color.BLACK);
right_mid.setFill(null);
left_mid = new Rectangle(left_mid);
left_mid.setStroke(Color.BLACK);
left_mid.setFill(null);
g.appendChild(right_mid);
g.appendChild(left_mid);
Transform transform;
double x = width / 2.0;
double y = height / 4.0;
transform = Transform.getRotateInstance(
Math.toRadians(90 + 15), x, y);
drawFace(g, transform, pieces, x, y, radius, colorScheme);
y *= 3.0;
transform = Transform.getRotateInstance(
Math.toRadians(-90 - 15), x, y);
drawFace(g, transform, GwtSafeUtils.copyOfRange(pieces, 12, pieces.length), x, y, radius, colorScheme);
return g;
}
public String toString() {
return "sliceSolved: " + sliceSolved + " " + Arrays.toString(pieces);
}
}
}