Dear Monks

Recently I stumbled on an old puzzle: You have a 6*6-grid and various blocks of the sizes 2*1 or 3*1. You can move the blocks either horizontal or vertical, but only in the direction "of their orientation". (meaning: a block that is aligned horizontal can only be moved to the left or to the right, but never "sideways" to the top or bottom). The grid has an exit - and your goal is to move a specially marked block out of the grid.

Implementations of the puzzle can be found here (electronic, called blue block) or here (classic hardware solution, called rush hour).

After being stuck with a scenario for more than 15 minutes, I decided to "outsource" solution-finding to a program (see below).

Some advice I'd like to ask of you

Thanks for your feedback! Rata

#!/usr/bin/perl -w use strict; use warnings; use Data::Dumper; # for debugging use Time::HiRes qw (time); # for profiling my @solution = (); # the solution as moves my @solpics = (); # the solution as "pictures" my %poslist = (); # list of all positions that have be +en reached already - to prevent loops my $MAXMOVES = 36; # upper limit for the number of mov +es (btw: the first move is move #0!) my $LINES = 8; my $COLUMNS = 8; my $EXIT = 34; my $MAXCHAR; # highest "name" of the blocks my %blocksize; # the size of each block my %blockdir; # the orientation of all blocks (h or + v) my $pos = "********\n". # The playfield "*..EEE.*\n". # * "A" needs to be moved out "*....C.*\n". # * all other blocks are numbered +from B...Z - with no gaps in the names "*AA.BC..\n". # * all blocks need to be 2 or 3 b +oxes long "*...BDF*\n". # * free positions are marked with + "." (including the exit) "*...BDF*\n". # * the borders are marked with "* +" "*....D.*\n". # * the exit is hardcoded (positio +n 34, see above) "********\n"; setpos(); init(); # init the meta-data of the field my $movecount = 0; # count the number of steps my $starttime = time(); # for profiling my $result = solve ($pos); # recursively solve the puzzle my $endtime = time(); if ($result == 1) { print ($pos,"can be solved with the following ",scalar(@solution) +, " moves: ", join(" ",@solution), "\n\n"); for (my $i = 0; $i < scalar(@solution); $i++) { print $solution[$i +],"\n",$solpics[$i]; } print ($pos,"can be solved with the following ",scalar(@solution) +, " moves: ", join(" ",@solution), "\n\n"); } else { print ($pos,"cannot be solved in ",$MAXMOVES+1," moves\n"); } print "$movecount steps have been executed to find that solution\n"; print $endtime-$starttime," seconds have been needed.\n"; exit 0; #Debuging #print( Data::Dumper->Dumpxs( [ \ %blocksize ], [ qw{ *blocksize } ] ) +); #print( Data::Dumper->Dumpxs( [ \ %blockdir ], [ qw{ *blockdir } ] ) +); #============================== recursive solution =================== +===================================================================== +== sub solve { my $pos = $_[0]; return 1 if (substr($pos, $EXIT, 1) eq "A"); # a solutio +n has been found :-) Horray! # return 0 if (scalar(@solution) > $MAXMOVES) ; # too many s +teps <--- comment this in (###1###) my @moves = getMoves($pos); return 0 if (scalar(@moves) == 0) ; # no moves = +> not finished # print $pos, join(",",@moves), " [$movecount steps][",scalar(keys( +%poslist))," positions checked][",scalar(@solution)," steps in curren +t solution]\n"; foreach my $m (@moves) { $movecount++; # increas +e the count of moves ... my $newpos = move ($pos, $m); # do the +next move next if (exists($poslist{$newpos})); # that pos +ition was already there .. skip it to prevent going in loops push (@solution, $m); # store t +he new move push (@solpics, $newpos); $poslist{$pos} = 1; my $res = solve ($newpos); # evalua +te the new position return $res if ($res == 1); # return i +f a soultion has been found pop (@solution); # no solut +ion has been found with this move pop (@solpics); # => re +move it and try the next one # delete $poslist{$pos}; # + <--- comment this in (###1###) } return 0; # we didn't f +ind anything... } #============================== get a list of all valid moves ======== +===================================================================== +============= sub getMoves { my $pos = $_[0]; my @moves = (); foreach my $ch ('A' .. $MAXCHAR) { my $p = index($pos, $ch); # left + top corner my $s = $blocksize{$ch}; # size my $d = $blockdir {$ch}; # direction if ($d eq "h") # horizontal handling { my $i = 1; while(1) { last unless (substr($pos, $p-$i, 1) eq "."); # +look left for free elements push (@moves, "$ch:L:$i"); $i++; } $i = 1; while(1) { last unless (substr($pos, $p+$s+$i-1, 1) eq "."); + # look right for free elements push (@moves, "$ch:R:$i"); $i++; } } else # vertical handling { my $i = 1; while(1) { last unless (substr($pos, $p-$i*($COLUMNS+1), 1) eq ". +"); # look up for free elements push (@moves, "$ch:U:$i"); $i++; } $i = 1; while(1) { last unless (substr($pos, $p+($s+$i-1)*($COLUMNS+1), 1 +) eq "."); # look down for free elements push (@moves, "$ch:D:$i"); $i++; } } } return @moves; } #============================== move a block ========================= +================================================================= + sub move { my $oldpos = $_[0]; my $m = $_[1]; $m =~ /(.):(.):(\d)/; my $ch = $1; my $dir = $2; my $num = $3; my $newpos; my @s = split (//, $oldpos); my $p = index($oldpos, $ch); if ($dir eq "L") { for (my $i = 0; $i < $blocksize{$ch}; $i++) { $s[$p+$i] + = "."; } # delete old string for (my $i = 0; $i < $blocksize{$ch}; $i++) { $s[$p+$i-$nu +m] = $ch; } # write new position } elsif ($dir eq "R") { for (my $i = 0; $i < $blocksize{$ch}; $i++) { $s[$p+$i] + = "."; } # delete old string for (my $i = 0; $i < $blocksize{$ch}; $i++) { $s[$p+$i+$nu +m] = $ch; } # write new position } elsif ($dir eq "U") { for (my $i = 0; $i < $blocksize{$ch}; $i++) { $s[$p+$i*($C +OLUMNS+1)] = "."; } # delete old string for (my $i = 0; $i < $blocksize{$ch}; $i++) { $s[$p+($i-$n +um)*($COLUMNS+1)] = $ch; } # write new position } elsif ($dir eq "D") { for (my $i = 0; $i < $blocksize{$ch}; $i++) { $s[$p+$i*($C +OLUMNS+1)] = "."; } # delete old string for (my $i = 0; $i < $blocksize{$ch}; $i++) { $s[$p+($i+$n +um)*($COLUMNS+1)] = $ch; } # write new position } $newpos = join("",@s); return $newpos; } #============================== init the main fields ================= +===================================================================== +== sub init { foreach my $i (reverse("A" .. "Z")) { $MAXCHAR = $i; # overcome impli +cit localization of foreach-loop last if (index ($pos, $i) != -1); } foreach my $ch ("A"..$MAXCHAR) # determine orie +ntation and size of the blocks { my $p = index ($pos, $ch); if (substr($pos, $p+1, 1) eq $ch) { my $i = 1; while (substr($pos, $p+$i, 1) eq $ch) { $i++; } $blocksize{$ch} = $i; $blockdir{$ch} = "h"; } else { my $i = 1; while (substr($pos, $p+$i*($COLUMNS+1), 1) eq $ch) { $i++; + } $blocksize{$ch} = $i; $blockdir{$ch} = "v"; } } } #============================== another starting position ============ +===================================================================== +========= sub setpos { $pos ="********\n". # A more complicated starting p +osition "*BBB.CC*\n". "*..DEEF*\n". "*AAD..F.\n". "*H.MMLG*\n". "*HJJKLG*\n". "*IIIK.G*\n". "********\n"; }

Replies are listed 'Best First'.
Re: Puzzle solver (rush hour)
by ambrus (Abbot) on Aug 31, 2010 at 08:47 UTC

      Wow - impressing!

      Thanks for the link

      Rata
Re: Puzzle solver (rush hour)
by JavaFan (Canon) on Aug 31, 2010 at 11:58 UTC
    How would you propose to change the algorithm to come to a short solution fast?!
    I haven't looked at your code, just read your description. However, the classical way is to do breadth-first, instead of depth-first. Outline:
    • 0. Let the starting position be PS. Let k be 0. Goto 15 if PS is a solution.
    • 1. Keep a cache C of "seen" positions.
    • 2. Keep a fifo queue Q of "todo" positions, with their move number.
    • 3. Push tuple (PS, 1) onto Q.
    • 4. If Q is empty, goto 14.
    • 5. Shift first tuple of Q. Let this be (P, k).
    • 6. Calculate all possible moves of position P. Call this set of moves M. Unless the starting position doesn't allow for any moves, M will never be empty.
    • 7. If M is empty, goto 4.
    • 8. Remove a move m from M. Apply m to P, yielding postion P'.
    • 9. If P' in C, goto 7.
    • 10. If P' is a solution, goto 15.
    • 11. Add P' to C.
    • 12. Push tuple (P', k+1) on Q. (So this tuple will be inserted at the end).
    • 13. Goto 7.
    • 14. Terminate unsuccesfully. (No solution possible).
    • 15. Terminate with success. Minimum number of moves is k.
    In fact, this is just Dijkstra's algorithm applied on a graph where each vertex of the graph is a possible position of the puzzle, and there's an edge between two positions one can go from one position to the other in a single move.

    I leave it up to the reader to turn the TAOCP style of describing the algorithm into a "real" program.

Re: Puzzle solver (rush hour)
by JavaFan (Canon) on Aug 31, 2010 at 14:41 UTC
    Here's a program that finds a shortest solution. To make things simpler, I only consider moves that slide a piece 1 unit (so sliding a piece two units to the right counts as 2 moves). To consider multi-unit slides as a single move, just change how $moves is created in the setup phase.
    #!/usr/bin/perl use 5.010; use strict; use warnings; my %C; # Cache of seen positions. my @Q; # Queue of position to work on. my $start = [[[3, 1], [3, 2]], [[3, 4], [4, 4], [5, 4]], [[2, 5], [3, 5]], [[4, 5], [5, 5], [6, 5]], [[1, 3], [1, 4], [1, 5]], [[4, 6], [5, 6]]]; $start = [[[3, 1], [3, 2]], # A [[1, 1], [1, 2], [1, 3]], # B [[1, 5], [1, 6]], # C [[2, 3], [3, 3]], # D [[2, 4], [2, 5]], # E [[2, 6], [3, 6]], # F [[4, 6], [5, 6], [6, 6]], # G [[4, 1], [5, 1]], # H [[6, 1], [6, 2], [6, 3]], # I [[5, 2], [5, 3]], # J [[5, 4], [6, 4]], # K [[4, 5], [5, 5]], # L [[4, 3], [4, 4]], # M ] if 1; my $moves; foreach my $piece (@$start) { if ($$piece[0][0] == $$piece[1][0]) { push @$moves, [[0, 1], [0, -1]] } else { push @$moves, [[1, 0], [-1, 0]] } } # # Uses the fact board size < 10. # sub flatten ($) {join "", map {map {@$_} @$_} @{$_[0]}} # # Return 1 if a position is valid. No pieces outside the board, # no overlapping pieces. Only key piece allowed in exit. # sub valid ($) { my %seen; my $pos = shift; foreach my $piece (@$pos) { foreach my $part (@$piece) { my ($x, $y) = @$part; return 0 if $x < 1 || $y < 1 || $x > 6; return 0 if $y > 6 && $x != 3; # Exit. return 0 if $seen{$x, $y}++; } } return 1; } # # Return 1 if the position is solved. # sub solved ($) {${$_[0]}[0][1][1] == 7} # # Return a copy of the position # sub copy ($) {[map {[map {[@$_]} @$_]} @{$_[0]}]} sub move ($$$) { my ($pos, $piece, $move) = @_; my $copy = copy $pos; foreach my $cell (@{$$copy[$piece]}) { $$cell[$_] += $$move[$_] for 0, 1; } $copy; } sub move2text ($) { my $move = shift; if ($$move[0] == 0 && $$move[1] == 1) {return "right"} if ($$move[0] == 0 && $$move[1] == -1) {return "left"} if ($$move[0] == 1 && $$move[1] == 0) {return "down"} if ($$move[0] == -1 && $$move[1] == 0) {return "up"} die; } sub print_solution { my $s = length scalar @_; for (my $i = 0; $i < @_; $i++) { my ($piece, $direction) = @{$_[$i]}; printf "%${s}d. %s -> %s\n", $i + 1, chr(ord('A') + $piece), $ +direction; } say "Total number of examined positions: ", scalar keys %C; } die "Not a valid starting position" unless valid $start; $C{flatten $start}++; push @Q, [$start]; while (@Q) { my ($P, @moves) = @{shift @Q}; # # Calculate all moves. Filter out those that are invalid. # foreach my $piece (0 .. $#{$moves}) { foreach my $m (0, 1) { my $move = $$moves[$piece][$m]; my $P_prime = move $P, $piece, $move; next unless valid $P_prime; # Not a valid move. next if $C{flatten $P_prime}++; # Seen position. if (solved $P_prime) { print_solution @moves, [$piece, move2text $move]; exit 0; } push @Q, [$P_prime, @moves, [$piece, move2text $move]]; } } } say "No solution"; exit 1; __END__ 1. C -> left 2. F -> up 3. G -> up 4. L -> up 5. M -> left 6. K -> up 7. I -> right 8. H -> down 9. I -> right 10. I -> right 11. K -> up 12. J -> right 13. J -> right 14. M -> left 15. D -> down 16. D -> down 17. A -> right 18. D -> down 19. M -> right 20. H -> up 21. H -> up 22. H -> up 23. M -> left 24. D -> up 25. I -> left 26. G -> down 27. F -> down 28. C -> right 29. B -> right 30. H -> up 31. A -> left 32. D -> up 33. J -> left 34. J -> left 35. J -> left 36. D -> down 37. A -> right 38. H -> down 39. B -> left 40. C -> left 41. F -> up 42. K -> down 43. A -> right 44. L -> down 45. A -> right 46. A -> right 47. A -> right Total number of examined positions: 5031
    I don't have a board handy, so I haven't actually checked whether the solution is valid.

      But you have Perl handy so you could:

      use strict; use warnings; my @puzzle = ( [3, 1, 2], [1, 1, 3], [1, 5, 2], [2, 3, -2], [2, 4, 2], [2, 6, - +2], [4, 6, -3], [4, 1, 2], [6, 1, 3], [5, 2, 2], [5, 4, -2], [4, 5, - +2], [4, 3, 2], ); while (<DATA>) { next if !/(\d+)\.\ (\w) -> (\w+)/; my ($step, $block, $move) = ($1, $2, $3); my $blockIndex = ord($block) - ord('A'); print "Before step $step $block $move:\n"; dumpPuzzle(@puzzle); my $piece = $puzzle[$blockIndex]; if ($move eq 'up') { --$piece->[0]; } elsif ($move eq 'down') { ++$piece->[0]; } elsif ($move eq 'left') { --$piece->[1]; } else { ++$piece->[1]; } } print "Final state:\n"; dumpPuzzle(@puzzle); sub dumpPuzzle { my (@puzzle) = @_; my @rows = map {'.' x 6} 1 .. 6; for my $pieceIdx (0 .. $#puzzle) { my ($y, $x, $len) = @{$puzzle[$pieceIdx]}; if ($len < 0) { $len = -$len; substr $rows[$_ - 1], $x - 1, 1, chr($pieceIdx + ord('A')) for $y .. $y + $len - 1; } else { substr $rows[$y - 1], $x - 1, $len, chr($pieceIdx + ord('A')) x $len; } } print join "\n", @rows, ''; } __END__ 1. C -> left 2. F -> up 3. G -> up 4. L -> up 5. M -> left 6. K -> up 7. I -> right 8. H -> down 9. I -> right 10. I -> right 11. K -> up 12. J -> right 13. J -> right 14. M -> left 15. D -> down 16. D -> down 17. A -> right 18. D -> down 19. M -> right 20. H -> up 21. H -> up 22. H -> up 23. M -> left 24. D -> up 25. I -> left 26. G -> down 27. F -> down 28. C -> right 29. B -> right 30. H -> up 31. A -> left 32. D -> up 33. J -> left 34. J -> left 35. J -> left 36. D -> down 37. A -> right 38. H -> down 39. B -> left 40. C -> left 41. F -> up 42. K -> down 43. A -> right 44. L -> down 45. A -> right 46. A -> right 47. A -> right

      Prints (in part):

      Before step 1 C left: BBB.CC ..DEEF AAD..F HHMMLG .JJKLG IIIK.G Before step 2 F up: BBBCC. ..DEEF AAD..F HHMMLG .JJKLG IIIK.G Before step 3 G up: BBBCCF ..DEEF AAD... HHMMLG .JJKLG IIIK.G . . . Before step 47 A right: BBBCCF HH.EEF ....AA MMDKLG JJDKLG ..IIIG Final state: BBBCCF HH.EEF .....AA MMDKLG JJDKLG ..IIIG

      Note that I've changed the puzzle coding to use a top left corner and size/orientation array for each piece and the board size is hard wired at 8.

      True laziness is hard work
Re: Puzzle solver (rush hour)
by Limbic~Region (Chancellor) on Aug 31, 2010 at 12:32 UTC
Re: Puzzle solver (rush hour)
by Fox (Pilgrim) on Aug 31, 2010 at 13:19 UTC
    interesting puzzle, I wonder if it's harder than Klotski

      The wikipedia-Link to Klotski states that the minimum number of moves is 81. This page claims that there is a rush-hour-configuration which needs at least 93 moves to solve it.

      However I can't say which of these solutions is easier to find ;-)

      Rata