-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Deploying to gh-pages from @ bb7a334 🚀
- Loading branch information
Showing
11 changed files
with
923 additions
and
899 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,42 @@ | ||
<!DOCTYPE html><html lang="en"><head> | ||
<link rel="stylesheet" href="/slide-puzzle/app-e7eee7ccd335af5d.css"> | ||
|
||
<link rel="preload" href="/slide-puzzle/slide-puzzle-460911f4e5e06097_bg.wasm" as="fetch" type="application/wasm" crossorigin=""> | ||
<link rel="modulepreload" href="/slide-puzzle/slide-puzzle-460911f4e5e06097.js"></head> | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" /> | ||
<link rel="stylesheet" href="./style.css" /> | ||
</head> | ||
<body> | ||
<div class="content"> | ||
<div class="header">Slide Puzzle</div> | ||
<div | ||
id="board" | ||
class="board" | ||
style=" | ||
width: 12rem; | ||
height: 12rem; | ||
position: relative; | ||
touch-action: none; | ||
" | ||
ontouchstart="on_touch_start" | ||
ontouchend="on_touch_end" | ||
></div> | ||
|
||
<script type="module">import init from '/slide-puzzle/slide-puzzle-460911f4e5e06097.js';init('/slide-puzzle/slide-puzzle-460911f4e5e06097_bg.wasm');</script></body></html> | ||
<button id="quick_swap">Shuffle Quick</button> | ||
<button id="granular_swap">Shuffle Granular</button> | ||
<button id="optimal_solve">Solve optimally</button> | ||
<button id="d_and_c_solve">Solve D and C</button> | ||
</div> | ||
</body> | ||
|
||
<script src="./pkg/slide_puzzle.js"></script> | ||
<script> | ||
const { wasm_main } = wasm_bindgen; | ||
|
||
async function run() { | ||
await wasm_bindgen("./pkg/slide_puzzle_bg.wasm"); | ||
|
||
wasm_main(); | ||
} | ||
|
||
run(); | ||
</script> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
# Slide Puzzle | ||
|
||
Implementation of a slide-puzzle game with random puzzles and two solver | ||
algorithms. Written in Rust for Wasm. | ||
|
||
![Example](./assets/slide_puzzle.gif) | ||
|
||
## Overview | ||
|
||
Slide puzzles are square arrays of fields who's surface together represent a | ||
picture or a pattern. One field is missing (the empty field). Horizontal and | ||
vertical neighbours of this missing field can be swapped with it. With this | ||
mechanism, we can rearrange the slide puzzle. | ||
|
||
Because we can only swap fields which are (non-diagonal) neighbours of the empty | ||
field, not all permutations of the field array can be solved to the order which | ||
shows the picture. Thus if we want to generate a solvable puzzle, the best way | ||
to do this is by randomly doing valid swaps. | ||
|
||
## Modeling | ||
|
||
To know which fields can be swapped with which other fields, we need to tranform | ||
the indices of the fields to the coordinates in the square puzzle grid and vice | ||
versa to perform swaps. | ||
|
||
## Solver Algorithms | ||
|
||
There are two algorithms implemented. The optimal one is well suited for smaller | ||
problems but fails to converge for very large puzzles or puzzles with many | ||
steps. The other algorithm is based on the divide&conquer principle, does not | ||
yield optimal solve orders but converges for any reasonable problem size. | ||
|
||
### Optimal algorithm | ||
|
||
For the optimal solve order, we ask what is the shortest sequence of swaps that | ||
will bring a current state to the final ordered puzzle state. Our puzzle state | ||
is defined by the array of fields. Thus our state space is the space of all | ||
those permutations of the array which can be reached with swaps. | ||
|
||
Because at some point one permutation may lead back to another permutation, this | ||
state space is not infinite but can be very large (trivial to see for 1x2 or | ||
also 2x2 puzzles). Finding the optimal solve order means finding the shortest | ||
path between the current state and the final state through the state spaces. | ||
|
||
Thus in its most basic form, our optimal algorithm is a breadth-first-search | ||
through the graph of states which builds the graph on the go. | ||
|
||
We can see that holding all the states in memory will be the main limiting | ||
factor for our algorithm. To cut down memory requirements as much as possible, | ||
we use `u8` values. With `u8::MAX` (255), we are | ||
limited to puzzles of size `floor(sqrt(255)) == 15` which is enough for our | ||
purposes. It is indispensible to recognize states which we have already seen | ||
before. To do this, we build a set of state hashes. | ||
|
||
#### Complexity | ||
|
||
For each state on the way from our initial state to the final state, we have to | ||
explore all unseen neighbours. Thus with every step, the amount of space | ||
required grows by the factor of unseen neighbours. We have an algorithm that | ||
grows _exponentially_ in the number of steps. Thus our space complexity is | ||
`O(C^n)`. | ||
|
||
What is the factor `C`? Let's take a look at a 3x3 puzzle. If we are trying to | ||
find a shortest path, we should never be undoing the immediately last move. | ||
Thus this are the number of possible moves: | ||
|
||
```conf | ||
# Number of possible moves from each field, excluding undoing the last move | ||
1 2 1 | ||
2 3 2 | ||
1 2 1 | ||
``` | ||
|
||
On average, we have `15 / 9 = 1.667` possible moves. On course, it is not clear | ||
if the moves are uniformly distributed over the field. The implementation of the | ||
algorithm actually prints the number of moves from the path and the number of | ||
iterations taken to find it. We can calculate a proxy for `C` from this. Below | ||
are some example numbers: | ||
|
||
```conf | ||
# 20 moves in path, 86059 iterations | ||
86059 ^ (1 / 20) = 1.765 | ||
# 20 moves in path, 88793 iterations | ||
88793 ^ (1 / 20) = 1.768 | ||
# 18 moves in path, 26549 iterations | ||
26549 ^ (1 / 18) = 1.761 | ||
``` | ||
|
||
So we were not that far off with our guess. Note that for larger puzzles, the | ||
factor is larger which contributes a lot in the exponential function. E.g. for | ||
5x5: | ||
|
||
```conf | ||
# Number of possible moves from each field, excluding undoing the last move | ||
1 2 2 2 1 | ||
2 3 3 3 2 | ||
2 3 3 3 2 | ||
2 3 3 3 2 | ||
1 2 2 2 1 | ||
``` | ||
|
||
On average, this is `55 / 25 = 2.2`. For a path with length 20, we expect around | ||
`2.2 ^ 20 = 7'054'294` states that have to be evaluated, which can exceed the | ||
memory provided to the process in the browser. | ||
|
||
### Divide and conquer algorithm | ||
|
||
An alternative algorithm is based on the divide&conquer approach. Instead of | ||
finding the optimal path of moves in the state space, we solve the fringes of | ||
the puzzle first and thereby make the problem ever smaller. Details can be found | ||
[on this website][d_and_c_algorithm_explained]. | ||
|
||
An illustration of the order in which we solve the fields for a 5x5 puzzle: | ||
|
||
```conf | ||
# The number is the order of the group in which the fields are solved/fixed | ||
0 0 0 0 0 | ||
1 2 2 2 2 | ||
1 3 4 4 4 | ||
1 3 5 6 6 | ||
1 3 5 6 6 | ||
``` | ||
|
||
While the divide&conquer part seems straight forward, the actual implementation | ||
is quite complicated and tedious: | ||
|
||
- The outermost loop alternates between solving rows and columns and enters a | ||
third special phase when only a 2x2 square is left. | ||
- For the individual fields that we move into a row or column, we need to | ||
compute a path along which they can move without moving any of the previously | ||
solved fields. | ||
- Fields cannot move directly but only by moving the empty field to the next | ||
field on their path and then swapping to this. We use a BFS to find the path | ||
of the empty field (excluding fixed fields and the field to move itself). | ||
|
||
[d_and_c_algorithm_explained]: https://www.kopf.com.br/kaplof/how-to-solve-any-slide-puzzle-regardless-of-its-size/ | ||
[wasm]: https://webassembly.org/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"name": "slide-puzzle", | ||
"version": "0.3.0", | ||
"files": [ | ||
"slide_puzzle_bg.wasm", | ||
"slide_puzzle.js", | ||
"slide_puzzle.d.ts" | ||
], | ||
"browser": "slide_puzzle.js", | ||
"types": "slide_puzzle.d.ts" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
declare namespace wasm_bindgen { | ||
/* tslint:disable */ | ||
/* eslint-disable */ | ||
export function wasm_main(): void; | ||
|
||
} | ||
|
||
declare type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; | ||
|
||
declare interface InitOutput { | ||
readonly memory: WebAssembly.Memory; | ||
readonly wasm_main: () => void; | ||
readonly __wbindgen_malloc: (a: number, b: number) => number; | ||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; | ||
readonly __wbindgen_export_2: WebAssembly.Table; | ||
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h2156f1b9ca58ebe0: (a: number, b: number, c: number) => void; | ||
readonly _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hc0b87f0d220d0506: (a: number, b: number) => void; | ||
readonly __wbindgen_free: (a: number, b: number, c: number) => void; | ||
readonly __wbindgen_exn_store: (a: number) => void; | ||
} | ||
|
||
/** | ||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and | ||
* for everything else, calls `WebAssembly.instantiate` directly. | ||
* | ||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated. | ||
* | ||
* @returns {Promise<InitOutput>} | ||
*/ | ||
declare function wasm_bindgen (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>; |
Oops, something went wrong.