From 9fb2ba3fda8d02ef3dfdb1cccd98f4e7c1feaf3c Mon Sep 17 00:00:00 2001 From: Lotto Date: Fri, 23 Feb 2018 23:25:03 +0100 Subject: [PATCH] puzzlesolver: use the new solver and display Solving.. + Solved! msgs --- .../plugins/puzzlesolver/PuzzleSolver.java | 346 ------------------ .../puzzlesolver/PuzzleSolverOverlay.java | 251 ++++++++----- .../puzzlesolver/PuzzleSolverPlugin.java | 4 + 3 files changed, 162 insertions(+), 439 deletions(-) delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolver.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolver.java b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolver.java deleted file mode 100644 index 79d222f785..0000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolver.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * This code file is a derivative of code by Jaap Scherphuis . - * The original code can be found in the page source of https://www.jaapsch.net/puzzles/javascript/fifteenj.htm. - * - * Quote copied verbatim from the creator's grant to use this code in RuneLite: - * - * "You have my permission to incorporate, adapt, and distribute any part - * of my 15-puzzle javascript code as part of your free, open source project" - * - * - * The adaptions made to Jaap Scherphuis' code is copyrighted and licensed according to: - * - * - * Copyright (c) 2018, Lotto - * 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 HOLDER 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; - -import java.util.ArrayList; -import java.util.List; - -/** - * This 5x5 sliding puzzle solver algorithm is based on https://www.jaapsch.net/puzzles/fifteen.htm. - */ -public class PuzzleSolver -{ - public static final int BLANK_TILE_VALUE = -1; - public static final int DIMENSION = 5; - private static final int SIZE = DIMENSION * DIMENSION - 1; - - private int[] tiles = new int[SIZE + DIMENSION]; - private List seq = new ArrayList<>(); - - private int blankX; - private int blankY; - - private int mode; - - public PuzzleSolver(int[] arr) - { - System.arraycopy(arr, 0, tiles, 0, arr.length); - - for (int i = 0; i < arr.length; i++) - { - if (arr[i] == BLANK_TILE_VALUE) - { - blankX = i % DIMENSION; - blankY = i / DIMENSION; - break; - } - } - } - - public boolean hasNext() - { - for (int i = SIZE; i >= 0; i--) - { - if (tiles[i] != i) - { - return true; - } - } - return false; - } - - public void next() - { - if (mode == 0) - { - mode = 3; - seq.clear(); - - int[] oldTiles = new int[tiles.length]; - System.arraycopy(tiles, 0, oldTiles, 0, tiles.length); - int oldBlankX = blankX; - int oldBlankY = blankY; - - //restore top rows - int rr = 0; - for (int r = 0; r < DIMENSION - 2; r++) - { - for (int c = 0; c < DIMENSION; c++) - { - movePiece(rr + c, r, c); - } - rr += DIMENSION; - } - - //restore left columns - for (int c = 0; c < DIMENSION - 2; c++) - { - //restore top tile of column. - movePiece(rr, DIMENSION - 2, c); - //restore bottom tile of column - if (blankX == c) push(3); //fill destination spot - if (tiles[rr + DIMENSION] != rr + DIMENSION) - { - movePiece(rr + DIMENSION, DIMENSION - 1, c + 1); - if (blankY != DIMENSION - 1) - { //0=right, 1=down, 2=up, 3=left - //A.X or AX. - //XBX XBX - if (blankX == c + 1) push(3); - push(2); - } - //AXX - //XB. - while (blankX > c + 2) - { - push(0); - } - push(0, 0, 1, 3, 2, 3, 1, 0, 0, 2, 3); - } - rr++; - } - - //last 2x2 square - if (blankX < DIMENSION - 1) push(3); - if (blankY < DIMENSION - 1) push(2); - rr = SIZE - DIMENSION - 1; - if (tiles[rr] == rr + 1) push(1, 0, 2, 3); - if (tiles[rr] == rr + DIMENSION) push(0, 1, 3, 2); - - //restore pieces; - System.arraycopy(oldTiles, 0, tiles, 0, tiles.length); - blankX = oldBlankX; - blankY = oldBlankY; - } - - if (mode >= 3) - { - if (!seq.isEmpty()) - { - Integer c = seq.get(0); - for (int i = 1; i < seq.size(); i++) - { - seq.set(i - 1, seq.get(i)); - } - seq.remove(seq.size() - 1); - doMove(c); - } - - if (seq.isEmpty()) - { - mode = 0; - } - } - } - - private void movePiece(int p, int y, int x) - { - //moves piece p to position y,x without disturbing previously placed pieces. - int c = -1; - int i = 0; - int j = 0; - for (; i < DIMENSION; i++) - { - for (j = 0; j < DIMENSION; j++) - { - c++; - if (tiles[c] == p) break; - } - if (tiles[c] == p) break; - } - - //Move piece to same column //0=right, 1=down, 2=up, 3=left - if (j < x && blankY == y) push(2); // move blank down if it might disturb solved pieces. - while (j > x) - { - //move piece to left - //First move blank to left hand side of it - if (blankY == i && blankX > j) - { //if blank on wrong side of piece - if (i == DIMENSION - 1) push(1); - else push(2); //then move it to other row - } - while (blankX >= j) push(0); // move blank to column left of piece - while (blankX < j - 1) push(3); - while (blankY < i) push(2); // move blank to same row as piece - while (blankY > i) push(1); - push(3); // move piece to left. - j--; - } - while (j < x) - { - //move piece to right - //First move blank to right hand side of it - if (blankY == i && blankX < j) - { - if (i == DIMENSION - 1) push(1); - else push(2); - } - while (blankX <= j) push(3); - while (blankX > j + 1) push(0); - while (blankY < i) push(2); - while (blankY > i) push(1); - push(0); - j++; - } - - //Move piece up to same row //0=right, 1=down, 2=up, 3=left - while (i > y) - { - if (y < i - 1) - { - while (blankY < i - 1) push(2); - if (blankX == j) push(j == DIMENSION - 1 ? 0 : 3); - while (blankY > i - 1) push(1); - while (blankX < j) push(3); - while (blankX > j) push(0); - push(2); - } - else - { - if (j != DIMENSION - 1) - { - if (blankY == i) push(2); - while (blankX < j + 1) push(3); - while (blankX > j + 1) push(0); - while (blankY > i - 1) push(1); - while (blankY < i - 1) push(2); - push(0, 2); - } - else - { - if (blankY < i && blankX == j) - { - while (blankY < i) push(2); - } - else - { - while (blankY > i + 1) push(1); - while (blankY < i + 1) push(2); - while (blankX < j) push(3); - while (blankX > j) push(0); - push(1, 1, 0, 2, 3, 2, 0, 1, 1, 3, 2); - } - } - } - i--; - } - while (i < y) - { - //move piece downwards - //First move blank below tile - if (blankX == j && blankY < i) - { - if (j == DIMENSION - 1) push(0); - else push(3); - } - while (blankY > i + 1) push(1); - while (blankY < i + 1) push(2); - while (blankX < j) push(3); - while (blankX > j) push(0); - push(1); - i++; - } - } - - private void push(int... moves) - { - for (int c : moves) - { - if (!seq.isEmpty() && seq.get(seq.size() - 1) + c == 3) - { - seq.remove(seq.size() - 1); - } - else - { - seq.add(c); - } - - doMove(c); - } - } - - private void doMove(int m) - { - int d = blankX + blankY * DIMENSION; - - if (m == 0) - { - // RIGHT - tiles[d] = tiles[d - 1]; - tiles[d - 1] = SIZE; - blankX--; - } - else if (m == 1) - { - // DOWN - tiles[d] = tiles[d - DIMENSION]; - tiles[d - DIMENSION] = SIZE; - blankY--; - } - else if (m == 2) - { - // UP - tiles[d] = tiles[d + DIMENSION]; - tiles[d + DIMENSION] = SIZE; - blankY++; - } - else if (m == 3) - { - // LEFT - tiles[d] = tiles[d + 1]; - tiles[d + 1] = SIZE; - blankX++; - } - } - - public int[] getTiles() - { - return tiles; - } - - public int getBlankX() - { - return blankX; - } - - public int getBlankY() - { - return blankY; - } -} 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 96c728ffe9..12d559c07b 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, Lotto + * Copyright (c) 2018, Henke * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -35,9 +36,9 @@ import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayDeque; import java.util.Arrays; -import java.util.Deque; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; import javax.imageio.ImageIO; import javax.inject.Inject; import lombok.extern.slf4j.Slf4j; @@ -48,7 +49,10 @@ import net.runelite.api.Item; import net.runelite.api.ItemContainer; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; -import static net.runelite.client.plugins.puzzlesolver.PuzzleSolver.BLANK_TILE_VALUE; +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.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayPosition; @@ -60,10 +64,10 @@ import net.runelite.client.ui.overlay.components.TextComponent; @Slf4j public class PuzzleSolverOverlay extends Overlay { - private static final int MOVES_LEFT_WIDTH = 100; - private static final int MOVES_LEFT_OFFSET_Y = 50; - private static final int MOVES_LEFT_TOP_BORDER = 2; - private static final int MOVES_LEFT_BOTTOM_BORDER = 2; + private static final int INFO_BOX_WIDTH = 100; + private static final int INFO_BOX_OFFSET_Y = 50; + private static final int INFO_BOX_TOP_BORDER = 2; + private static final int INFO_BOX_BOTTOM_BORDER = 2; private static final int PUZZLE_TILE_SIZE = 39; @@ -72,9 +76,10 @@ public class PuzzleSolverOverlay extends Overlay private final Client client; private final PuzzleSolverConfig config; + private final ScheduledExecutorService executorService; private PuzzleSolver solver; - private Deque nextMoves; + private Future solverFuture; private int[] cachedItems; private BufferedImage downArrow; @@ -83,13 +88,14 @@ public class PuzzleSolverOverlay extends Overlay private BufferedImage rightArrow; @Inject - public PuzzleSolverOverlay(Client client, PuzzleSolverConfig config) + public PuzzleSolverOverlay(Client client, PuzzleSolverConfig config, ScheduledExecutorService executorService) { setPosition(OverlayPosition.DYNAMIC); setPriority(OverlayPriority.HIGH); setLayer(OverlayLayer.ABOVE_WIDGETS); this.client = client; this.config = config; + this.executorService = executorService; } @Override @@ -115,117 +121,177 @@ public class PuzzleSolverOverlay extends Overlay return null; } + net.runelite.api.Point puzzleBoxLocation = puzzleBox.getCanvasLocation(); + + String infoString = "Solving.."; + int[] itemIds = getItemIds(container); boolean shouldCache = false; - if (nextMoves != null) + if (solver != null) { - Integer nextMove = nextMoves.peek(); - - // If this is true, nextMove is actually the current state (player has moved the empty tile), - // so let's throw away this step and try to find the actual next move. - while (nextMove != null && itemIds[nextMove] == BLANK_TILE_VALUE) + if (solver.hasFailed()) { - nextMoves.pop(); // pop the current move - nextMove = nextMoves.peek(); - shouldCache = true; + infoString = "The puzzle could not be solved"; } - - // If nextMove is null, the puzzle is either finished - // or the player has not followed the instructions. - if (nextMove != null) + else { - net.runelite.api.Point puzzleBoxLocation = puzzleBox.getCanvasLocation(); - - if (config.displayRemainingMoves()) + if (solver.hasSolution()) { - int x = puzzleBoxLocation.getX() + puzzleBox.getWidth() / 2 - MOVES_LEFT_WIDTH / 2; - int y = puzzleBoxLocation.getY() - MOVES_LEFT_OFFSET_Y; + boolean foundPosition = false; - String movesLeftString = "Moves left: " + nextMoves.size(); - - FontMetrics fm = graphics.getFontMetrics(); - int height = MOVES_LEFT_TOP_BORDER + fm.getHeight() + MOVES_LEFT_BOTTOM_BORDER; - - BackgroundComponent backgroundComponent = new BackgroundComponent(); - backgroundComponent.setRectangle(new Rectangle(x, y, MOVES_LEFT_WIDTH, height)); - backgroundComponent.render(graphics, parent); - - int textOffsetX = (MOVES_LEFT_WIDTH - fm.stringWidth(movesLeftString)) / 2; - int textOffsetY = fm.getHeight(); - - TextComponent textComponent = new TextComponent(); - textComponent.setPosition(new Point(x + textOffsetX, y + textOffsetY)); - textComponent.setText(movesLeftString); - textComponent.render(graphics, parent); - } - - if (config.displaySolution()) - { - int i = 0; - int lastBlankX = 0; - int lastBlankY = 0; - - // First find the current blank tile position - for (int j = 0; j < itemIds.length; j++) + // Find the current state by looking at the current step and then the next 3 steps + for (int i = 0; i < 4; i++) { - if (itemIds[j] == BLANK_TILE_VALUE) + int j = solver.getPosition() + i; + + if (j == solver.getStepCount()) { - lastBlankX = j % PuzzleSolver.DIMENSION; - lastBlankY = j / PuzzleSolver.DIMENSION; + break; + } + + Integer currentState = solver.getStep(j); + + // If this is false, player has moved the empty tile + if (currentState != null && itemIds[currentState] == BLANK_TILE_VALUE) + { + foundPosition = true; + solver.setPosition(j); + if (i > 0) + { + shouldCache = true; + } break; } } - for (Integer futureMove : nextMoves) + // If looking at the next steps didn't find the current state, + // see if we can find the current state in the 3 previous steps + if (!foundPosition) { - int blankX = futureMove % PuzzleSolver.DIMENSION; - int blankY = futureMove / PuzzleSolver.DIMENSION; + for (int i = 1; i < 4; i++) + { + int j = solver.getPosition() - i; - int xDelta = blankX - lastBlankX; - int yDelta = blankY - lastBlankY; + if (j < 0) + { + break; + } - BufferedImage arrow; - if (xDelta > 0) - { - arrow = getRightArrow(); + Integer currentState = solver.getStep(j); + + if (currentState != null && itemIds[currentState] == BLANK_TILE_VALUE) + { + foundPosition = true; + shouldCache = true; + solver.setPosition(j); + break; + } } - else if (xDelta < 0) + } + + if (foundPosition) + { + int stepsLeft = solver.getStepCount() - solver.getPosition() - 1; + + if (stepsLeft == 0) { - arrow = getLeftArrow(); + infoString = "Solved!"; } - else if (yDelta > 0) + else if (config.displayRemainingMoves()) { - arrow = getDownArrow(); + infoString = "Moves left: " + stepsLeft; } else { - arrow = getUpArrow(); + infoString = null; } - int x = puzzleBoxLocation.getX() + blankX * PUZZLE_TILE_SIZE - + PUZZLE_TILE_SIZE / 2 - arrow.getWidth() / 2; - - int y = puzzleBoxLocation.getY() + blankY * PUZZLE_TILE_SIZE - + PUZZLE_TILE_SIZE / 2 - arrow.getHeight() / 2; - - OverlayUtil.renderImageLocation(graphics, new net.runelite.api.Point(x, y), arrow); - - lastBlankX = blankX; - lastBlankY = blankY; - - if (++i == 3) + if (config.displaySolution()) { - break; + // Find the current blank tile position + Integer currentMove = solver.getStep(solver.getPosition()); + + int lastBlankX = currentMove % DIMENSION; + int lastBlankY = currentMove / DIMENSION; + + // Display the next 3 steps + for (int j = 1; j < 4; j++) + { + Integer futureMove = solver.getStep(solver.getPosition() + j); + + if (futureMove == null) + { + break; + } + + int blankX = futureMove % DIMENSION; + int blankY = futureMove / DIMENSION; + + int xDelta = blankX - lastBlankX; + int yDelta = blankY - lastBlankY; + + BufferedImage arrow; + if (xDelta > 0) + { + arrow = getRightArrow(); + } + else if (xDelta < 0) + { + arrow = getLeftArrow(); + } + else if (yDelta > 0) + { + arrow = getDownArrow(); + } + else + { + arrow = getUpArrow(); + } + + int x = puzzleBoxLocation.getX() + blankX * PUZZLE_TILE_SIZE + + PUZZLE_TILE_SIZE / 2 - arrow.getWidth() / 2; + + int y = puzzleBoxLocation.getY() + blankY * PUZZLE_TILE_SIZE + + PUZZLE_TILE_SIZE / 2 - arrow.getHeight() / 2; + + OverlayUtil.renderImageLocation(graphics, new net.runelite.api.Point(x, y), arrow); + + lastBlankX = blankX; + lastBlankY = blankY; + } } } } } } + // Draw info box + if (infoString != null) + { + int x = puzzleBoxLocation.getX() + puzzleBox.getWidth() / 2 - INFO_BOX_WIDTH / 2; + int y = puzzleBoxLocation.getY() - INFO_BOX_OFFSET_Y; + + FontMetrics fm = graphics.getFontMetrics(); + int height = INFO_BOX_TOP_BORDER + fm.getHeight() + INFO_BOX_BOTTOM_BORDER; + + BackgroundComponent backgroundComponent = new BackgroundComponent(); + backgroundComponent.setRectangle(new Rectangle(x, y, INFO_BOX_WIDTH, height)); + backgroundComponent.render(graphics, parent); + + int textOffsetX = (INFO_BOX_WIDTH - fm.stringWidth(infoString)) / 2; + int textOffsetY = fm.getHeight(); + + TextComponent textComponent = new TextComponent(); + textComponent.setPosition(new Point(x + textOffsetX, y + textOffsetY)); + textComponent.setText(infoString); + textComponent.render(graphics, parent); + } + + // Solve the puzzle if we don't have an up to date solution if (solver == null || cachedItems == null || (!shouldCache && !Arrays.equals(cachedItems, itemIds))) { - nextMoves = solve(itemIds); + solve(itemIds); shouldCache = true; } @@ -263,19 +329,18 @@ public class PuzzleSolverOverlay extends Overlay System.arraycopy(items, 0, cachedItems, 0, cachedItems.length); } - private Deque solve(int[] items) + private void solve(int[] items) { - Deque steps = new ArrayDeque<>(); - - solver = new PuzzleSolver(convertToSolverFormat(items)); - - while (solver.hasNext()) + if (solverFuture != null) { - solver.next(); - steps.add(solver.getBlankX() + solver.getBlankY() * PuzzleSolver.DIMENSION); + solverFuture.cancel(true); } - return steps; + int[] puzzleItems = convertToSolverFormat(items); + PuzzleState puzzleState = new PuzzleState(puzzleItems); + + solver = new PuzzleSolver(new IDAStar(new ManhattanDistance()), puzzleState); + solverFuture = executorService.submit(solver); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverPlugin.java index d246a3fde9..84750dcd9c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/puzzlesolver/PuzzleSolverPlugin.java @@ -26,6 +26,7 @@ package net.runelite.client.plugins.puzzlesolver; import com.google.inject.Provides; +import java.util.concurrent.ScheduledExecutorService; import javax.inject.Inject; import net.runelite.client.config.ConfigManager; import net.runelite.client.plugins.Plugin; @@ -39,6 +40,9 @@ public class PuzzleSolverPlugin extends Plugin @Inject private PuzzleSolverOverlay puzzleSolverOverlay; + @Inject + private ScheduledExecutorService executorService; + @Provides PuzzleSolverConfig provideConfig(ConfigManager configManager) {