package net.gnehzr.tnoodle.scrambles; import static net.gnehzr.tnoodle.utils.GwtSafeUtils.azzert; import java.util.ArrayList; import java.util.HashMap; import java.util.logging.Logger; import net.gnehzr.tnoodle.scrambles.Puzzle.PuzzleState; import net.gnehzr.tnoodle.utils.GwtSafeUtils; public class AlgorithmBuilder { private static final Logger l = Logger.getLogger(AlgorithmBuilder.class.getName()); private ArrayList<String> moves = new ArrayList<String>(); /** * states.get(i) = state achieved by applying moves[0]...moves[i-1] */ private ArrayList<PuzzleState> states = new ArrayList<PuzzleState>(); /** * If we are in CANONICALIZE_MOVES MergingMode, then something like * Uw Dw' on a 4x4x4 will become Uw2. This means the state we end * up in is actually different than the state we would have ended up in * if we had just naively appended moves (NO_MERGING). * unNormalizedState keeps track of the state we would have been in * if we had just naively appended turns. */ private PuzzleState originalState, unNormalizedState; private int totalCost; private MergingMode mergingMode = MergingMode.NO_MERGING; private Puzzle puzzle; public AlgorithmBuilder(Puzzle puzzle, MergingMode mergingMode) { this(puzzle, mergingMode, puzzle.getSolvedState()); } public AlgorithmBuilder(Puzzle puzzle, MergingMode mergingMode, PuzzleState originalState) { this.puzzle = puzzle; this.mergingMode = mergingMode; resetToState(originalState); } private void resetToState(PuzzleState originalState) { this.totalCost = 0; this.originalState = originalState; this.unNormalizedState = originalState; this.moves.clear(); this.states.clear(); states.add(unNormalizedState); } public static enum MergingMode { // There are several degrees of manipulation we can choose to do // while building an algorithm. Here they are, ranging from least to // most aggressive. Examples are on a 3x3x3. // Straightforward, blindly append moves. // For example: // - "R R" stays unmodified. NO_MERGING, // Merge together redundant moves, but preserve the exact state // of the puzzle (unlike CANONICALIZE_MOVES). // In other words, the resulting state will be the // same as if we had used NO_MERGING. // For example: // - "R R" becomes "R2" // - "L Rw" stays unmodified. // - "F x U" will become something like "F2 x". // TODO - add actual support for this! feel free to rename it //MERGE_REDUNDANT_MOVES_PRESERVE_STATE, // Most aggressive merging. // See PuzzleState.getCanonicalMovesByState() for the // definition of "canonical" moves. // Canonical moves will not necessarily let us preserve the // exact state we would have achieved with NO_MERGING. This is // because canonical moves may not let us rotate the puzzle. // However, the resulting state when normalized will be the // same as the normalization of the state we would have // achieved if we had used NO_MERGING. // For example: // - "R R" becomes "R2" // - "L Rw" becomes "L2" // - "F x U" becomes "F2" CANONICALIZE_MOVES; } public boolean isRedundant(String move) throws InvalidMoveException { // TODO - add support for MERGE_REDUNDANT_MOVES_PRESERVE_STATE //MergingMode mergingMode = preserveState ? MergingMode.MERGE_REDUNDANT_MOVES_PRESERVE_STATE : MergingMode.CANONICALIZE_MOVES; MergingMode mergingMode = MergingMode.CANONICALIZE_MOVES; IndexAndMove indexAndMove = findBestIndexForMove(move, mergingMode); return indexAndMove.index < moves.size() || indexAndMove.move == null; } public static class IndexAndMove { public int index; public String move; public IndexAndMove(int index, String move) { this.index = index; this.move = move; } public String toString() { return "{ index: " + index + " move: " + move + " }"; } } public IndexAndMove findBestIndexForMove(String move, MergingMode mergingMode) throws InvalidMoveException { if(mergingMode == MergingMode.NO_MERGING) { return new IndexAndMove(moves.size(), move); } PuzzleState newUnNormalizedState = unNormalizedState.apply(move); if(newUnNormalizedState.equalsNormalized(unNormalizedState)) { // move must just be a rotation. if(mergingMode == MergingMode.CANONICALIZE_MOVES) { return new IndexAndMove(0, null); } } PuzzleState newNormalizedState = newUnNormalizedState.getNormalized(); HashMap<? extends PuzzleState, String> successors = getState().getCanonicalMovesByState(); move = null; // Search for the right move to do to our current state in // order to match up with newNormalizedState. for(PuzzleState ps : successors.keySet()) { if(ps.equalsNormalized(newNormalizedState)) { move = successors.get(ps); break; } } // One of getStates()'s successors must be newNormalizedState. // If not, something has gone very wrong. azzert(move != null); if(mergingMode == MergingMode.CANONICALIZE_MOVES) { for(int lastMoveIndex = moves.size() - 1; lastMoveIndex >= 0; lastMoveIndex--) { String lastMove = moves.get(lastMoveIndex); PuzzleState stateBeforeLastMove = states.get(lastMoveIndex); if(!stateBeforeLastMove.movesCommute(lastMove, move)) { break; } PuzzleState stateAfterLastMove = states.get(lastMoveIndex+1); PuzzleState stateAfterLastMoveAndNewMove = stateAfterLastMove.apply(move); if(stateBeforeLastMove.equalsNormalized(stateAfterLastMoveAndNewMove)) { // move cancels with lastMove return new IndexAndMove(lastMoveIndex, null); } else { successors = stateBeforeLastMove.getCanonicalMovesByState(); for(PuzzleState ps : successors.keySet()) { if(ps.equalsNormalized(stateAfterLastMoveAndNewMove)) { String alternateLastMove = successors.get(ps); // move merges with lastMove return new IndexAndMove(lastMoveIndex, alternateLastMove); } } } } } return new IndexAndMove(moves.size(), move); } public void appendMove(String newMove) throws InvalidMoveException { l.fine("appendMove(" + newMove + ")"); IndexAndMove indexAndMove = findBestIndexForMove(newMove, mergingMode); int oldCostMove, newCostMove; if(indexAndMove.index < moves.size()) { // This move is redundant. azzert(mergingMode != MergingMode.NO_MERGING); oldCostMove = states.get(indexAndMove.index).getMoveCost(moves.get(indexAndMove.index)); if(indexAndMove.move == null) { // newMove cancelled perfectly with the move at // indexAndMove.index. moves.remove(indexAndMove.index); states.remove(indexAndMove.index + 1); newCostMove = 0; } else { // newMove merged with the move at indexAndMove.index. moves.set(indexAndMove.index, indexAndMove.move); newCostMove = states.get(indexAndMove.index).getMoveCost(indexAndMove.move); } } else { oldCostMove = 0; newCostMove = states.get(states.size() - 1).getMoveCost(indexAndMove.move); // This move is not redundant. moves.add(indexAndMove.move); // The code to update the states array is right below us, // but it requires that the states array be of the correct // size. states.add(null); } totalCost += newCostMove - oldCostMove; // We modified moves[ indexAndMove.index ], so everything in // states[ indexAndMove.index+1, ... ] is now invalid for(int i = indexAndMove.index + 1; i < states.size(); i++) { states.set(i, states.get(i - 1).apply(moves.get(i - 1))); } unNormalizedState = unNormalizedState.apply(newMove); azzert(states.size() == moves.size() + 1); azzert(unNormalizedState.equalsNormalized(getState())); } public String popMove(int index) { ArrayList<String> movesCopy = new ArrayList<String>(moves); String poppedMove = movesCopy.remove(index); resetToState(originalState); for(String move : movesCopy) { try { appendMove(move); } catch(InvalidMoveException e) { azzert(false, e); } } return poppedMove; } public void appendAlgorithm(String algorithm) throws InvalidMoveException { for(String move : splitAlgorithm(algorithm)) { appendMove(move); } } public void appendAlgorithms(String[] algorithms) throws InvalidMoveException { for(String algorithm : algorithms) { appendAlgorithm(algorithm); } } public PuzzleState getState() { azzert(states.size() == moves.size() + 1); return states.get(states.size() - 1); } public int getTotalCost() { return totalCost; } public String toString() { return GwtSafeUtils.join(moves, " "); } public PuzzleStateAndGenerator getStateAndGenerator() { return new PuzzleStateAndGenerator(getState(), toString()); } public static String[] splitAlgorithm(String algorithm) { if(algorithm.trim().isEmpty()) { return new String[0]; } return algorithm.split("\\s+"); } }