Solving the "Unblock Me" puzzle

Even since I can remember, I've always been fascinated with puzzles. This fascination has manifested itself in temporary obsessions of various natures - mathematical, probabilistic, chess related, AI ... you name it, I've probably wasted time playing with it :‑)

A natural consequence is the kind of computer games I am drawn to... that is, puzzle games. I still consider Sokoban the best puzzle game I've ever played (and finished) - but also love most of Simon Tatham's collection. And recently, I started passing my half-hour train commutes to and from work, playing Unblock-Me on my iPhone.

Would you like to play a game?

This game is deceptively simple: you have to move horizontal blocks left or right, and vertical blocks up or down, in order to free ("unblock") the red block - i.e. allow it to escape from the exit to its right. The red block is always at the 3rd line, as is the exit.

If you haven't played before, here's a link to a random Youtube video I found with a quick search. Apparently I am not the only one who likes this puzzle; there are lots of similar videos, with players showcasing how they solved a level they had trouble with. Obligatory warning: this game is very addictive, if you try it, there's no turning back.

My brain has more or less adapted, and quickly solves most of the levels. But now that I am close to completing the Expert set, there are some levels that torture me for minutes!

How dare they! :‑)

Time to use my brain - in a different way.

Stage 1: Detecting board sets from screenshots

On my iPhone, hitting both the main and the power button at the same time, results in an auto-saved 480x320 snapshot of the current screen (btw, that's how the picture you see above was obtained). Since the dimensions of this screenshot are constant, the placement of the tile grid will also remain constant: at identical pixel positions in every such snapshot.

(Translation: if you want to replicate my results in phones with different screenshot sizes... you'll have to edit my code :‑)

This means we can do a small number of tests, and identify the tile coordinates in these snapshots:

Hunting down the tile coordinates

In the screenshot above, you see how I manipulated the .ppm data of the image (generated via ImageMagick: "convert IMG_0354.PNG old.ppm") and emitted horizontal lines. Fine-tuning the location of these white lines allowed me to find where the tile centers are located, in both horizontal, and (via a similar pass) vertical coordinates.

At that point ...

I sampled the color of the center pixel of each tile - and mapped the RGB values into the 3 tile categories (empty, block and prisoner) via threshold values that I experimented with.

I sampled the top center and the bottom center pixel of each tile - and stored whether it's RGB values thresholded as white, black, or non-border. The game uses these colors to "shade" the blocks, and thus allowed me to learn the vertical "borders" between blocks.

I could then write a simple algorithm that re-created the block information from these two arrays:

With the exception of neighbouring horizontal blocks - which I had to hack around - it worked on the first try. Feeding it with screenshots, I saw it emit the board information:

From screenshot to block data

Stage 2: The Solver

So, I had a list of blocks that represented a board - what then?

Well, from each such list, I could generate ... a list of such lists - that is, the list of possible boards that can be reached from this one, by moving a block one position to the left, right, top or bottom.

Following the same approach I used in my Knight's Tour, I wrote a brute-force recursive traversal:

voidSolveBoard( board ){ can prisoner escape?he can! Blisshe cant?for each possible board that can arise out of the current oneSolveBoard(newBoard)}

I also added a set<Board> that was checked upon SolveBoard entry, so that the algorithm never went back to a position that it had already passed through before (in the call stack).

And run it...

And waited...

Did some web browsing. Went for some coffee...

Came back - still running.

Apparently, the problem space of this puzzle is not as small as I thought - this recursive attempt at finding the solution, goes "deep" first, exhausts the "move" it tried, then tries another.

In computer-science parlance, what I did was a "depth-first" search of the solution - meaning that the algorithm descended the tree of possible moves all the way to it's leaves (i.e. to the board states where there is no move left), and then backtracked back to try something else. And as is obvious on hindsight, this will only work for "small" problem spaces - like my Knight's Tour search. In UnblockMe however, the combinations of moves one can do at every level, multiply with the ones he can do on the next one - and basically, "explode" exponentially.

Instead of diving "deep" in the tree, I had to visit it in "layers" of increasing depth - i.e. in Breadth First Search:

Enqueue the current boardwhile Q not empty: Dequeue a board and examine it can prisoner escape?he can! Blisshe cant?for each possible board that can arise out of this one add board to END of Q

The pseudocode above visits the first level of available moves, then all the second-level moves, then the 3rd, etc. This means that it finds the optimal solution - the one the prisoner can use to escape, in the least amount of moves.

The actual implementation, below, has to account for not visiting already visited boards, and for tracing back from the solved board into the one we started from - I used a map<Board,MoveIusedToGetThere> to do that.

// When we find the solution, we also need to backtrack// to display the moves we used to get there...//// "Move" stores what block moved and to what directionstructMove{int _blockId;enum Direction {left, right, up, down} _move;Move(int blockID,Direction d):_blockId(blockID),_move(d){}};...// The brains of the operation - basically a Breadth-First-Search// of the problem space:// http://en.wikipedia.org/wiki/Breadth-first_search//voidSolveBoard(list<Block>& blocks){ cout <<"\nSearching for a solution...\n";// We need to store the last move that got us to a specific// board state - that way we can backtrack from a final board// state to the list of moves we used to achieve it.map< Board, Move> previousMoves;// Start by storing a "sentinel" value, for the initial board// state - we used no Move to achieve it, so store a block id// of -1 to mark it: previousMoves.insert( pair<Board,Move>(renderBlocks(blocks),Move(-1, Move::left)));// We must not revisit board states we have already examined,// so we need a 'visited' set:set<Board> visited;// Now, to implement Breadth First Search, all we need is a Queue// storing the states we need to investigate - so it needs to// be a list of board states... i.e. a list of list of Blocks!list< list<Block> > queue;// Start with our initial board state queue.push_back(blocks);while(!queue.empty()){// Extract first element of the queuelist<Block> blocks =*queue.begin(); queue.pop_front();// Create a Board for fast 2D access to tile stateBoard board =renderBlocks(blocks);// Have we seen this board before?if(visited.find(board)!= visited.end())// Yep - skip itcontinue;// No, we haven't - store it so we avoid re-doing// the following work again in the future... visited.insert(board);// Check if this board state is a winning state:// Find prisoner block... list<Block>::iterator it=blocks.begin();for(; it!=blocks.end(); it++){ Block& block =*it;if(block._kind == prisoner)break;}assert(it != blocks.end());// The prisoner is always there!// Can he escape? Check to his right!bool allClear =true;for(int x=it->_x+it->_length; x<SIZE ; x++){ allClear = allClear &&!board(it->_y, x);if(!allClear)break;}if(allClear){// Yes, he can escape - we did it! cout <<"Solved!\n";// To print the Moves we used in normal order, we will// backtrack through the board states to print// the Move we used at each one...list<list<Block> > solution; solution.push_front(copyBlocks(blocks)); map<Board,Move>::iterator itMove = previousMoves.find(board);while(itMove != previousMoves.end()){if(itMove->second._blockId ==-1)// Sentinel - reached starting boardbreak;// Find the block we moved, and move it// (in reverse direction - we are going back)for(it=blocks.begin(); it!=blocks.end(); it++){if(it->_id == itMove->second._blockId){switch(itMove->second._move){case Move::left: it->_x++;break;case Move::right: it->_x--;break;case Move::up: it->_y++;break;case Move::down: it->_y--;break;}break;}}assert(it != blocks.end());// Add this board to the front of the list... solution.push_front(copyBlocks(blocks)); board =renderBlocks(blocks); itMove = previousMoves.find(board);}// Now that we have the full list, emit it in orderfor(list<list<Block>>::iterator itState=solution.begin(); itState != solution.end(); itState++){printBoard(*itState); cout <<"Press ENTER for next move\n"; cin.get();} cout <<"Run free, prisoner, run! :-)\n";exit(0);}// Nope, the prisoner is still trapped.//// Add all potential states arrising from immediate// possible moves to the end of the queue.for(it=blocks.begin(); it!=blocks.end(); it++){ Block& block =*it;#defineCOMMON_BODY(direction)\list<Block> copied =copyBlocks(blocks);\/* Add to the end of the queue for further study :-) */\ queue.push_back(copied);\/* Store board and move, so we can backtrack later */\ previousMoves.insert(\ pair<Board,Move>(\renderBlocks(copied),\Move(block._id, Move::direction)));if(block._isHorizontal){// Can the block move to the left?if(block._x>0&& empty==board(block._y, block._x-1)){ block._x--;COMMON_BODY(left) block._x++;}// Can the block move to the right?if(block._x+block._length<SIZE && empty==board(block._y, block._x+block._length)){ block._x++;COMMON_BODY(right) block._x--;}}else{// Can the block move up?if(block._y>0&& empty==board(block._y-1, block._x)){ block._y--;COMMON_BODY(up) block._y++;}// Can the block move down?if(block._y+block._length<SIZE && empty==board(block._y + block._length, block._x)){ block._y++;COMMON_BODY(down) block._y--;}}}// and go recheck the queue, from the top!}}

And to my immense delight, I saw it work:

Never again shall I be blocked for long on a level :‑)

No, this is not cheating! It's a product of my own brain, addressing the same problem... in a different way. It just so happens... that I know how to extend my brain :‑)

Some thoughts

The code lives in GitHub, and it's relatively simple to follow through (I commented it a lot). However, if you compare this implementation with the one I did for Score4, you will naturally notice the verbosity of C++.
Why didn't I use OCaml for this? Or even C++11, which would drastically cut down on e.g. iterator loops?

Well - the problem parameters... are that I want to run this on my phone. And as it turns out, I have succeeded in installing a Linux-based cross compiler for my jailbroken iPhone - but it only works for old-style C++ (i.e. non-C++11).

OCaml on the iPhone would be cool, though - if I ever get my hands on an iPhone OCaml compiler, I'll probably port my code.

The comments on this website require the use of JavaScript. Perhaps your browser isn't
JavaScript capable or the script is not being run for another reason. If you're
interested in reading the comments or leaving a comment behind please try again with a
different browser or from a different connection.