commit 4048acf2d23e25c82fe61f36a78ea866049438ac Author: Charles Gould Date: Sat Sep 12 23:26:54 2020 -0400 Implement puzzle solver diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec10551 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target/ +.idea/ +*.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..98be793 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020 Charles Gould + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f31069 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +## Rush + +Quick and dirty solver for [Rush Hour](https://en.wikipedia.org/wiki/Rush_Hour_(puzzle)) sliding block puzzles. + +The game board is modeled as an array of integers with indices: + +``` + 0 1 2 3 4 5 + 6 7 8 9 10 11 +12 13 14 15 16 17 +18 19 20 21 22 23 +24 25 26 27 28 29 +30 31 32 33 34 35 +``` + +Cars are assigned different integers. + +``` + 1 1 1 2 3 4 + 5 6 6 2 3 4 + 5 0 7 7 3 4 + 8 8 9 0 0 0 + 0 10 9 11 11 0 + 0 10 12 12 13 13 +``` + +#### Running the solver + +Requirements: Java 8, Maven 3 + +1. `mvn clean compile` +2. `mvn exec:java` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..60f11c0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + com.charego + rush + 1.0-SNAPSHOT + + + UTF-8 + 1.4.10 + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + com.charego.rush.MainKt + + + + + diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..5d291a0 --- /dev/null +++ b/src/main/kotlin/Main.kt @@ -0,0 +1,63 @@ +package com.charego.rush + +import java.lang.RuntimeException + +fun main() { + val startPosition = loadBoard("/boards/board3.txt") + println("Input:") + printBoard(startPosition.board) + + val finalPosition = solve(startPosition, maxDepth = 50) + println("Solution (n=${finalPosition.previousBoards.size}):") + //for (board in finalPosition.previousBoards) { + // printBoard(board) + //} + printBoard(finalPosition.board) +} + +/* Breadth-first search */ +fun solve(startPosition: Position, maxDepth: Int): Position { + val visitedBoards = mutableSetOf(startPosition.id) + val queue = ArrayDeque(setOf(startPosition)) + + while (queue.isNotEmpty()) { + val position = queue.removeFirst() + if (position.isSolution()) { + return position + } + if (position.previousBoards.size + 1 >= maxDepth) { + continue + } + for (child in position.findAvailableMoves()) { + if (!visitedBoards.contains(child.id)) { + queue.addLast(child) + visitedBoards.add(child.id) + } + } + } + + // If we exit the loop without finding a solution, throw an exception + throw RuntimeException("No solution could be found up to $maxDepth moves!") +} + +fun loadBoard(resourceName: String): Position { + val boardString = {}::class.java.getResource(resourceName).readText().trim() + val board = boardString.split(Regex("\\s+")).map { x -> x.toInt() }.toIntArray() + return Position(board, emptyList()) +} + +fun printBoard(board: IntArray) { + fun printRow(startIndex: Int, endIndex: Int) { + val row = board.slice(startIndex..endIndex) + val fmt = " %2d %2d %2d %2d %2d %2d" + val res = fmt.format(*row.toTypedArray()) + println(res) + } + printRow(0, 5) + printRow(6, 11) + printRow(12, 17) + printRow(18, 23) + printRow(24, 29) + printRow(30, 35) + println() +} diff --git a/src/main/kotlin/Position.kt b/src/main/kotlin/Position.kt new file mode 100644 index 0000000..608c862 --- /dev/null +++ b/src/main/kotlin/Position.kt @@ -0,0 +1,135 @@ +package com.charego.rush + +/** + * Models a Rush Hour game board as an array of integers with indices: + * + * ``` + * 0 1 2 3 4 5 + * 6 7 8 9 10 11 + * 12 13 14 15 16 17 + * 18 19 20 21 22 23 + * 24 25 26 27 28 29 + * 30 31 32 33 34 35 + * ``` + */ +class Position(val board: IntArray, val previousBoards: List) { + val id = board.contentHashCode() + + fun isSolution(): Boolean { + return board[16] != 0 && board[16] == board[17] + } + + fun findAvailableMoves(): List { + val availableMoves = mutableListOf() + for ((spaceIndex, value) in board.withIndex()) { + if (value == 0) { + val rowIndex = spaceIndex / 6 + val colIndex = spaceIndex % 6 + //println("$spaceIndex: (${rowIndex}, ${colIndex})") + //println("ROW: ${board.sliceArray((rowIndex * 6) until ((rowIndex + 1) * 6)).contentToString()}") + //println("COL: ${board.slice(colIndex .. (30 + colIndex) step 6).toIntArray().contentToString()}") + if (colIndex > 1) { + val lookLeft = board.sliceArray((rowIndex * 6) until spaceIndex).reversedArray() + //println("LOOKING LEFT: ${lookLeft.contentToString()}") + val carData = findCar(lookLeft) + if (carData.size > 1) { + //println("CAR: $carData") + val newBoard = board.copyOf() + val startIndex = spaceIndex - carData.size - carData.distance + for (i in startIndex .. spaceIndex) { + newBoard[i] = 0 + } + for (i in (spaceIndex - carData.size + 1) .. spaceIndex) { + newBoard[i] = carData.number + } + //println("UPDATED ROW: ${newBoard.sliceArray((rowIndex * 6) until ((rowIndex + 1) * 6)).contentToString()}") + availableMoves.add(moveTo(newBoard)) + } + } + if (colIndex < 4) { + val lookRight = board.sliceArray((spaceIndex + 1) until ((rowIndex + 1) * 6)) + //println("LOOKING RIGHT: ${lookRight.contentToString()}") + val carData = findCar(lookRight) + if (carData.size > 1) { + //println("CAR: $carData") + val newBoard = board.copyOf() + val endIndex = spaceIndex + carData.size + carData.distance + for (i in spaceIndex .. endIndex) { + newBoard[i] = 0 + } + for (i in spaceIndex until (spaceIndex + carData.size)) { + newBoard[i] = carData.number + } + //println("UPDATED ROW: ${newBoard.sliceArray((rowIndex * 6) until ((rowIndex + 1) * 6)).contentToString()}") + availableMoves.add(moveTo(newBoard)) + } + } + if (rowIndex > 1) { + val lookUp = board.slice(colIndex .. (spaceIndex - 6) step 6).toIntArray().reversedArray() + //println("LOOKING UP: ${lookUp.contentToString()}") + val carData = findCar(lookUp) + if (carData.size > 1) { + //println("CAR: $carData}") + val newBoard = board.copyOf() + for (i in (spaceIndex - (carData.size + carData.distance) * 6) .. (spaceIndex - carData.size * 6) step 6) { + newBoard[i] = 0 + } + for (i in (spaceIndex - (carData.size - 1) * 6) .. spaceIndex step 6) { + newBoard[i] = carData.number + } + //println("UPDATED COL: ${newBoard.slice(colIndex .. (30 + colIndex) step 6).toIntArray().contentToString()}") + availableMoves.add(moveTo(newBoard)) + } + } + if (rowIndex < 4) { + val lookDown = board.slice((spaceIndex + 6) .. (colIndex + 30) step 6).toIntArray() + //println("LOOKING DOWN: ${lookDown.contentToString()}") + val carData = findCar(lookDown) + if (carData.size > 1) { + //println("CAR: $carData") + val newBoard = board.copyOf() + for (i in spaceIndex .. (spaceIndex + (carData.size - 1) * 6) step 6) { + newBoard[i] = carData.number + } + for (i in (spaceIndex + carData.size * 6) .. (spaceIndex + ((carData.size + carData.distance) * 6)) step 6) { + newBoard[i] = 0 + } + //println("UPDATED COL: ${newBoard.slice(colIndex .. (30 + colIndex) step 6).toIntArray().contentToString()}") + availableMoves.add(moveTo(newBoard)) + } + } + } + } + //println("Found ${availableMoves.size} moves...") + return availableMoves + } + + private fun moveTo(newBoard: IntArray): Position { + val previousBoardsCopy = ArrayList(previousBoards) + previousBoardsCopy.add(board) + return Position(newBoard, previousBoardsCopy) + } + + data class CarData(val size: Int, val number: Int, val distance: Int) + + private fun findCar(view: IntArray): CarData { + var carSize = 0 + var carNumber = -1 + var carDistance = 0 + for (number in view) { + if (carNumber == -1) { + if (number == 0) { + carDistance += 1 + } else { + carNumber = number + carSize = 1 + } + } else if (carNumber == number) { + carSize += 1 + } else { + break + } + } + return CarData(carSize, carNumber, carDistance) + } +} diff --git a/src/main/resources/boards/board0.txt b/src/main/resources/boards/board0.txt new file mode 100644 index 0000000..97709b3 --- /dev/null +++ b/src/main/resources/boards/board0.txt @@ -0,0 +1,6 @@ +0 0 0 0 2 0 +0 0 0 0 2 0 +0 1 1 0 2 0 +0 0 0 0 0 0 +0 0 0 0 0 0 +0 0 0 0 0 0 diff --git a/src/main/resources/boards/board1.txt b/src/main/resources/boards/board1.txt new file mode 100644 index 0000000..840c370 --- /dev/null +++ b/src/main/resources/boards/board1.txt @@ -0,0 +1,6 @@ +1 1 1 2 0 3 +0 0 4 2 0 3 +5 5 4 0 0 6 +0 0 4 7 7 6 +8 8 8 0 9 0 +0 10 10 0 9 0 diff --git a/src/main/resources/boards/board2.txt b/src/main/resources/boards/board2.txt new file mode 100644 index 0000000..c9be0d1 --- /dev/null +++ b/src/main/resources/boards/board2.txt @@ -0,0 +1,6 @@ + 1 1 2 2 2 3 + 4 0 5 5 6 3 + 4 0 7 7 6 8 + 9 9 10 11 11 8 +12 12 10 13 0 0 +14 14 14 13 0 0 diff --git a/src/main/resources/boards/board3.txt b/src/main/resources/boards/board3.txt new file mode 100644 index 0000000..356ea00 --- /dev/null +++ b/src/main/resources/boards/board3.txt @@ -0,0 +1,6 @@ +1 1 1 2 3 4 +5 6 6 2 3 4 +5 0 7 7 3 4 +8 8 9 0 0 0 +0 10 9 11 11 0 +0 10 12 12 13 13