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