RE: [QUIZ] Perl 'Hard' Quiz of the Whatever #2008-12-28 - Symmetric Sokoban

"Jurgen Pletinckx" <[email protected]> Fri, 04 Jan 2008 15:44:15 +0100
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
Shlomi Fish <mailto:shlomif-ik1l9ssToec+JF/[email protected]> mailed on 28 December 2007
12:31:

| IMPORTANT: Please do not post solutions, hints, or other spoilers
| until at least 60 hours after the date of this message.  Thanks.
|=20
| I was told the "What does this code do?" quizzes were not as good as a
new
| programming task, so here's a more traditional QOTW that I came up
with. In
| this quiz you'll try to solve the following Sokoban (
| http://en.wikipedia.org/wiki/Sokoban ) puzzle using Perl:
|=20
| {{{{{{{{{{{{{{
|   ####
|   #  #
|   #  ####
| ###$.$  #
| #  .@.  #
| #  $.$###
| ####  #
|    #  #
|    ####
| }}}}}}}}}}}}}}


Well past the spoiler line now ...

(I do prefer these tasks to "WDTCD?" quizzes. *And* I=20
had some tuits over the holidays. Anyone else out there?)

I've tried not to be too clever.=20

* Standard breadth-first search, no heuristics

* tree is stored in a hash, as child =3D> parent pairs

* sparse tree (I only record the fastest way to get to a state)

* the text representation of a board state is used=20
  for both move detection and for storing

* subroutine 'move' checks whether the sokoban can
  move to the right, and what the result would be.
  By all rights it should be simple and elegant code.
  Instead, it's a workmanlike hack. Emphasis is on=20
  'works', however.

* subroutine 'moves' finds all possible descendants
  of a given board state by presenting consecutive=20
  rotated board states to sub move

There are gains to be made by taking advantage of the
fourfold rotational symmetry here. But that would be
clever.

Also, this text representation (xsb format) is highly
compressible, and it might be worthwhile to store the
board states that way. This is a small puzzle (25 pos-
itions, 4 boxes - only 25*24*23*22*21/4*3*2*1 =3D 265650=20
board states possible).

I get a solution (77 steps) in slighly over three min-
utes on this box (WinXP, single CPU, 3GHz).=20


##############################################
#!/perl

use strict;
use warnings;
# use Devel::Size qw(total_size);

my $puzzle =3D<<'EOP';
  ####
  #  #
  #  ####
###$.$  #
#  .@.  #
#  $.$###
####  #
   #  #
   ####
EOP
solve($puzzle);


sub move
{
  my $puzzle =3D shift;
  my $pos =3D index($puzzle, '@');
  my $tar0 =3D my $tar1 =3D 0;
 =20
  if ($pos =3D=3D -1)
  {
    $pos =3D index($puzzle, '+');
    $tar0 =3D 1;
  }
 =20
  my $next =3D substr($puzzle,$pos+1,1);
 =20
  if ($next eq ' ')
  {
    substr($puzzle, $pos+1, 1, '@');
    substr($puzzle, $pos,   1, $tar0 ? '.' : ' ');
    return $puzzle;   =20
  }
  elsif ($next eq '.')
  {
    substr($puzzle, $pos+1, 1, '+');
    substr($puzzle, $pos,   1, $tar0 ? '.' : ' ');
    return $puzzle;   =20
  }
  elsif ($next eq '#')
  {
    return undef;
  }
 =20
  # only boxes left
  $tar1 =3D 1 if $next eq '*';
  die "expecting a box (*\$), found '$next' in \n$puzzle\n" unless $next
eq '$' or $tar1;
  $next =3D substr($puzzle,$pos+2,1);
 =20
  if ($next eq ' ')
  {
    substr($puzzle, $pos+2, 1, '$');
    substr($puzzle, $pos+1, 1, $tar1 ? '+' : '@');
    substr($puzzle, $pos,   1, $tar0 ? '.' : ' ');
    return $puzzle;       =20
  }
  elsif ($next eq '.')
  {
    substr($puzzle, $pos+2, 1, '*');
    substr($puzzle, $pos+1, 1, $tar1 ? '+' : '@');
    substr($puzzle, $pos,   1, $tar0 ? '.' : ' ');
    return $puzzle;       =20
  }
  return undef;
}

sub rotate
{
  my $puzzle =3D shift;
  my @lines =3D split /\n/, $puzzle;

  my $maxl =3D 0;
  for (@lines)
  {
    my $l =3D length;
    $maxl =3D $l if $l > $maxl;
  }
 =20
  my @newlines;
  for my $j (0..$#lines)
  {
    my $l =3D length $lines[$j];
    for my $i (0..$maxl-1)
    {
      my $char;
      if ($i >=3D $l)
      {
        $char =3D ' ';
      }
      else
      {
        $char =3D substr($lines[$j],$i,1);
      }
      $newlines[$maxl-$i-1] .=3D $char;
    }
  }
 =20
  my $newpuzzle =3D (join "\n", @newlines) . "\n";
  return $newpuzzle;
}

sub moves
{
  my $puzzle =3D shift;
  my @moves;
 =20
  my $move =3D move($puzzle);
  push @moves, $move if defined $move;
 =20
  my $rot =3D rotate($puzzle);
  $move =3D move($rot);
  push @moves, rotate(rotate(rotate($move))) if defined $move;
 =20
  $rot =3D rotate($rot);
  $move =3D move($rot);
  push @moves, rotate(rotate($move)) if defined $move;
 =20
  $rot =3D rotate($rot);
  $move =3D move($rot);
  push @moves, rotate($move) if defined $move;
 =20
  return @moves;
}

sub is_solved
{
  my $puzzle =3D shift;
 =20
  return 0 if $puzzle =3D~ /\./;
  return 0 if $puzzle =3D~ /\+/;
 =20
  if ($puzzle =3D~ /\$/)
  {
    die "Apparently more boxes than targets in \n$puzzle\n";
  }
 =20
  return 1;
}


sub solve
{
  my $puzzle =3D shift;
  my %adjacency =3D ($puzzle =3D> 'START');
  my @queue =3D ($puzzle);
  my $maxsize =3D 0;
  while (1)
  {
    my $state =3D shift @queue;
    for my $cand (moves($state))
    {
      next if exists $adjacency{$cand};
      $adjacency{$cand} =3D $state;
      if (is_solved($cand))
      {
        print_sol($cand, \%adjacency);
#	my $size =3D total_size(\%adjacency) + total_size(\@queue);
#	print "Tree contains $size bytes.\n";
        return;
      }
      push @queue, $cand;
    }
#    print join "\t", scalar(keys %adjacency), scalar @queue, "\n";
  }
 =20
  die "Unsolvable?"; =20
}

sub print_sol
{
  my $solution =3D shift;
  my $href =3D shift;
 =20
  my @steps;
  while ($solution ne 'START')
  {
    unshift @steps, $solution;
    $solution =3D $href->{$solution};
  }
 =20
  print join "---\n", @steps;
  print "---\n... and it took me only ".(time - $^T)." seconds!";
}


--=20
Jurgen Pletinckx
AlgoNomics NV