Sokoban Solver in Puzzle Dungeon

Overview

Puzzle Dungeon includes a full-featured Sokoban solver that is tightly integrated with both the graphical game dungeon and the command-line tool sokodun.

The code is Free Software, and should run on GNU/Linux, Windows, MacOS.

The solver is implemented entirely in Python, follows classical search theory, and its correctness and optimality are validated by comprehensive automated tests.

It works well for small maps, and in some cases medium-sized maps. It prioritizes correctness, flexibility, and clarity over raw performance, although a considerable number of domain-specific and python-specific optimizations are present.

Supported Search Algorithms

The solver supports multiple classical graph search algorithms, selectable at runtime:

The optimality of BFS, UCS, and A* is confirmed by comprehensive tests across different level classes.

Solution Types and Cost Metrics

The solver supports two solution types:

All costs are represented uniformly as a lexicographic pair (num_moves, num_shifts). This representation is used consistently for path costs, lower-bound estimates, and solution costs.

Solver Architecture

The Position (search node) represents a unique combination of the character location and all barrel locations.

Barrel locations are stored in a SuperPosition object. A Position is one character placement within its super-position. This allows multiple character states to share properties of the same barrel configuration, such as:

Other properties are stored per Position, including:

Each position has a single best parent at any time. If a better path to an existing position is found, the position is reparented. For algorithms that require edge relaxation (A*, BFS by moves), potential child edges are stored to allow reparenting back when accumulated path cost improves.

Heuristics and Lower Bounds

All search algorithms benefit from prepared domain-specific data:

The lower bound from a position to a solution uses perfect matching. The lower bound from the initial position to intermediate positions (currently used for pruning after a solution is found) is computed as a sum of minimal barrel-to-target costs without matching.

For heuristic-based algorithms (A* and Greedy), the perfect matching is used. The admissible heuristics preserves optimality for A*.

For UCS, optimality follows directly from cost-order exploration: the first solution found is optimal.

For BFS:

Prepared data can be disabled for experimentation or benchmarking.

Special Level Support

The solver supports several special Sokoban level classes:

Deadlock Detection

The following types of deadlocks are detected:

Solver Design Highlights

Incremental Solving and Budgeting

The solver implements a cooperative budget protocol:

This report-progress behavior is used by both the GUI and the command-line tool and can be disabled.

Special Debug Flags

Both the graphical game dungeon and the command-line tool sokodun support the following -d FEATURE (or –debug FEATURE) flags:

These feature flags are handled by SokobanSolver itself, either changing behaviour or writing relevant output to stdout. Multiple flags may be specified using separate -d options.

There is no way to disable detection of Match (or Bipartite) deadlocks. By design, the costs and valid shifts do not make sense for mismatched board positions (for which there is no perfect barrel-to-plate matching). So such Position object is just never created; this can’t be disabled.

GUI vs CLI, Keys and Options

GUI key CLI option Action
RCtrl-1 -1 Toggle return-first solution mode
RCtrl-A -A Use A* algorithm
RCtrl-B -B Use BFS algorithm
RCtrl-D -D Use DFS algorithm
RCtrl-G -G Use Greedy algorithm
RCtrl-U -U Use UCS (Uniform Cost) algorithm
RCtrl-0 -0 Disable periodic progress reporting
RCtrl-- -_ Disable cost and valid shift preparation (debug only)
KP_Enter default Find a push optimal solution
Shift-KP_Enter -m Find a move optimal solution
Backspace Ctrl-C or N/A Stop solving or playing a solution, or unset the solution
RCtrl-Tab N/A Show the most recently created position while solving
RCtrl-Backquote N/A Show the most recently detected deadlock while solving
Alt-R N/A Reload level (guaranteed same map unlike plain r)
Alt-E N/A Reload level with toggled reverse-barrel mode
Alt-C N/A Load custom collection levels from the clipboard
Alt-S N/A Load a solution from the clipboard

In both GUI and CLI modes, levels may be loaded from the clipboard or standard input using clipboard: or stdin: or - arguments, and all detected deadlocks may be dumped to standard output using --debug dlck option (this can be pretty floody).

Performance Notes

This is possibly the most complete Sokoban solver implemented in Python.

It may be much slower than some specialized solvers because:

A typical Sokoban level contains many millions of unique positions. Currently the solver keeps all visited positions and never removes them while solving. It would be much slower without this permanent cache.

There is no memory management yet. It will eventually fill all available memory. The operating system may kill the process, but this can freeze the computing unit. Therefore, remember to stop the process (by pressing Ctrl-C in the CLI or any button like Space in the GUI), or limit it by time (“-T 600” limits execution to 10 minutes in the CLI). Garbage collection takes time, so please be patient after using lots of memory.

You can run the solver on large levels to observe where it gets stuck, but do not leave it running for too long. It is recomended to always start with return-first Greedy mode (specify “sokodun -1” or press RCtrl-1 in the GUI). Once it finds a return-first solution in reasonable time, you may attempt to solve it using BFS, A* or USC (-B, -A, -U with optional -m in the CLI, or RCtrl-{B,A,U} followed by KP_Enter or Shift-KP_Enter in the GUI) to find a push-optimal or move-optimal solution.

If the solution does not progress quickly for Greedy or A, this can mean some non-trivial deadlock was not detected, and expanding a deadlocked position is costy. Press RCtrl-Backquote* in the GUI to see the last discovered dynamic deadlock. Press RCtrl-Tab (any number of times) to see the last created position; if it contains an undetected deadlock, this can explain long processing times.

Despite performance limitations, this Sokoban solver is well suited for Puzzle Dungeon gameplay, testing, and provides a solid foundation for tooling, research, and further optimization.

Validity of Solutions

There are comprehensive tests. If you discover any anomaly, like a non-optimal solution for a letslogic level using BFS or A*, or a “Solution not found” result, please report such exotic cases. They can then be fixed, added to tests, and prevented from recurring.

If the minimal solution depth (number of pushes) is determined to be larger than 500, the solution is currently reported immediately as “not found”. This limitation may be removed in the future.

Enjoy!