package puzzle;
import static net.gnehzr.tnoodle.utils.GwtSafeUtils.azzert;
import java.util.Random;
public class PyraminxSolver {
public PyraminxSolver() {}
/** There are 4 corners on the pyraminx that are in a fixed position.
* There are 3 different orientations for each corner.
*
* U
* ____ ____ ____ ____ ____ ____
* \ /\ /\ / /\ \ /\ /\ /
* \ /3 \ /0 \ / / \ \ /0 \ /3 \ /
* \/____\/____\/ /____\ \/____\/____\/
* \ /\ / /\ /\ \ /\ /
* \ /1 \ / / \0 / \ \ /2 \ /
* \/____\/ /____\/____\ \/____\/
* \ / /\ /\ /\ \ /
* \ / / \1 / \2 / \ \ /
* \/ /____\/____\/____\ \/
* L R
* ____ ____ ____
* \ /\ /\ /
* \ /1 \ /2 \ /
* \/____\/____\/
* \ /\ /
* \ /3 \ /
* \/____\/
* \ /
* \ /
* \/
*
* B
*
* There are 6 edges, each one having 2 different orientations.
* Dollars mark the primary facelet position.
* U
* ____ ____ ____ ____ ____ ____
* \ /\ /\ / /\ \ /\ /\ /
* \ / \5$/ \ / / \ \ / \5 / \ /
* \/____\/____\/ /____\ \/____\/____\/
* \ /\ / /\ /\ \ /\ /
* \2 / \1 / /1$\ /3$\ \3 / \4 /
* \/____\/ /____\/____\ \/____\/
* \ / /\ /\ /\ \ /
* \ / / \ /0$\ / \ \ /
* \/ /____\/____\/____\ \/
* L R
* ____ ____ ____
* \ /\ /\ /
* \ / \0 / \ /
* \/____\/____\/
* \ /\ /
* \2$/ \4$/
* \/____\/
* \ /
* \ /
* \/
*
* B
*/
static final int N_EDGE_PERM = 720; // Number of permutations of edges
static final int N_EDGE_ORIENT = 32; // Number of orientations of edges
static final int N_CORNER_ORIENT = 81; // Number of orientations of corners
static final int N_ORIENT = N_EDGE_ORIENT * N_CORNER_ORIENT;
static final int N_TIPS = 81; // Number of tips positions
static final int N_MOVES = 8; // Number of moves
static final int MAX_LENGTH = 20;
static final String[] moveToString = {"U", "U'", "L", "L'", "R", "R'", "B", "B'"};
static final String[] inverseMoveToString = {"U'", "U", "L'", "L", "R'", "R", "B'", "B"};
static final String[] tipToString = {"u", "u'", "l", "l'", "r", "r'", "b", "b'"};
static final String[] inverseTipToString = {"u'", "u", "l'", "l", "r'", "r", "b'", "b"};
static final int[] fact = {1, 1, 2, 6, 24, 120, 720}; // fact[x] = x!
/**
* Converts the list of edges into a number representing the permutation of the edges.
* @param edges edges representation (ori << 3 + perm)
* @return an integer between 0 and 719 representing the permutation of 6 elements
*/
public static int packEdgePerm(int[] edges) {
int idx = 0;
int val = 0x543210;
for (int i = 0; i < 5; i++) {
int v = ( edges[i] & 0x7 ) << 2;
idx = (6 - i) * idx + ((val >> v) & 0x7);
val -= 0x111110 << v;
}
return idx;
}
/**
* Converts an integer representing a permutation of 6 elements into a list of edges.
* @param perm an integer between 0 and 719 representing the permutation of 6 elements
* @param edges edges representation (ori << 3 + perm)
*/
private static void unpackEdgePerm(int perm, int[] edges) {
int val = 0x543210;
for (int i = 0; i < 5; i++) {
int p = fact[5-i];
int v = perm / p;
perm -= v*p;
v <<= 2;
edges[i] = (val >> v) & 0x7;
int m = (1 << v) - 1;
val = (val & m) + ((val >> 4) & ~m);
}
edges[5] = val;
}
/**
* Converts the list of edges into a number representing the orientation of the edges.
* @param edges edges representation (ori << 3 + perm)
* @return an integer between 0 and 31 representing the orientation of 5 elements (the 6th is fixed)
*/
public static int packEdgeOrient(int[] edges) {
int ori = 0;
for (int i=0; i<5; i++) {
ori = 2 * ori + ( edges[i] >> 3 );
}
return ori;
}
/**
* Converts an integer representing the orientation of 5 elements into a list of cubies.
* @param ori an integer between 0 and 31 representing the orientation of 5 elements (the 6th is fixed)
* @param edges edges representation (ori << 3 + perm)
*/
private static void unpackEdgeOrient(int ori, int[] edges) {
int sum_ori = 0;
for (int i = 4; i >= 0; i--) {
edges[i] = ( ori & 1 ) << 3;
sum_ori ^= ori & 1;
ori >>= 1;
}
edges[5] = sum_ori << 3;
}
/**
* Converts the list of corners into a number representing the orientation of the corners.
* @param corners corner representation
* @return an integer between 0 and 80 representing the orientation of 4 elements
*/
public static int packCornerOrient(int[] corners) {
int ori = 0;
for (int i = 0; i < 4; i++) {
ori = 3 * ori + corners[i];
}
return ori;
}
/**
* Converts an integer representing the orientation of 4 elements into a list of corners.
* @param ori an integer between 0 and 80 representing the orientation of 4 elements
* @param corners corners representation
*/
private static void unpackCornerOrient(int ori, int[] corners) {
for (int i = 3; i >= 0; i--) {
corners[i] = ori % 3;
ori /= 3;
}
}
/**
* Cycle three elements of an array. Also orient first and second elements.
* @param edges edges representation (ori << 3 + perm)
* @param a first element to cycle
* @param b second element to cycle
* @param c third element to cycle
* @param times number of times to cycle
*/
private static void cycleAndOrient(int[] edges, int a, int b, int c, int times) {
int temp = edges[c];
edges[c] = (edges[b] + 8) % 16;
edges[b] = (edges[a] + 8) % 16;
edges[a] = temp;
if( times > 1 ) {
cycleAndOrient(edges, a, b, c, times - 1);
}
}
/**
* Apply a move on the edges representation.
* @param edges edges representation (ori << 3 + perm)
* @param move move to apply to the edges
*/
private static void moveEdges(int[] edges, int move) {
int face = move / 2;
int times = ( move % 2 ) + 1;
switch (face) {
case 0: // U face
cycleAndOrient(edges, 5, 3, 1, times);
break;
case 1: // L face
cycleAndOrient(edges, 2, 1, 0, times);
break;
case 2: // R face
cycleAndOrient(edges, 0, 3, 4, times);
break;
case 3: // B face
cycleAndOrient(edges, 2, 4, 5, times);
break;
default:
azzert(false);
}
}
/**
* Apply a move on the corners representation.
* @param corners corners representation
* @param move move to apply to the corners
*/
private static void moveCorners(int[] corners, int move) {
int face = move / 2;
int times = ( move % 2 ) + 1;
corners[face] = ( corners[face] + times ) % 3;
}
/**
* Fill the arrays to move permutation and orientation coordinates.
*/
public static int[][] moveEdgePerm = new int[N_EDGE_PERM][N_MOVES];
public static int[][] moveEdgeOrient = new int[N_EDGE_ORIENT][N_MOVES];
public static int[][] moveCornerOrient = new int[N_CORNER_ORIENT][N_MOVES];
private static void initMoves() {
int[] edges1 = new int[6];
int[] edges2 = new int[6];
for (int perm=0; perm < N_EDGE_PERM; perm++) {
unpackEdgePerm(perm, edges1);
for (int move=0; move < N_MOVES; move++) {
System.arraycopy(edges1, 0, edges2, 0, 6);
moveEdges(edges2, move);
int newPerm = packEdgePerm(edges2);
moveEdgePerm[perm][move] = newPerm;
}
}
for (int orient=0; orient < N_EDGE_ORIENT; orient++) {
unpackEdgeOrient(orient, edges1);
for (int move=0; move < N_MOVES; move++) {
System.arraycopy(edges1, 0, edges2, 0, 6);
moveEdges(edges2, move);
int newOrient = packEdgeOrient(edges2);
moveEdgeOrient[orient][move] = newOrient;
}
}
int[] corners1 = new int[4];
int[] corners2 = new int[4];
for(int orient = 0; orient < N_CORNER_ORIENT; orient++) {
unpackCornerOrient(orient, corners1);
for(int move = 0; move < N_MOVES; move++) {
System.arraycopy(corners1, 0, corners2, 0, 4);
moveCorners(corners2, move);
int newOrient = packCornerOrient(corners2);
moveCornerOrient[orient][move] = newOrient;
}
}
}
/**
* Fill the pruning tables for the permutation and orientation coordinates.
*/
private static int[] prunPerm = new int[N_EDGE_PERM];
private static int[] prunOrient = new int[N_ORIENT];
private static void initPrun() {
for (int perm = 0; perm < N_EDGE_PERM; perm++) {
prunPerm[perm] = -1;
}
prunPerm[0] = 0;
int done = 1;
for(int length = 0; done < N_EDGE_PERM/2; length++) { // Only half of the permutations are accessible due to parity
for(int perm = 0; perm < N_EDGE_PERM; perm++) {
if(prunPerm[perm] == length) {
for(int move=0; move < N_MOVES; move++) {
int newPerm = moveEdgePerm[perm][move];
if(prunPerm[newPerm] == -1) {
prunPerm[newPerm] = length + 1;
done++;
}
}
}
}
}
for(int orient = 0; orient < N_ORIENT; orient++) {
prunOrient[orient] = -1;
}
prunOrient[0] = 0;
done = 1;
for(int length = 0; done < N_ORIENT; length++) {
for(int orient = 0; orient < N_ORIENT; orient++) {
if(prunOrient[orient] == length ) {
for(int move=0; move < N_MOVES; move++) {
int newEdgeOrient = moveEdgeOrient[orient % N_EDGE_ORIENT][move];
int newCornerOrient = moveCornerOrient[orient / N_EDGE_ORIENT][move];
int newOrient = newCornerOrient * N_EDGE_ORIENT + newEdgeOrient;
if(prunOrient[newOrient] == -1) {
prunOrient[newOrient] = length + 1;
done++;
}
}
}
}
}
}
static {
initMoves();
initPrun();
}
/**
* Search a solution from a position given by permutation and orientation coordinates
* @param edgePerm edge permutation coordinate to solve
* @param edgeOrient edge orientation coordinate to solve
* @param cornerOrient corner orientation coordinate to solve
* @param depth current depth of the search (first called with 0)
* @param length the remaining number of moves we can apply
* @param last_move what was the last move done (first called with an int >= 9)
* @param solution the array containing the current moves done.
* @return true if a solution was found (stored in the solution array)
* false if no solution was found
*/
private boolean search(int edgePerm, int edgeOrient, int cornerOrient, int depth, int length, int last_move, int[] solution, Random randomiseMoves) {
/* If there are no moves left to try (length=0), returns if the current position is solved */
if( length == 0 ) {
return ( edgePerm == 0 ) && ( edgeOrient == 0 ) && ( cornerOrient == 0 );
}
/* Check if we might be able to solve the permutation or the orientation of the position
* given the remaining number of moves ('length' parameter), using the pruning tables.
* If not, there is no point keeping searching for a solution, just stop.
*/
if(( prunPerm[edgePerm] > length ) || ( prunOrient[cornerOrient*N_EDGE_ORIENT+edgeOrient] > length )) {
return false;
}
/* The recursive part of the search function.
* Try every move from the current position, and call the search function with the new position
* and the updated parameters (depth -> depth+1; length -> length-1; last_move -> move)
* We don't need to try a move of the same face as the last move.
* We randomise the move order by generating a random offset.
*/
int randomOffset = randomiseMoves.nextInt(N_MOVES);
for( int move=0; move<N_MOVES; move++) {
int randomMove = ( move + randomOffset ) % N_MOVES;
// Check if the tested move is of the same face as the previous move (last_move).
if(( randomMove / 2 ) == ( last_move / 2 )) {
continue;
}
// Apply the move
int newEdgePerm = moveEdgePerm[edgePerm][randomMove];
int newEdgeOrient = moveEdgeOrient[edgeOrient][randomMove];
int newCornerOrient = moveCornerOrient[cornerOrient][randomMove];
// Call the recursive function
if( search( newEdgePerm, newEdgeOrient, newCornerOrient, depth+1, length-1, randomMove, solution, randomiseMoves )) {
// Store the move
solution[depth] = randomMove;
return true;
}
}
return false;
}
public static class PyraminxSolverState {
public int edgePerm, edgeOrient, cornerOrient, tips;
public int unsolvedTips() {
int numberUnsolved = 0;
int tempTips = this.tips;
while(tempTips != 0){
if((tempTips % 3) > 0) {
numberUnsolved++;
}
tempTips /= 3;
}
azzert(numberUnsolved <= 4);
return numberUnsolved;
}
}
/**
* Generate a random pyraminx position.
* @param r random int generator
*/
public PyraminxSolverState randomState(Random r) {
PyraminxSolverState state = new PyraminxSolverState();
do {
state.edgePerm = r.nextInt(N_EDGE_PERM);
} while(prunPerm[state.edgePerm] == -1); // incorrect permutation (bad parity)
state.edgeOrient = r.nextInt(N_EDGE_ORIENT);
state.cornerOrient = r.nextInt(N_CORNER_ORIENT);
state.tips = r.nextInt(N_TIPS);
return state;
}
/**
* Solve a given position in less than or equal to length number of turns.
* Returns either the solution or the generator (inverse solution)
* @param state state
* @param length length of the desired solution
* @param includingTips do we want to include tips in the solution lenght ?
* @return a string representing the solution or the scramble of a random position
*/
public String solveIn(PyraminxSolverState state, int length, boolean includingTips) {
return solve(state, length, false, false, includingTips);
}
/**
* Return a generator of a given position in exactly length number of turns or not at all.
* Returns either the solution or the generator (inverse solution)
* @param state state
* @param length length of the desired solution
* @param includingTips do we want to include tips in the solution lenght ?
* @return a string representing the solution or the scramble of a random position
*/
public String generateExactly(PyraminxSolverState state, int length, boolean includingTips) {
return solve(state, length, true, true, includingTips);
}
private String solve(PyraminxSolverState state, int desiredLength, boolean exactLength, boolean inverse, boolean includingTips) {
Random r = new Random();
int[] solution = new int[MAX_LENGTH];
boolean foundSolution = false;
// If we count the tips in the desired length, we have to subtract the number of unsolved tips from the length of the main puzzle search.
if(includingTips) {
desiredLength -= state.unsolvedTips();
}
int length = exactLength ? desiredLength : 0;
while(length <= desiredLength) {
if(search(state.edgePerm, state.edgeOrient, state.cornerOrient, 0, length, 42, solution, r)) {
foundSolution = true;
break;
}
length++;
}
if(!foundSolution) {
return null;
}
StringBuilder scramble = new StringBuilder((MAX_LENGTH+4)*3);
if(inverse){
for(int i = length - 1; i >= 0; i--) {
scramble.append(" ").append(inverseMoveToString[solution[i]]);
}
} else {
for(int i = 0; i < length; i++) {
scramble.append(" ").append(moveToString[solution[i]]);
}
}
// Scramble the tips
int[] arrayTips = new int[4];
unpackCornerOrient(state.tips, arrayTips);
for(int tip = 0; tip < 4; tip++) {
int dir = arrayTips[tip];
if(dir > 0) {
if(inverse) {
scramble.append(" ").append(tipToString[tip*2+dir-1]);
} else {
scramble.append(" ").append(inverseTipToString[tip*2+dir-1]);
}
}
}
return scramble.toString().trim();
}
}