fifteen-solver is a Kotlin library for solving 15 puzzle games.
- Add JitPack repository (e.g. in
settings.gradle):dependencyResolutionManagement { repositories { maven { url 'https://jitpack.io' content { includeGroup("me.italankin.fifteen-solver") } // or just add this line, if you have jitpack already } } } - Add the dependency:
See releases for available versions.
dependencies { // solver dependency implementation 'me.italankin.fifteen-solver:solver:<version>' // optional: game dependency, if you only need game implementation implementation 'me.italankin.fifteen-solver:game:<version>' }
fun main(): Unit = runBlocking {
val session = Session(
// generate 100 games of size 3x3 using random shuffle for scrambling
generator = randomGames()
.size(3 x 3)
.scrambler(ShuffleScrambler())
.generator()
.bounded(100),
// compare two A* solvers using Manhattan Distance and Linear Conflict heuristics
solvers = listOf(
Solver(
heuristics = ManhattanDistance(),
algorithm = AStar(),
),
Solver(
heuristics = LinearConflict(),
algorithm = AStar(),
),
),
// report results and compare solvers by average solve time
reporter = SystemOutReporter(
compareBy = listOf(SystemOutReporter.DefaultFields.AVG_TIME)
).withProgress(), // display live progress
)
session.dumpParameters() // dump session parameters to stdout
session.execute() // start solving
}Example output
session parameters:
concurrency: 11
generator:
Bounded(count=100) {
Random(Classic, 3x3, Default, ShuffleScrambler)
}
games count: 100
total games count: 200
solvers:
* Solver(heuristics=ManhattanDistance, algorithm=A*(Default))
* Solver(heuristics=LinearConflict, algorithm=A*(Default))
┌────────────┬────────────────┐
│ Total │ 200 (0 errors) │
│ Total time │ 188 ms │
│ Max memory │ 59 MB │
│ Avg memory │ 32 MB │
│ GC count │ 2 │
│ GC time │ 3 ms │
└────────────┴────────────────┘
┌─────────────────────────────────────────────────────────────┬───────────────┬─────────────────┬─────────────────────┬────────────────┬────────────────┬────────────────┬─────────────┬─────────────┬─────────────┐
│ Solver │ CPU time (ms) │ Speed (games/s) │ Avg speed (ms/game) │ Time (min, ms) │ Time (max, ms) │ Time (avg, ms) │ Moves (min) │ Moves (max) │ Moves (avg) │
├─────────────────────────────────────────────────────────────┼───────────────┼─────────────────┼─────────────────────┼────────────────┼────────────────┼────────────────┼─────────────┼─────────────┼─────────────┤
│ Solver(heuristics=LinearConflict, algorithm=A*(Default)) │ 778 │ 128.560 │ 7.778 │ 0.033 │ 113.835 │ 7.778 │ 11 │ 28 │ 22.580 │
│ Solver(heuristics=ManhattanDistance, algorithm=A*(Default)) │ 993 │ 100.745 │ 9.926 │ 0.021 │ 116.841 │ 9.926 │ 11 │ 28 │ 22.580 │
├─────────────────────────────────────────────────────────────┼───────────────┼─────────────────┼─────────────────────┼────────────────┼────────────────┼────────────────┼─────────────┼─────────────┼─────────────┤
│ Total │ 1770 │ 1063.830 │ 0.940 │ 0.021 │ 116.841 │ 8.852 │ 11 │ 28 │ 22.580 │
└─────────────────────────────────────────────────────────────┴───────────────┴─────────────────┴─────────────────────┴────────────────┴────────────────┴────────────────┴─────────────┴─────────────┴─────────────┘
Compare by AVG_TIME:
┌─────────────────────────────────────────────────────────────┬───────┬────────┬──────────┐
│ Solver │ Base │ Diff │ Diff (%) │
├─────────────────────────────────────────────────────────────┼───────┼────────┼──────────┤
│ Solver(heuristics=LinearConflict, algorithm=A*(Default)) │ 7.778 │ 0.000 │ 0.000% │
│ Solver(heuristics=ManhattanDistance, algorithm=A*(Default)) │ 9.926 │ +2.148 │ +27.610% │
└─────────────────────────────────────────────────────────────┴───────┴────────┴──────────┘
Session is the main entry point. It encapsulates solving, reporting and threading logic for games.
Each session can be execute()d only once.
generator- generator for gamessolvers- list of solversreporter- reports progress and results of solvesconcurrency- limit number of parallel solves (defaults to available processors count)
For example:
val session = Session(
generator = randomGames().generator().bounded(50),
solvers = listOf(Solver(ManhattanDistance(), AStar())),
reporter = SystemOutReporter().withProgress(),
concurrency = Session.Concurrency.Fixed(numThreads = 4)
)15 puzzle can be solved using different algorithms, but currently implemented are:
-
A*- consumes large amount of memory, but solves relatively quick.Warning: solving anything above 4x3 or 3x4 can be slow and result in OOMs for most machines.
If you just need any solution, the process can be sped up with different strategies for picking most promising node:
- Default (optimal solutions)
- Bounded relaxation (suboptimal solutions):
StaticWeightingDynamicWeighting
-
IDA*- slow, but has low memory usage -
Fringe Search- middle ground between A* and IDA*
You can implement your own algorithm using Algorithm.
Currently available heuristics are:
ManhattanDistanceLinearConflictRelaxedAdjacencyHammingDistanceInversionsEuclideanDistance
Custom heuristics can be created by implementing Heuristics interface.
Solvers allow testing and comparing different combinations Heuristics and Algorithms. For example:
val session = Session(
generator = /* ... */,
solvers = listOf(
ManhattanDistance(),
LinearConflict(),
HammingDistance()
).map { heuristics -> Solver(heuristics, AStar()) },
reporter = /* ... */
)To solve a game, it must be created, which can be done with a help of generators:
RandomGameGenerator- create random games with specified parametersval randomGameGenerator = randomGames() .size(4 x 4) .scrambler(ShuffleScrambler(random = Random(0))) .factory(GameFactory.Classic) .missingTile(MissingTile.Default) .skipSolved(true) .generator()
StaticGameGenerator- yields the same games on every requestval staticGameGenerator = staticGames { +ClassicGame(3, 3, listOf(0, 4, 8, 5, 7, 3, 6, 1, 2)) +listOf( ClassicGame(3, 3, listOf(7, 8, 4, 1, 6, 2, 3, 0, 5)), ClassicGame(3, 3, listOf(5, 1, 4, 0, 2, 7, 8, 3, 6)), ) }
BoundedGameGenerator-GameGeneratorwith fixed number of produced gamesval boundedGameGenerator = randomGames() .generator() .bounded(100)
FilterGameGenerator- filter gamesval filterGameGenerator = randomGames() .bounded(100) .filterGames { it.state[0] != 0 }
ConcatGameGenerator- concat multipleBoundedGameGeneratorsval concatGameGenerator = concatGames { +randomGames().size(3 x 3).bounded(100) +randomGames().size(4 x 4).bounded(100) }
ShufflingGameGenerator- shuffle results fromBoundedGameGeneratoron each requestval shufflingGameGenerator = randomGames() .bounded(100) .shuffle(random = Random(0))
RepeatingGameGenerator- repeat results fromBoundedGameGeneratorspecified number of timesval repeatingGameGenerator = randomGames() .bounded(100) .repeat(5)
FrozenGameGenerator- freeze state ofBoundedGameGeneratorto produce same results on each requestval frozenGameGenerator = randomGames() .bounded(100) .freeze()
Game.Scrambler is an interface for creating game scrambles. Currently available Scramblers are:
ShuffleScrambler- random shuffleShuffleHarderScrambler- random shuffle, but requires more moves on average thanShuffleScramblerSolvedScrambler- solved gameRandomClickScrambler- randomly clicks on a tile in the same row or column as empty spaceRandomMovesScrambler- scramble puzzle by doing random movesStrideScrambler- randomly select a cell and move0to that cell
Example comparison of average moves for 3x3
┌────────────────────────────────────────────────────────┬────────┬─────────┬──────────┐
│ Solver │ Base │ Diff │ Diff (%) │
├────────────────────────────────────────────────────────┼────────┼─────────┼──────────┤
│ ShuffleHarderScrambler │ 25.346 │ 0.000 │ 0.000 │
│ ShuffleScrambler │ 21.987 │ -3.359 │ -13.254 │
│ StrideScrambler(iterations=50, allowEmptyMoves=false) │ 21.466 │ -3.880 │ -15.307 │
│ RandomClickScrambler(rounds=50) │ 15.610 │ -9.736 │ -38.412 │
│ RandomMovesScrambler(moves=50, allowPrevMoveUndo=true) │ 12.570 │ -12.776 │ -50.406 │
└────────────────────────────────────────────────────────┴────────┴─────────┴──────────┘
Available Reporters:
SystemOutReporter- reports summary stats tostdoutProgressReporter- reports current progress tostdout, e.g.:queue: 86/11/3/100 (3%), 1.476 games/s, memory: 3679 MB, elapsed: 2s, remaining: 66sJsonReporter- dumps results to JSONCsvReporter- dumps results to CSVCompositeReporter- concatenates multiple reportersval compositeReporter = SystemOutReporter() + ProgressReporter() + JsonReporter(JsonReporter.Output.ToFile("results.json"))
Statistics can be calculated by simply calling stats() extension function on the results:
val session = Session(...)
val results = session.execute()
val stats = results.stats()
// do something with statsYou can also utilize Table class, example usage can be found in SystemOutReporter.
See LICENSE.