File Coverage

blib/lib/Games/Pentominos.pm
Criterion Covered Total %
statement 61 62 98.3
branch 12 18 66.6
condition n/a
subroutine 7 7 100.0
pod 1 1 100.0
total 81 88 92.0


line stmt bran cond sub pod time code
1             #======================================================================
2             package Games::Pentominos; # see doc at end of file
3             #======================================================================
4             our $VERSION = "1.0";
5 1     1   26804 use strict;
  1         3  
  1         37  
6 1     1   5 use warnings;
  1         2  
  1         29  
7 1     1   2990 use Time::HiRes qw/time/;
  1         1786  
  1         4  
8 1     1   1266 use List::MoreUtils qw/uniq/;
  1         1274  
  1         1086  
9              
10             # work mostly with global vars because this is fastest than parameter-passing
11             our # because accessed from eval
12             $board; # cells remaining to be filled
13             my $placed; # cells filled so far
14             my $print_solution; # callback for printing a solution
15             my ($t_ini, $t_tot); # times in milliseconds
16             my $n_solutions; # how many solutions found
17             my %substitutions; # a coderef for each pentomino/permutation
18              
19             # description of the 12 pentominos. Each of them has a labelling letter,
20             # a number of permutations, and for each permutation a rectangle describing
21             # the pentomino shape. Occupied cells are shown with an 'x', untouched cells
22             # with a '.' (this character explicitly chosen so that in regexes it will
23             # match anything except a newline character).
24              
25             my %pentominos = (
26             F => [8, qw/.xx xx. x.. ..x .x. .x. .x. .x.
27             xx. .xx xxx xxx xxx xxx xx. .xx
28             .x. .x. .x. .x. x.. ..x .xx xx./],
29              
30             I => [2, qw/xxxxx x
31             ..... x
32             ..... x
33             ..... x
34             ..... x/],
35              
36             L => [4, qw/xxxx xxxx x. .x
37             x... ...x x. .x
38             .... .... x. .x
39             .... .... xx xx/],
40              
41             P => [8, qw/xx xx xxx xxx x. .x xx. .xx
42             xx xx xx. .xx xx xx xxx xxx
43             x. .x ... ... xx xx ... .../],
44              
45             S => [8, qw/xx.. ..xx xxx. .xxx x. .x x. .x
46             .xxx xxx. ..xx xx.. xx xx x. .x
47             .... .... .... .... .x x. xx xx
48             .... .... .... .... .x x. .x x./],
49              
50             T => [4, qw/xxx .x. x.. ..x
51             .x. .x. xxx xxx
52             .x. xxx x.. ..x/],
53              
54             U => [4, qw/xxx x.x xx xx
55             x.x xxx x. .x
56             ... ... xx xx/],
57              
58             V => [4, qw/xxx xxx x.. ..x
59             x.. ..x x.. ..x
60             x.. ..x xxx xxx/],
61              
62             W => [4, qw/xx. .xx x.. ..x
63             .xx xx. xx. .xx
64             ..x x.. .xx xx./],
65              
66             X => [1, qw/.x.
67             xxx
68             .x./],
69              
70             Y => [8, qw/.x x. .x x. xxxx xxxx ..x. .x..
71             xx xx .x x. .x.. ..x. xxxx xxxx
72             .x x. xx xx .... .... .... ....
73             .x x. .x x. .... .... .... ..../],
74              
75             Z => [4, qw/xx. .xx x.. ..x
76             .x. .x. xxx xxx
77             .xx xx. ..x x../],
78             );
79              
80              
81              
82             #----------------------------------------------------------------------
83             sub solve {
84             #----------------------------------------------------------------------
85 1     1 1 391 my ($self, $submitted_board, $submitted_callback) = @_;
86              
87             # initialize globals
88 1         3 ($board, $placed) = ($submitted_board, "");
89 1         2 $print_solution = $submitted_callback;
90              
91             # check if $board meets requirements
92 1         3 my $n_cells = ($board =~ tr/x//);
93 1         30 my ($board_n_cols, @others) = uniq map length, ($board =~ m/.+/g);
94 1 50       7 $n_cells == 60 or die "board does not have 60 empty cells noted as 'x'";
95 1 50       4 not @others or die "board has rows of different lengths";
96              
97             # check if callback is a coderef
98 1 50       5 ref $print_solution eq 'CODE' or die "improper callback for solutions";
99              
100             # compile the substitution subroutines
101 1         4 _compile_substitutions($board_n_cols);
102              
103             # anything up to first free cell goes to "placed"
104 1 50       9 $board =~ s/^([^x]+)// and $placed .= $1;
105              
106             # start computing solutions
107 1         6 $t_ini = time;
108 1         2 $t_tot = 0;
109 1         2 $n_solutions = 0;
110 1         6 _place_pentomino(keys %pentominos);
111             }
112              
113              
114              
115             #----------------------------------------------------------------------
116             sub _compile_substitutions {
117             #----------------------------------------------------------------------
118 1     1   2 my ($board_n_cols) = @_; # how many columns in each row
119              
120 1         3 %substitutions = ();
121 1         7 while (my ($letter, $array_ref) = each %pentominos) {
122              
123 12         22 my $n_permutations = $array_ref->[0]; # how many possible layouts
124 12         23 my $n_rows = (@$array_ref-1) / $n_permutations;
125              
126 12         24 for my $perm_id (0 .. $n_permutations-1) {
127              
128             # gather data rows for that permutation
129 59         126 my @rows = map {$array_ref->[$_ * $n_permutations + $perm_id + 1]}
  201         568  
130             (0..$n_rows-1);
131 59         134 my $n_cols = length ($rows[0]);
132              
133             # construct regex to match that permutation
134             # NOTE: \D below is just a convenience for char class [FILPSTUVWXYZx.\n]
135 59         140 my $skip_to_next_row = sprintf "\\D{%d}", $board_n_cols + 1 - $n_cols;
136 59         125 my $regex = join $skip_to_next_row, @rows;
137              
138             # remove everything before or after the touched cells
139 59         152 $regex =~ s/^[^x]+//;
140 59         234 $regex =~ s/[^x]+$//;
141              
142             # add capture brackets in regex
143 59         400 $regex =~ s/([^x]+)/($1)/g;
144              
145             # substitution string : replace 'x' by letter
146             # and brackets by captured groups
147 59         233 (my $subst = $regex) =~ s/x/$letter/g;
148 59         69 my $num_paren = 1;
149 59         407 $subst =~ s/\(.*?\)/'$'.$num_paren++/eg;
  114         289  
150              
151             # compile a sub performing the substitution
152 59         60 push @{$substitutions{$letter}},
  59         6812  
153             eval qq{sub {\$board =~ s/^$regex/$subst/}};
154             }
155             }
156             }
157              
158              
159             #----------------------------------------------------------------------
160             sub _place_pentomino { # the recursive algorithm
161             #----------------------------------------------------------------------
162             # my @letters = @_; # commented out for speed (avoiding copy)
163              
164 9060     9060   18101 my ($board_ini, $placed_ini) = ($board, $placed);
165              
166 9060         14529 foreach my $letter (@_) {
167 25905         34076 foreach my $substitution (@{$substitutions{$letter}}) {
  25905         57019  
168 94237 100       2606189 if ($substitution->()) { # try to apply this pentomino to $board
169              
170             # anything up to next free cell goes to "placed"
171 9060 50       56926 $board =~ s/^([^x]+)// and $placed .= $1;
172              
173 9060 100       17628 if (!$board) { # no more free cells, so this is a solution
174 1         7 my $t_solution = time - $t_ini;
175 1         3 $t_tot += $t_solution;
176 1         2 $n_solutions += 1;
177 1 50       10 $print_solution->($placed, $n_solutions, $t_solution, $t_tot)
178             or return; # stop searching if callback did not return true
179 0         0 $t_ini = time;
180             }
181             else {
182 9059 100       21554 _place_pentomino(grep {$_ ne $letter} @_)
  34999         77685  
183             or return;
184             }
185              
186             # restore to previous state (remove pentomino from board)
187 9048         27868 ($board, $placed) = ($board_ini, $placed_ini);
188             }
189             }
190             }
191 9048         26140 return 1; # continue searching
192             }
193              
194              
195             __END__