diff --git a/runelite-api/src/main/java/net/runelite/api/InventoryID.java b/runelite-api/src/main/java/net/runelite/api/InventoryID.java index 16fdbc8e29..000bb8aa52 100644 --- a/runelite-api/src/main/java/net/runelite/api/InventoryID.java +++ b/runelite-api/src/main/java/net/runelite/api/InventoryID.java @@ -49,6 +49,10 @@ public enum InventoryID * Barrows reward chest inventory. */ BARROWS_REWARD(141), + /** + * Monkey madness puzzle box inventory. + */ + MONKEY_MADNESS_PUZZLE_BOX(221), /** * Chambers of Xeric chest inventory. */ diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverOverlay.java index 9069ddfd53..67c821d841 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverOverlay.java @@ -52,6 +52,7 @@ import net.runelite.client.plugins.puzzlesolver.solver.PuzzleSolver; import net.runelite.client.plugins.puzzlesolver.solver.PuzzleState; import net.runelite.client.plugins.puzzlesolver.solver.heuristics.ManhattanDistance; import net.runelite.client.plugins.puzzlesolver.solver.pathfinding.IDAStar; +import net.runelite.client.plugins.puzzlesolver.solver.pathfinding.IDAStarMM; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayPosition; @@ -105,11 +106,18 @@ public class PuzzleSolverOverlay extends Overlay return null; } + boolean useNormalSolver = true; ItemContainer container = client.getItemContainer(InventoryID.PUZZLE_BOX); if (container == null) { - return null; + useNormalSolver = false; + container = client.getItemContainer(InventoryID.MONKEY_MADNESS_PUZZLE_BOX); + + if (container == null) + { + return null; + } } Widget puzzleBox = client.getWidget(WidgetInfo.PUZZLE_BOX); @@ -123,7 +131,7 @@ public class PuzzleSolverOverlay extends Overlay String infoString = "Solving.."; - int[] itemIds = getItemIds(container); + int[] itemIds = getItemIds(container, useNormalSolver); boolean shouldCache = false; if (solver != null) @@ -335,7 +343,7 @@ public class PuzzleSolverOverlay extends Overlay if (solver == null || cachedItems == null || (!shouldCache && solver.hasExceededWaitDuration() && !Arrays.equals(cachedItems, itemIds))) { - solve(itemIds); + solve(itemIds, useNormalSolver); shouldCache = true; } @@ -347,7 +355,7 @@ public class PuzzleSolverOverlay extends Overlay return null; } - private int[] getItemIds(ItemContainer container) + private int[] getItemIds(ItemContainer container, boolean useNormalSolver) { int[] itemIds = new int[DIMENSION * DIMENSION]; @@ -364,13 +372,10 @@ public class PuzzleSolverOverlay extends Overlay itemIds[items.length] = BLANK_TILE_VALUE; } - return convertToSolverFormat(itemIds); + return convertToSolverFormat(itemIds, useNormalSolver); } - /** - * This depends on there being no gaps in between item ids in puzzles. - */ - private int[] convertToSolverFormat(int[] items) + private int[] convertToSolverFormat(int[] items, boolean useNormalSolver) { int lowestId = Integer.MAX_VALUE; @@ -393,7 +398,15 @@ public class PuzzleSolverOverlay extends Overlay { if (items[i] != BLANK_TILE_VALUE) { - convertedItems[i] = items[i] - lowestId; + int value = items[i] - lowestId; + + // The MM puzzle has gaps + if (!useNormalSolver) + { + value /= 2; + } + + convertedItems[i] = value; } else { @@ -410,7 +423,7 @@ public class PuzzleSolverOverlay extends Overlay System.arraycopy(items, 0, cachedItems, 0, cachedItems.length); } - private void solve(int[] items) + private void solve(int[] items, boolean useNormalSolver) { if (solverFuture != null) { @@ -419,7 +432,15 @@ public class PuzzleSolverOverlay extends Overlay PuzzleState puzzleState = new PuzzleState(items); - solver = new PuzzleSolver(new IDAStar(new ManhattanDistance()), puzzleState); + if (useNormalSolver) + { + solver = new PuzzleSolver(new IDAStar(new ManhattanDistance()), puzzleState); + } + else + { + solver = new PuzzleSolver(new IDAStarMM(new ManhattanDistance()), puzzleState); + } + solverFuture = executorService.submit(solver); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/solver/PuzzleState.java b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/solver/PuzzleState.java index 440276a13f..a34675feb8 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/solver/PuzzleState.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/solver/PuzzleState.java @@ -53,20 +53,8 @@ public class PuzzleState throw new IllegalStateException("Piece array does not have the right dimensions"); } - for (int i = 0; i < pieces.length; i++) - { - if (pieces[i] == BLANK_TILE_VALUE) - { - emptyPiece = i; - } - } - - if (emptyPiece == -1) - { - throw new IllegalStateException("Incorrect empty piece passed in!"); - } - this.pieces = pieces; + findEmptyPiece(); } private PuzzleState(PuzzleState state) @@ -75,6 +63,19 @@ public class PuzzleState this.emptyPiece = state.emptyPiece; } + private void findEmptyPiece() + { + for (int i = 0; i < pieces.length; i++) + { + if (pieces[i] == BLANK_TILE_VALUE) + { + this.emptyPiece = i; + return; + } + } + throw new IllegalStateException("Incorrect empty piece passed in!"); + } + public List computeMoves() { List moves = new ArrayList<>(); @@ -179,4 +180,46 @@ public class PuzzleState return h; } + + public PuzzleState swap(int x1, int y1, int x2, int y2) + { + int val1 = getPiece(x1, y1); + int val2 = getPiece(x2, y2); + + if (!isValidSwap(x1, y1, x2, y2)) + { + throw new IllegalStateException(String.format("Invalid swap: (%1$d, %2$d), (%3$d, %4$d)", x1, y1, x2, y2)); + } + + PuzzleState newState = new PuzzleState(this); + + newState.pieces[y1 * DIMENSION + x1] = val2; + newState.pieces[y2 * DIMENSION + x2] = val1; + newState.findEmptyPiece(); + + return newState; + } + + private boolean isValidSwap(int x1, int y1, int x2, int y2) + { + int absX = Math.abs(x1 - x2); + int absY = Math.abs(y1 - y2); + + if (getPiece(x1, y1) != BLANK_TILE_VALUE && getPiece(x2, y2) != BLANK_TILE_VALUE) + { + return false; + } + + if (x1 == x2 && absY == 1) + { + return true; + } + + if (y1 == y2 && absX == 1) + { + return true; + } + + return false; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/solver/PuzzleSwapPattern.java b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/solver/PuzzleSwapPattern.java new file mode 100644 index 0000000000..fe7949efd5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/solver/PuzzleSwapPattern.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018, Steffen Hauge + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.puzzlesolver.solver; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum PuzzleSwapPattern +{ + ROTATE_LEFT_UP(new int[]{1, -1, 0, -1, -1, -1, -1, 0}, 1, 1), //Reference point + ROTATE_LEFT_DOWN(1, -1), + ROTATE_RIGHT_UP(-1, 1), + ROTATE_RIGHT_DOWN(-1, -1), + ROTATE_UP_LEFT(new int[]{-1, 1, -1, 0, -1, -1, 0, -1}, 1 , 1), //Reference point + ROTATE_UP_RIGHT(-1, 1), + ROTATE_DOWN_LEFT(1, -1), + ROTATE_DOWN_RIGHT(-1, -1), + LAST_PIECE_ROW(new int[]{-1, -1, 0, -1, -1, 0, -1, 1}, 1, 1), + LAST_PIECE_COLUMN(new int[]{-1, -1, -1, 0, 0, -1, 1, -1}, 1, 1), + SHUFFLE_UP_RIGHT(new int[]{1, -1, 0, -1}, 1, 1), + SHUFFLE_UP_LEFT(new int[]{-1, -1, 0, -1}, 1, 1), + SHUFFLE_UP_BELOW(new int[]{-1, 1, -1, 0}, 1, 1), + SHUFFLE_UP_ABOVE(new int[]{-1, -1, -1, 0}, 1, 1); + + /** + * Points used for swaps relative to locVal + */ + private final int[] points; + /** + * Modifier for X coordinate + */ + private final int modX; + /** + * Modifier for Y coordinate + */ + private final int modY; + + PuzzleSwapPattern(int modX, int modY) + { + this(null, modX, modY); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/solver/pathfinding/IDAStarMM.java b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/solver/pathfinding/IDAStarMM.java new file mode 100644 index 0000000000..f96b6971bd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/solver/pathfinding/IDAStarMM.java @@ -0,0 +1,716 @@ +/* + * Copyright (c) 2018, Steffen Hauge + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.puzzlesolver.solver.pathfinding; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import net.runelite.api.Point; +import static net.runelite.client.plugins.puzzlesolver.solver.PuzzleSolver.BLANK_TILE_VALUE; +import static net.runelite.client.plugins.puzzlesolver.solver.PuzzleSolver.DIMENSION; +import net.runelite.client.plugins.puzzlesolver.solver.PuzzleState; +import net.runelite.client.plugins.puzzlesolver.solver.PuzzleSwapPattern; +import static net.runelite.client.plugins.puzzlesolver.solver.PuzzleSwapPattern.*; +import net.runelite.client.plugins.puzzlesolver.solver.heuristics.Heuristic; + +public class IDAStarMM extends IDAStar +{ + private PuzzleState currentState; + private List stateList = new ArrayList<>(); + private List> validRowNumbers = new ArrayList<>(); + private List> validColumnNumbers = new ArrayList<>(); + + public IDAStarMM(Heuristic heuristic) + { + super(heuristic); + + //Add valid numbers for rows and columns + validRowNumbers.add(Arrays.asList(0, 1, 2, 3, 4)); + validRowNumbers.add(Arrays.asList(6, 7, 8, 9)); + validColumnNumbers.add(Arrays.asList(5, 10, 15, 20)); + } + + @Override + public List computePath(PuzzleState root) + { + currentState = root; + stateList.add(root); + + List path = new ArrayList<>(); + + //Reduce to 4x5 + solveRow(0); + + //Reduce to 4x4 + solveColumn(); + + //Reduce to 3x4 + solveRow(1); + + //Remove last state + stateList.remove(stateList.size() - 1); + + //Pathfinder for 4x4 + path.addAll(super.computePath(currentState)); + + path.addAll(0, stateList); + + return path; + } + + private void solveRow(int row) + { + for (int i = row; i < DIMENSION; i++) + { + int valTarget = row * DIMENSION + i; + + int valCurrent = currentState.getPiece(i, row); + + if (valCurrent != valTarget) + { + moveTowardsVal(valTarget, i, row, true); + } + } + } + + private void solveColumn() + { + int column = 0; + + for (int i = column + 1; i < DIMENSION; i++) + { + int valTarget = column + i * DIMENSION; + + int valCurrent = currentState.getPiece(column, i); + + if (valCurrent != valTarget) + { + moveTowardsVal(valTarget, column, i, false); + } + } + } + + private void moveTowardsVal(int valTarget, int x, int y, boolean rowMode) + { + //Not in place + boolean reached = false; + + while (currentState.getPiece(x, y) != valTarget) + { + //Find piece location + Point locVal = findPiece(valTarget); + Point locBlank = findPiece(BLANK_TILE_VALUE); + + if (reached) + { + //Swap towards locTarget + if (rowMode) + { + alignTargetX(valTarget, x, y); + swapUpRow(valTarget, x, y); + } + else + { + alignTargetY(valTarget, x, y); + swapLeftColumn(valTarget, x, y); + } + } + else + { + int distX = locVal.getX() - locBlank.getX(); + int distY = locVal.getY() - locBlank.getY(); + int distAbsX = Math.abs(distX); + int distAbsY = Math.abs(distY); + + if (distX == 0) + { + //Same column + if (distAbsY == 1) + { + //Next to + reached = true; + } + else + { + //More than 2 away, move towards on Y-axis + if (distY >= 2) + { + Point locSwap = new Point(locBlank.getX(), locBlank.getY() + 1); + swap(locBlank, locSwap); + } + else if (distY <= -2) + { + Point locSwap = new Point(locBlank.getX(), locBlank.getY() - 1); + swap(locBlank, locSwap); + } + } + } + else if (distY == 0) + { + //Same row + if (distAbsX == 1) + { + //Next to + reached = true; + } + else + { + //More than 2 away, move towards on X-axis + if (distX >= 2) + { + Point locSwap = new Point(locBlank.getX() + 1, locBlank.getY()); + swap(locBlank, locSwap); + } + else if (distX <= -2) + { + Point locSwap = new Point(locBlank.getX() - 1, locBlank.getY()); + swap(locBlank, locSwap); + } + } + } + else + { + //Different row and column + if (rowMode) + { + //Check if already correct above + if (locBlank.getY() - 1 == y + && validRowNumbers.get(y).contains(currentState.getPiece(locBlank.getX(), locBlank.getY() - 1)) + && currentState.getPiece(locBlank.getX(), locBlank.getY() - 1) < valTarget + && distY <= -1) + { + //Move forward + Point locSwap = new Point(locBlank.getX() + 1, locBlank.getY()); + swap(locBlank, locSwap); + continue; + } + + //Move downwards or upwards + if (distY >= 1) + { + Point locSwap = new Point(locBlank.getX(), locBlank.getY() + 1); + swap(locBlank, locSwap); + } + else if (distY <= -1) + { + Point locSwap = new Point(locBlank.getX(), locBlank.getY() - 1); + swap(locBlank, locSwap); + } + } + else + { + //Check if already correct to the left + if (locBlank.getX() - 1 == x + && validColumnNumbers.get(x).contains(currentState.getPiece(locBlank.getX() - 1, locBlank.getY())) + && currentState.getPiece(locBlank.getX() - 1, locBlank.getY()) < valTarget + && distX <= -1) + { + //Move down + Point locSwap = new Point(locBlank.getX(), locBlank.getY() + 1); + swap(locBlank, locSwap); + continue; + } + + //Move right or left + if (distX >= 1) + { + Point locSwap = new Point(locBlank.getX() + 1, locBlank.getY()); + swap(locBlank, locSwap); + } + else if (distX <= -1) + { + Point locSwap = new Point(locBlank.getX() - 1, locBlank.getY()); + swap(locBlank, locSwap); + } + } + } + } + } + } + + private void alignTargetX(int valTarget, int x, int y) + { + Point locVal = findPiece(valTarget); + + //Check if same column + if (locVal.getX() == x) + { + return; + } + + //1 = right, -1 = left + int direction = Integer.signum(x - locVal.getX()); + + while (locVal.getX() != x) + { + locVal = findPiece(valTarget); + Point locBlank = findPiece(BLANK_TILE_VALUE); + + //Check if aligned + if (x - locVal.getX() == 0) + { + break; + } + + if (locVal.getX() == locBlank.getX()) + { + int diff = locBlank.getY() - locVal.getY(); + if (diff == 1) + { + //Below + Point loc1 = new Point(locBlank.getX() + direction, locBlank.getY()); + Point loc2 = new Point(loc1.getX(), loc1.getY() - 1); + + swap(locBlank, loc1); + swap(loc1, loc2); + swap(loc2, locVal); + } + else if (diff == -1) + { + //Above + swap(locBlank, locVal); + } + } + else if (locVal.getY() == locBlank.getY()) + { + int diff = locBlank.getX() - locVal.getX(); + if (diff == 1) + { + //Right + if (direction == 1) + { + swap(locVal, locBlank); + } + else if (direction == -1) + { + //Check space + if (locVal.getY() == DIMENSION - 1) + { + //No space below, use upper rotate + performSwapPattern(locBlank, locVal, ROTATE_LEFT_UP); + } + else + { + //Space below, use lower rotate + performSwapPattern(locBlank, locVal, ROTATE_LEFT_DOWN); + } + } + } + else if (diff == -1) + { + //Left + if (direction == -1) + { + swap(locVal, locBlank); + } + else if (direction == 1) + { + //Check space + if (locVal.getY() == DIMENSION - 1) + { + //No space below, use upper rotate + performSwapPattern(locBlank, locVal, ROTATE_RIGHT_UP); + } + else + { + //Space below, use lower rotate + performSwapPattern(locBlank, locVal, ROTATE_RIGHT_DOWN); + } + } + } + } + } + } + + //Swaps up until inserted into the correct place + private void swapUpRow(int valTarget, int x, int y) + { + Point locVal = findPiece(valTarget); + Point locBlank = findPiece(BLANK_TILE_VALUE); + + //Check if already placed correct + if (locVal.getX() == x && locVal.getY() == y) + { + return; + } + + //Check if simple swap is enough + if (locBlank.getX() == x && locBlank.getY() == y && locVal.getY() - 1 == y) + { + swap(locBlank, locVal); + return; + } + + //Move up + while (true) + { + locVal = findPiece(valTarget); + locBlank = findPiece(BLANK_TILE_VALUE); + + //Check if already placed correct + if (locVal.getX() == x && locVal.getY() == y) + { + return; + } + + if (locVal.getX() == locBlank.getX()) + { + int diff = locBlank.getY() - locVal.getY(); + if (diff == 1) + { + //Below + + //Last piece + if (x == DIMENSION - 1) + { + performSwapPattern(locBlank, locVal, LAST_PIECE_ROW); + return; + } + + performSwapPattern(locBlank, locVal, ROTATE_UP_RIGHT); + } + else if (diff == -1) + { + //Above + swap(locBlank, locVal); + } + } + else if (locVal.getY() == locBlank.getY()) + { + int diff = locBlank.getX() - locVal.getX(); + if (diff == 1) + { + //Right + performSwapPattern(locBlank, locVal, SHUFFLE_UP_RIGHT); + } + else if (diff == -1) + { + //Left + + //Don't remove correct pieces from row + if (locVal.getY() - 1 == y) + { + //Swap blank to below and continue + Point loc1 = new Point(locBlank.getX(), locBlank.getY() + 1); + Point loc2 = new Point(loc1.getX() + 1, loc1.getY()); + + swap(locBlank, loc1); + swap(loc1, loc2); + + continue; + } + + performSwapPattern(locBlank, locVal, SHUFFLE_UP_LEFT); + } + } + } + } + + private void alignTargetY(int valTarget, int x, int y) + { + Point locVal = findPiece(valTarget); + + //Check if same row + if (locVal.getY() == y) + { + return; + } + + //1 = down, -1 = up + int direction = Integer.signum(y - locVal.getY()); + + while (locVal.getY() != y) + { + locVal = findPiece(valTarget); + Point locBlank = findPiece(BLANK_TILE_VALUE); + + //Check if aligned + if (y - locVal.getY() == 0) + { + break; + } + + if (locVal.getY() == locBlank.getY()) + { + int diff = locBlank.getX() - locVal.getX(); + if (diff == 1) + { + //Right + Point loc1 = new Point(locBlank.getX(), locBlank.getY() + direction); + Point loc2 = new Point(loc1.getX() - 1, loc1.getY()); + + swap(locBlank, loc1); + swap(loc1, loc2); + swap(loc2, locVal); + } + else if (diff == -1) + { + //Left + swap(locBlank, locVal); + } + } + else if (locVal.getX() == locBlank.getX()) + { + int diff = locBlank.getY() - locVal.getY(); + if (diff == 1) + { + //Below + if (direction == 1) + { + swap(locVal, locBlank); + } + else if (direction == -1) + { + //Check space + if (locVal.getX() == DIMENSION - 1) + { + //No space to the right, use left rotate + performSwapPattern(locBlank, locVal, ROTATE_UP_LEFT); + } + else + { + //Space to the right, use right rotate + performSwapPattern(locBlank, locVal, ROTATE_UP_RIGHT); + } + } + } + else if (diff == -1) + { + //Above + if (direction == -1) + { + swap(locVal, locBlank); + } + else if (direction == 1) + { + //Check space + if (locVal.getX() == DIMENSION - 1) + { + //No space to the right, use left rotate + performSwapPattern(locBlank, locVal, ROTATE_DOWN_LEFT); + } + else + { + //Space to the right, use right rotate + performSwapPattern(locBlank, locVal, ROTATE_DOWN_RIGHT); + } + } + } + } + } + } + + //Swaps left until inserted into the correct place + private void swapLeftColumn(int valTarget, int x, int y) + { + Point locVal = findPiece(valTarget); + Point locBlank = findPiece(BLANK_TILE_VALUE); + + //Check if already placed correct + if (locVal.getX() == x && locVal.getY() == y) + { + return; + } + + //Check if simple swap is enough + if (locBlank.getX() == x && locBlank.getY() == y && locVal.getX() - 1 == x) + { + swap(locBlank, locVal); + return; + } + + //Move left + while (true) + { + locVal = findPiece(valTarget); + locBlank = findPiece(BLANK_TILE_VALUE); + + //Check if already placed correct + if (locVal.getX() == x && locVal.getY() == y) + { + return; + } + + if (locVal.getX() == locBlank.getX()) + { + int diff = locBlank.getY() - locVal.getY(); + if (diff == 1) + { + //Below + performSwapPattern(locBlank, locVal, SHUFFLE_UP_BELOW); + } + else if (diff == -1) + { + //Above + + //Don't remove correct pices from row + if (locVal.getX() - 1 == x) + { + //Swap blank to right and continue + Point loc1 = new Point(locBlank.getX() + 1, locBlank.getY()); + Point loc2 = new Point(loc1.getX(), loc1.getY() + 1); + + swap(locBlank, loc1); + swap(loc1, loc2); + + continue; + } + + performSwapPattern(locBlank, locVal, SHUFFLE_UP_ABOVE); + } + } + else if (locVal.getY() == locBlank.getY()) + { + int diff = locBlank.getX() - locVal.getX(); + if (diff == 1) + { + //Right + + //Last piece + if (y == DIMENSION - 1) + { + performSwapPattern(locBlank, locVal, LAST_PIECE_COLUMN); + return; + } + + performSwapPattern(locBlank, locVal, ROTATE_LEFT_DOWN); + } + else if (diff == -1) + { + //Left + swap(locBlank, locVal); + } + } + } + } + + private void swap(Point p1, Point p2) + { + PuzzleState newState = currentState.swap(p1.getX(), p1.getY(), p2.getX(), p2.getY()); + + currentState = newState; + stateList.add(newState); + } + + private Point findPiece(int val) + { + for (int x = 0; x < DIMENSION; x++) + { + for (int y = 0; y < DIMENSION; y++) + { + if (currentState.getPiece(x, y) == val) + { + return new Point(x, y); + } + } + } + // This should never happen + throw new IllegalStateException("Piece wasn't found!"); + } + + /** + * Assumes locBlank is first point for swap and locVal is last point for swap + * + * swap(locBlank, loc1); + * swap(loc1, loc2); + * swap(loc2, locVal); + */ + private void performSwapPattern(Point locBlank, Point locVal, PuzzleSwapPattern pattern) + { + int[] offsets; + switch (pattern) + { + case ROTATE_LEFT_UP: + case ROTATE_RIGHT_UP: + case ROTATE_RIGHT_DOWN: + case ROTATE_LEFT_DOWN: + offsets = ROTATE_LEFT_UP.getPoints(); + break; + case ROTATE_UP_LEFT: + case ROTATE_UP_RIGHT: + case ROTATE_DOWN_LEFT: + case ROTATE_DOWN_RIGHT: + offsets = ROTATE_UP_LEFT.getPoints(); + break; + default: + offsets = pattern.getPoints(); + } + + if (offsets == null || offsets.length % 2 == 1) + { + // This should never happen + throw new IllegalStateException("Unexpected points given in pattern!"); + } + + int modX = pattern.getModX(); + int modY = pattern.getModY(); + + ArrayList points = new ArrayList<>(); + + for (int i = 0; i < offsets.length; i += 2) + { + int x = locVal.getX() + modX * offsets[i]; + int y = locVal.getY() + modY * offsets[i + 1]; + + points.add(new Point(x, y)); + } + + // Add locVal as last point + points.add(locVal); + + if (pattern != LAST_PIECE_ROW && pattern != LAST_PIECE_COLUMN) + { + Point start = locBlank; + for (Point p : points) + { + swap(start, p); + start = p; + } + } + else + { + Point loc1 = points.get(0); + Point loc2 = points.get(1); + Point loc3 = points.get(2); + Point loc4 = points.get(3); + + swap(locBlank, locVal); + swap(locVal, loc3); + swap(loc3, loc1); + swap(loc1, loc2); + swap(loc2, locVal); + swap(locVal, loc3); + swap(loc3, loc1); + swap(loc1, loc2); + swap(loc2, locVal); + swap(locVal, locBlank); + swap(locBlank, loc4); + swap(loc4, loc3); + swap(loc3, loc1); + swap(loc1, loc2); + swap(loc2, locVal); + } + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverTest.java index af14ee8ebb..6a09ff0f71 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverTest.java @@ -29,6 +29,7 @@ import net.runelite.client.plugins.puzzlesolver.solver.PuzzleSolver; import net.runelite.client.plugins.puzzlesolver.solver.PuzzleState; import net.runelite.client.plugins.puzzlesolver.solver.heuristics.ManhattanDistance; import net.runelite.client.plugins.puzzlesolver.solver.pathfinding.IDAStar; +import net.runelite.client.plugins.puzzlesolver.solver.pathfinding.IDAStarMM; import org.junit.Test; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -83,8 +84,72 @@ public class PuzzleSolverTest new PuzzleState(new int[]{1, 5, 2, 3, 4, -1, 0, 7, 14, 8, 11, 6, 13, 9, 23, 10, 12, 15, 19, 17, 20, 21, 16, 22, 18}), }; + private static final PuzzleState[] START_STATES_MM = + { + new PuzzleState(new int[]{0, 5, 1, 3, 4, 15, 2, 8, 10, 9, 22, 16, 11, 6, 14, 7, 21, 23, 12, 18, 20, -1, 17, 13, 19}), + new PuzzleState(new int[]{0, 12, 8, 13, 4, 3, 16, 2, 1, 9, 21, 5, 6, 10, 14, 7, 17, 20, 18, -1, 15, 11, 22, 23, 19}), + new PuzzleState(new int[]{1, 3, 7, 9, 8, 13, 17, 2, 4, 6, 15, 5, 22, 12, 14, 11, 0, 10, 21, -1, 20, 18, 16, 23, 19}), + new PuzzleState(new int[]{5, 2, 16, 14, 4, 3, 0, 8, 9, 11, 15, 6, 17, 19, 7, 1, 10, -1, 23, 18, 20, 12, 21, 13, 22}), + new PuzzleState(new int[]{0, 6, 1, 4, 13, 10, 2, 16, 7, 3, 20, -1, 8, 14, 9, 21, 5, 18, 11, 19, 17, 15, 12, 22, 23}), + new PuzzleState(new int[]{5, 0, 1, 4, 8, 10, 6, 7, 12, 3, 17, 16, 21, 2, 9, 18, 20, 13, 14, 19, 11, -1, 23, 15, 22}), + new PuzzleState(new int[]{1, 9, 2, 13, 17, 5, 7, 8, 3, 22, 6, -1, 16, 12, 4, 15, 18, 0, 23, 14, 10, 21, 11, 20, 19}), + new PuzzleState(new int[]{1, 2, 11, 13, 4, 21, 7, 3, 6, 9, 0, 8, 10, 19, 14, 20, 12, 16, 23, -1, 5, 17, 15, 22, 18}), + new PuzzleState(new int[]{2, 0, 1, 4, 13, 6, 7, 3, 8, 9, 22, 15, 10, 14, 18, 5, 12, -1, 17, 21, 20, 11, 23, 16, 19}), + new PuzzleState(new int[]{0, 1, 2, 8, 3, 6, 12, 22, 9, 7, 11, 21, 13, 4, 14, 5, 10, -1, 18, 19, 20, 15, 16, 23, 17}), + new PuzzleState(new int[]{1, 2, 3, 4, 8, 0, 6, 15, 14, 18, 16, 17, 20, -1, 9, 10, 12, 22, 11, 13, 21, 7, 5, 19, 23}), + new PuzzleState(new int[]{0, 5, 2, 4, 9, 7, 15, 20, 12, 13, 6, -1, 22, 1, 8, 10, 11, 23, 14, 3, 21, 16, 17, 19, 18}), + new PuzzleState(new int[]{0, 1, 9, 6, 13, 5, 18, -1, 4, 2, 15, 12, 3, 17, 7, 16, 10, 8, 23, 14, 20, 21, 19, 11, 22}), + new PuzzleState(new int[]{11, 5, 12, 3, 4, 15, 8, 0, 7, 1, 6, -1, 19, 2, 9, 16, 10, 13, 17, 23, 20, 21, 22, 14, 18}), + new PuzzleState(new int[]{10, 0, 1, 3, 4, 18, 5, 6, 12, 9, 7, 11, 8, -1, 22, 15, 23, 14, 19, 13, 20, 2, 17, 16, 21}), + new PuzzleState(new int[]{19, -1, 6, 2, 4, 0, 21, 10, 3, 9, 1, 15, 17, 8, 14, 11, 13, 22, 7, 18, 16, 12, 5, 23, 20}), + new PuzzleState(new int[]{11, 6, 3, 4, 9, 1, 10, 16, 2, 7, 5, 0, 13, -1, 12, 21, 8, 18, 17, 14, 15, 20, 22, 23, 19}), + new PuzzleState(new int[]{0, 1, 5, 3, 4, -1, 6, 2, 15, 10, 7, 8, 23, 16, 13, 22, 11, 9, 12, 14, 20, 21, 18, 17, 19}), + new PuzzleState(new int[]{10, 0, 1, -1, 2, 6, 5, 4, 13, 9, 16, 17, 12, 8, 19, 20, 15, 7, 21, 11, 22, 18, 14, 23, 3}), + new PuzzleState(new int[]{1, 0, 5, 3, 9, 20, 15, 7, 2, 14, 6, 4, 12, -1, 8, 13, 18, 10, 23, 11, 21, 16, 17, 19, 22}), + new PuzzleState(new int[]{0, 7, 6, 3, 4, 15, 1, 2, 8, 18, 11, 5, 13, -1, 22, 17, 16, 23, 14, 9, 20, 10, 12, 19, 21}), + new PuzzleState(new int[]{5, 7, 0, 2, 9, 10, 1, 11, 3, 4, 16, 22, 8, 14, 17, 15, 20, 12, 13, 6, 21, 23, 19, -1, 18}), + new PuzzleState(new int[]{3, 0, 1, 5, 4, 11, 6, 2, 16, 9, 15, 10, 7, 12, 13, 21, 19, -1, 22, 8, 20, 17, 14, 18, 23}), + new PuzzleState(new int[]{6, 0, 3, 2, 4, 5, 1, 8, 13, 12, 15, 14, 10, 7, 9, -1, 22, 11, 19, 23, 16, 20, 17, 21, 18}), + new PuzzleState(new int[]{11, 5, 6, 8, 9, 0, 21, 16, 4, 3, 17, 18, 2, 7, 1, 15, 10, -1, 22, 14, 20, 19, 13, 12, 23}), + new PuzzleState(new int[]{2, 18, 3, 11, 4, -1, 5, 6, 12, 1, 10, 20, 0, 7, 9, 21, 15, 14, 23, 19, 16, 22, 13, 8, 17}), + new PuzzleState(new int[]{0, 6, 8, 3, 1, 5, 2, 12, 9, 13, 16, 14, 19, 7, 18, 10, 11, -1, 4, 15, 20, 17, 23, 21, 22}), + new PuzzleState(new int[]{1, 16, 10, 4, 3, 0, 15, 2, 9, -1, 8, 5, 23, 12, 6, 21, 18, 14, 13, 11, 20, 22, 7, 19, 17}), + new PuzzleState(new int[]{1, 7, 6, 3, 4, 0, 2, 14, 5, 22, 18, 21, 16, 9, 13, 10, 20, -1, 8, 17, 15, 23, 11, 19, 12}), + new PuzzleState(new int[]{5, 0, 1, 7, 9, 11, 8, 4, 2, 14, 15, 17, 18, -1, 3, 20, 10, 12, 22, 19, 16, 6, 13, 21, 23}), + new PuzzleState(new int[]{5, 0, 6, 14, 7, 13, 15, 1, 3, 10, 20, 9, 17, 4, 2, 11, 12, 8, 19, -1, 21, 16, 22, 18, 23}), + new PuzzleState(new int[]{12, 7, 8, 4, 9, 6, 11, 15, 2, 1, 5, -1, 13, 16, 3, 17, 0, 10, 18, 14, 20, 22, 21, 19, 23}), + new PuzzleState(new int[]{15, 1, 2, 3, 14, -1, 20, 9, 4, 19, 0, 6, 7, 16, 13, 10, 5, 12, 17, 18, 22, 11, 21, 23, 8}), + new PuzzleState(new int[]{0, 1, 17, -1, 14, 6, 4, 2, 3, 16, 10, 18, 13, 19, 9, 7, 5, 8, 21, 22, 11, 20, 15, 12, 23}), + new PuzzleState(new int[]{5, 11, 9, 0, 3, 8, 14, -1, 6, 4, 1, 13, 7, 2, 19, 10, 21, 18, 23, 17, 15, 20, 12, 16, 22}), + new PuzzleState(new int[]{2, 0, 14, -1, 4, 18, 1, 10, 12, 13, 5, 9, 11, 22, 7, 15, 8, 17, 19, 3, 20, 21, 6, 16, 23}), + new PuzzleState(new int[]{0, 1, 13, 9, 2, 6, 8, 22, 3, 4, 12, 16, 10, 7, 19, -1, 5, 11, 14, 17, 15, 20, 21, 18, 23}), + new PuzzleState(new int[]{0, 13, 17, 8, 3, 5, 1, 12, 14, 4, 10, -1, 6, 7, 9, 15, 23, 2, 16, 19, 20, 11, 21, 22, 18}), + new PuzzleState(new int[]{5, 10, 7, 2, 9, 15, 0, -1, 1, 3, 18, 4, 17, 12, 14, 21, 11, 6, 8, 23, 20, 16, 22, 19, 13}), + new PuzzleState(new int[]{0, 3, 1, 2, 4, 10, 5, 7, 8, 9, 11, 6, 21, 13, 12, 20, 17, -1, 14, 19, 22, 18, 15, 16, 23}), + new PuzzleState(new int[]{0, 2, 7, 11, 13, 3, 14, 1, 4, 9, 5, -1, 12, 8, 18, 20, 10, 15, 22, 23, 17, 16, 6, 21, 19}), + new PuzzleState(new int[]{0, 16, 3, 22, 7, 11, 6, -1, 9, 4, 2, 1, 13, 12, 18, 5, 10, 8, 19, 14, 15, 20, 17, 23, 21}), + new PuzzleState(new int[]{0, 13, 5, 12, 3, 2, 10, 4, 6, 8, 1, 21, 19, 14, 9, 17, 23, 22, 16, 11, 15, 7, 20, -1, 18}), + new PuzzleState(new int[]{14, 5, 6, 12, 4, 10, 20, 1, 0, 23, 2, 16, 13, 19, 3, 15, 22, -1, 9, 8, 11, 7, 18, 17, 21}), + new PuzzleState(new int[]{0, 1, 2, 4, 7, 5, 11, -1, 18, 8, 16, 10, 12, 13, 3, 17, 6, 21, 23, 9, 15, 20, 22, 14, 19}), + new PuzzleState(new int[]{1, 6, 7, 3, 4, 5, 17, 0, 22, 12, 10, 15, 8, -1, 14, 11, 13, 16, 18, 19, 20, 2, 21, 9, 23}), + }; + private static final int[] FINISHED_STATE = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, -1}; + @Test + public void testSolverMM() + { + for (PuzzleState state : START_STATES_MM) + { + PuzzleSolver solver = new PuzzleSolver(new IDAStarMM(new ManhattanDistance()), state); + solver.run(); + + assertTrue(solver.hasSolution()); + assertFalse(solver.hasFailed()); + assertTrue(solver.getStep(solver.getStepCount() - 1).hasPieces(FINISHED_STATE)); + } + } + @Test public void testSolver() {