On Dec 6, 2007, at 11:44 AM, Brad Greenlee wrote:

I haven't written any Perl for fun in a while, but I recently entertained myself a bit by writing a Boggle puzzle solver in Ruby:

http://blog.footle.org/2007/12/05/cheating-for-fun/

I'm not sure if this particular puzzle has come up before in FWP, but I was curious as to how closely this yak could be shaved in Perl. I suspect Ton could do it in somewhere around 40 characters. Anyone interested?

Brad


Attached is my version coded in Perl which produces identical output. I employed a shortcutting algorithm that resulted in a 77x speedup over your Ruby implementation using the same word dictionary. My algorithm stops looking when the current prefix matches no known word. However, my code is 33% longer, counting lines. I think those extra 30 lines are worth saving 7.5 minutes of execution time.

% time perl boggler.pl lyltonzigororwho > boggler.pl.out
4.383u 0.275s 0:06.46 71.9%     0+0k 0+0io 0pf+0w
% time ruby boggler.rb lyltonzigororwho > boggler.rb.out
340.764u 5.144s 7:45.79 74.2%   0+0k 0+4io 0pf+0w
% diff boggler.pl.out boggler.rb.out

Chris

#!/usr/bin/perl -w

use strict;
use List::MoreUtils qw(uniq);

my $MIN_LETTERS = 3;
my $MAX_LETTERS = 12;
my @POINTS = (0,0,0,1,2,4,6,9,12, (16)x($MAX_LETTERS-8)); # no idea what the scoring should really be

main();
exit 0;

sub main {
   my $board_letters = shift @ARGV or syntax();
   my @board = init_board(lc $board_letters);
   my ($words, $nwords) = load_words();
   print "loaded $nwords words. solving '$board_letters'...\n";
   my @found;
   for my $i (0..$#board) {
      push @found, walk_board([EMAIL PROTECTED], $i, $words);
   }
   @found = uniq @found;
   my $score = 0;
   for my $word (@found) {
      print "found word: $word\n";
      $score += $POINTS[length $word];
   }
   print 'found ', scalar @found, " words\n";
   print "total score: $score\n";
}

sub syntax {
   die <<"SYNTAX";
Usage: boggler.pl <board string>
  where the board string is all the letters on the board, from left to right
  and top to bottom.
  Example: given the board:
   LYLT
   ONZI
   GORO
   RWHO
  the board string would be: lyltonzigororwho (case is ignored)
  the letter 'q' will automatically have a 'u' appended
SYNTAX
}

sub load_words {
   my $words = {};
   open my $fh, '<', 'words' or die 'Failed to read file "words"';
   my $nwords = 0;
   my $nlines = 0;
   while (my $line = <$fh>) {
      ++$nlines;
      # Remember that line length includes \n
      my $wordlength = -1 + length $line;
      next if $line =~ m/q[^u]/xms; # reject impossible Boggle word
      $line =~ s/qu/q/gxms;
      next if $MIN_LETTERS > $wordlength;
      next if $MAX_LETTERS < $wordlength;
      my $hash = $words;
      for my $letter ($line =~ m/(.)/gxms) {
         $hash = $hash->{$letter} ||= {};
      }
      ++$nwords;
   }
   close $fh;
   #print "Loaded $nwords words, rejected @{[$nlines-$nwords]} words\n";
   return ($words, $nlines);
}

sub init_board {
   my ($board_letters) = @_;

   my $board_size = int(0.5 + sqrt length $board_letters);
   if ($board_size * $board_size != length $board_letters) {
      die "Board is not square! ($board_size x $board_size != @{[length $board_letters]})\n";
   }

   my @board = map { { letter => $_, moves => [], seen => undef } } $board_letters =~ m/(.)/gxms;
   for my $i (0 .. $board_size - 1) {
      for my $j (0 .. $board_size - 1) {
         my $pos = $i + $j * $board_size;
         # Are we at any edges/corners?
         my $i0 = $i > 0;
         my $in = $i < $board_size -1;
         my $j0 = $j > 0;
         my $jn = $j < $board_size -1;
         push @{$board[$pos]->{moves}}, $pos + 1               if $in;
         push @{$board[$pos]->{moves}}, $pos + $board_size + 1 if $jn && $in;
         push @{$board[$pos]->{moves}}, $pos + $board_size     if $jn;
         push @{$board[$pos]->{moves}}, $pos + $board_size - 1 if $jn && $i0;
         push @{$board[$pos]->{moves}}, $pos - 1               if $i0;
         push @{$board[$pos]->{moves}}, $pos - $board_size - 1 if $j0 && $i0;
         push @{$board[$pos]->{moves}}, $pos - $board_size     if $j0;
         push @{$board[$pos]->{moves}}, $pos - $board_size + 1 if $j0 && $in;
      }
   }
   return @board;
}

sub walk_board {
   my ($board, $pos, $words, @letters) = @_;
   my $here = $board->[$pos];
   return if $here->{seen};
   my @found;
   my $nextword = $words->{$here->{letter}};
   if ($nextword) {
      if ($nextword->{"\n"}) {
         push @found, join q{}, @letters, $here->{letter};
      }
      if (@letters < $MAX_LETTERS) {
         $here->{seen} = 1;
         for my $nextpos (@{$here->{moves}}) {
            push @found, walk_board($board, $nextpos, $nextword, @letters, $here->{letter}); 
         }
         $here->{seen} = undef;
      }
   }
   return @found;
}

Reply via email to