So I cleared everything so you can understand the structure of the code. It is
really simple:
- First the program copies `0x36` bytes from the init_state array to a local array
- It then prompts for the password, asking for a maximum of 24 bytes
- Initialize an index that will be used to iterate over the input in an infinite loop
- We have 2 clear parts in this loop:
- The first one when we are done iterating on the input: We will go back to what this does later but we can clearly see that it performs the final check and prints us the flag if we are good.
- The second one is the part that iterates on the input, so the one that is executed first. We can also see that it matches the characters from our input against lower case letters, instantly exiting the program if it isn't one
dimensions I basically saw that the permutation matrix was indexed by `(pos / 4) * 0x36`, since our alphabet holds 24 characters, that gives us `24 / 4 == 6` arrays of size `0x36` in the matrix. We can confirm the 0x36 is we will iterate over `0x36` elements of the permutation array right after this.
to shift the `board`, before keeping only the lowest weight hex-digit with the
`& 0xf`. The hex digit is then compared against the byte we fetched from our
state. If they differ, we basically skip this loop, decrementing the `shifter`
by 4 (meaning that on the next iteration we will select the next hex digit of
the board), and go to the next array in the `allowed_moves` matrix.
So to recap this, if you consider the `board` as an array of 4 bits elements,
`shifter` is actually an index used to iterate over this array. So we are
iterating to find at which index is the byte from the `state` inside the
`board`. `allowed_moves` will follow the same iterations, meaning that we will
select an `allowed_moves` sub-array based on where the `state` byte is located
in the `board`.
Good, so once we find out `state` byte inside `board` we start iterating over
the `allowed_moves`, basically the move is multiplied by `0xf`, which will effectively set the 4 bits corresponding to the single bit in the initial move, this will now be used as a mask on the board.
So this move simply selects an hex digit from the board actually, if the
What you need to notice is that every move can be inverted, so you can go back to your position after moving. MOST tiles can go to the tiles right next to them or to the tiles that are 4 tiles farther. For example, tile at index 1 can go either at 0, 2, or 5. You are probably starting to understand, let's just put
So we need to get from one to the other in `0x36` moves exactly, each move swapping the 0 with an adjacent tile.
Cool, all of this simply for a slide puzzle. Each element of the state corresponds to the tile that we must push on the `0` (which represent the empty tile in the slide puzzle). Let's go and solve th...
### But wait there was a first step
I hope you did not forgot that the state used to select which tiles are moved in
So know we are done with the reversing part, with our input, the program will:
- Consider each character as a move on a rubiks cube, applying said move
- Consider the flatenned shuffled cube as a list of slide puzzle moves
Basically we need to solve a rubik's cube in order to ordonate a slide puzzle
solution.
To solve this, we will need to take one problem at a time, first find a solutions to the slide puzzle, then map the solution to a cube which we will consider as the solved cube, then map the color of the solved cube back to
the starting cube in order to solve it.
### Naive backtracking
So to start I want to say that I am really impressed that the solver of
`face0xff` found a solution this fast, I was really convinced that without the
little tricks that I will show you, it would take too long to search for a 54
So my approach was a backtracking algorithm, however you can clearly see why
backtracking 54 moves is not a good solution so we will need to add some
heuristics to narrow down the search for valid moves.
Basically the algorithm works as follow given a current state of the grid:
- If the puzzle is solved it's won
- If you have no move left it's lost
- generate all possible moves
- try each move by applying the move and recursively calling this procedure
- if the recursive call returned true then it's won
- else cancel the move and try the next
- if no move returned true there is no solution
### Slide puzzle heuristics
Okay this algorithm will take way too long, we need to cancel some search paths
early so that we will not further go down the calls if we can already know that we cannot solve anymore from a given grid.
#### Cannot cancel previous move
This heuristic takes the assumption that you cannot play the same move as the
previous one, because otherwise you will go back to same state as the previous
move, effectively doing 2 moves for nothing.
Okay this heuristic is not exactly 100% reliable, since actually the puzzle is
also solvable this way, simply with useless moves. But I was convinced that 54
was the optimal solution, I don't imagine that `\J` would have asked us to solve
a puzzle in a non-optimal way.
#### Last move
Since the tile in bottom right needs to be `0`, this means that the last move needs to be `f` or `c`, so if at any point, both of these tiles do not have anymore moves remaining moves while the puzzle isn't already solved then you can already tell it's unsolvable.
Then, we can compute the manhattan distance of the tile to its supposed
location, which is the minimum number of moves that would be required for the
tile to go to that location if there were no other tile on the grid. For example
tile `f` placed in the top left corner will have a manhattan distance of 5 to
its supposed location.
So whenever the move counter of a tile becomes lower than its manhattant distance to its suppose location, you know that you will never be able to place the tile back to where it is supposed to be.
### Solver
So I implemented this in rust because I wanted this to be fast: