Implement puzzle solver
This commit is contained in:
commit
4048acf2d2
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
target/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -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.
|
||||||
|
|
32
README.md
Normal file
32
README.md
Normal file
@ -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`
|
58
pom.xml
Normal file
58
pom.xml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.charego</groupId>
|
||||||
|
<artifactId>rush</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<kotlin.version>1.4.10</kotlin.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||||
|
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-maven-plugin</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>compile</id>
|
||||||
|
<phase>compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>test-compile</id>
|
||||||
|
<phase>test-compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>test-compile</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
<version>3.0.0</version>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>com.charego.rush.MainKt</mainClass>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
63
src/main/kotlin/Main.kt
Normal file
63
src/main/kotlin/Main.kt
Normal file
@ -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()
|
||||||
|
}
|
135
src/main/kotlin/Position.kt
Normal file
135
src/main/kotlin/Position.kt
Normal file
@ -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<IntArray>) {
|
||||||
|
val id = board.contentHashCode()
|
||||||
|
|
||||||
|
fun isSolution(): Boolean {
|
||||||
|
return board[16] != 0 && board[16] == board[17]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findAvailableMoves(): List<Position> {
|
||||||
|
val availableMoves = mutableListOf<Position>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
6
src/main/resources/boards/board0.txt
Normal file
6
src/main/resources/boards/board0.txt
Normal file
@ -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
|
6
src/main/resources/boards/board1.txt
Normal file
6
src/main/resources/boards/board1.txt
Normal file
@ -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
|
6
src/main/resources/boards/board2.txt
Normal file
6
src/main/resources/boards/board2.txt
Normal file
@ -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
|
6
src/main/resources/boards/board3.txt
Normal file
6
src/main/resources/boards/board3.txt
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user