File Coverage

blib/lib/Freecell/App/Tableau.pm
Criterion Covered Total %
statement 15 222 6.7
branch 0 86 0.0
condition 0 54 0.0
subroutine 5 26 19.2
pod 18 18 100.0
total 38 406 9.3


line stmt bran cond sub pod time code
1             package Freecell::App::Tableau;
2 1     1   5 use version;
  1         2  
  1         6  
3             our $VERSION = '0.03';
4 1     1   65 use warnings;
  1         2  
  1         23  
5 1     1   6 use strict;
  1         2  
  1         29  
6 1     1   1076 use Storable qw(dclone);
  1         3581  
  1         63  
7 1     1   8 use List::Util qw(min);
  1         2  
  1         3385  
8            
9             my %conf = (
10             winxp_opt => 0, # 1 is solve for XP
11             winxp_warn => 0, # 1 is invalid for XP
12             );
13             sub _property {
14 0     0     my ($class, $attr, $value) = @_;
15 0 0         if (defined $value) {
16 0           my $oldv = $conf{$attr};
17 0           $conf{$attr} = $value;
18 0           return $oldv;
19             }
20 0           return $conf{$attr};
21             }
22 0     0 1   sub winxp_opt () { return shift->_property('winxp_opt', @_) }
23 0     0 1   sub winxp_warn () { return shift->_property('winxp_warn', @_) }
24            
25 0     0 1   sub rank { $_[0] & 15 }
26 0     0 1   sub suit { $_[0] >> 4 & 3 }
27 0     0 1   sub opposite_colors { ( $_[0] & 16 ) != ( $_[1] & 16 ) }
28            
29             sub new {
30 0     0 1   my ( $class, $key, $token ) = @_;
31 0           my $self = [ map [ (0) x 21 ], 0 .. 7 ];
32 0           bless $self, $class;
33 0           $self;
34             }
35            
36             sub from_string {
37 0     0 1   my ( $self, $string ) = @_;
38 0           my $r = 0;
39 0           foreach ( split /\n/, $string ) {
40 0           my $c = 0;
41 0           while (/(.)(.) ?/g) {
42 0           my ( $rank, $suit ) = ( $1, $2 );
43 0 0         unless ( "$rank$suit" eq " " ) {
44 0           $rank =~ tr/ATJQK/1\:\;\<\=/;
45 0           $suit =~ tr/DCHS/0123/;
46 0           $self->[$c][$r] =
47             64 | ( ( 3 & ord $suit ) << 4 ) + ( 15 & ord $rank );
48             }
49 0           $c++;
50             }
51 0           $r++;
52             }
53             # fix home if out of order
54            
55 0           my %home = map {
56 0           my $card = $self->[$_][0];
57 0           suit($card) + 4, $card;
58             } 4 .. 7;
59 0           foreach ( 4 .. 7 ) {
60 0 0         $self->[$_][0] = exists( $home{$_} ) ? $home{$_} : 0;
61             }
62 0           $self;
63             }
64            
65             sub from_token {
66 0     0 1   my ( $self, $key, $token ) = @_;
67 0           my @i = @{$token};
  0            
68 0           my @t = split / /, $key;
69 0           my @f = split //, shift @t;
70 0           foreach ( splice @i, 0, @f ) { # array,offset,length
71 0           $self->[$_][0] = ord shift @f;
72             }
73 0           foreach my $i (@i) {
74 0           my $j = 1;
75 0           foreach ( split //, shift @t ) {
76 0           $self->[$i][ $j++ ] = ord $_;
77             }
78             }
79 0           $self;
80             }
81            
82             sub from_deal { # http://rosettacode.org/wiki/Deal_cards_for_FreeCell#Perl
83 0     0 1   my ( $self, $s ) = @_;
84             my $rnd = sub {
85 0     0     return ( ( $s = ( $s * 214013 + 2531011 ) % 2**31 ) >> 16 );
86 0           };
87 0           my @d;
88 0           for my $b ( split "", "A23456789TJQK" ) {
89 0           push @d, map ( "$b$_", qw/C D H S/ );
90             }
91 0           for my $idx ( reverse 0 .. $#d ) {
92 0           my $r = $rnd->() % ( $idx + 1 );
93 0           @d[ $r, $idx ] = @d[ $idx, $r ];
94             }
95 0           my $cards = [ reverse @d ];
96 0           my $num_cards_in_height = 8;
97 0           my $string = '';
98 0           while (@$cards) {
99 0           $string .= join( ' ', splice( @$cards, 0, 8 ) ) . "\n";
100             }
101 0           $self->from_string( "\n" . $string );
102             }
103            
104             sub to_token {
105 0     0 1   my $self = shift;
106 0           my @t = sort { $a->[1] cmp $b->[1] } grep $_->[1],
  0            
107 0           map [ $_, join "", map chr($_), grep $_, @{ $self->[$_] }[ 1 .. 20 ] ],
108             0 .. 7;
109 0           my @f = sort { $a->[1] <=> $b->[1] } grep $_->[1],
  0            
110             map [ $_, $self->[$_][0] ], 0 .. 7;
111            
112 0           join( " ", join( "", map chr( $_->[1] ), @f ), map $_->[1], @t ),
113             [ ( map $_->[0], @f ), ( map $_->[0], @t ) ];
114             }
115            
116             sub undo {
117 0     0 1   my $self = shift;
118 0           foreach ( reverse @{ $_[0] } ) {
  0            
119 0           my ( $src_col, $src_row, $dst_col, $dst_row ) = @$_;
120            
121             # return dst back to src
122            
123 0           $self->[$src_col][$src_row] = $self->[$dst_col][ $dst_row + 1 ];
124            
125             # if dst == home && rank > Ace decrement home else clear
126            
127 0 0 0       if ( $dst_col > 3
      0        
128             && $dst_row < 0
129             && rank( $self->[$dst_col][ $dst_row + 1 ] ) > 1 )
130             {
131 0           $self->[$dst_col][ $dst_row + 1 ]--;
132             }
133             else {
134 0           $self->[$dst_col][ $dst_row + 1 ] = 0;
135             }
136             }
137             }
138            
139             sub play {
140 0     0 1   my $self = shift;
141 0           my ( $src_col, $src_row, $dst_col, $dst_row ) = @{ $_[0] };
  0            
142            
143             # dst points to last card in col so move src to dst_row +1
144            
145 0           $self->[$dst_col][ $dst_row + 1 ] = $self->[$src_col][$src_row];
146 0           $self->[$src_col][$src_row] = 0;
147             }
148            
149             sub _home {
150 0     0     my ( $self, $move, $src, $src_col, $src_row, $type ) = @_;
151            
152             # src rank == home rank+1 and an A or duece
153            
154 0 0 0       if (
      0        
155             rank($src) == rank( $self->[ suit($src) + 4 ][0] ) + 1
156             && (
157             rank($src) < 3
158            
159             # or src rank <= rank+1 of both home cards of opposite color
160            
161             || 2 ==
162             grep rank($src) <= rank($_) + 1, # rank($self->[suit($src) + 4][0]
163            
164             # home cards of opposite colors
165            
166             ( map $_->[0], @$self )
167            
168             # index of home cards of opposite color; << 4 = 0100.... 0101.... 0110.... 0111....
169            
170             [ grep opposite_colors( $src, $_ << 4 ), 4 .. 7 ]
171            
172             )
173             )
174             {
175            
176 0           $self->play( $_ = [ $src_col, $src_row, suit($src) + 4, -1, $type ] );
177 0           push @{$move}, $_;
  0            
178 0           1;
179             }
180             else {
181 0           0;
182             }
183             }
184            
185             sub autoplay {
186 0     0 1   my ( $self, $move ) = @_;
187 0           my ( $safe, @z, @auto ) = 1;
188 0           while ($safe) {
189 0           map { $z[$_] = grep $_, @{ $self->[$_] }[ 1 .. 20 ] } 0 .. 7;
  0            
  0            
190 0           $safe = 0;
191 0           foreach my $c ( 0 .. 3 ) {
192 0           my $src = $self->[$c][0];
193 0 0         next unless $src;
194 0   0       $safe ||=
195             $self->_home( $move, $src, $c, 0, 'afh' ); # auto free -> home
196             }
197 0           foreach my $c ( 0 .. 7 ) {
198 0           my $r = $z[$c];
199 0 0         next unless $r; # any cards in src col?
200 0           my $src = $self->[$c][$r]; # yes, get last one;
201 0   0       $safe ||=
202             $self->_home( $move, $src, $c, $r, 'ach' ); # auto col -> home
203             }
204             }
205             }
206            
207             sub generate_nodelist {
208 0     0 1   my ( $self ) = @_;
209 0           my @z = map { scalar grep $_, @$_[ 1 .. 20 ] } @$self;
  0            
210 0           my @empty = grep !$self->[$_][1], 0 .. 7;
211 0           my @free = grep !$self->[$_][0], 0 .. 3;
212 0           my @moves;
213            
214 0           foreach my $c ( 0 .. 3 ) {
215 0           my $src = $self->[$c][0];
216 0 0         next unless $src;
217 0 0         if ( rank($src) - 1 == rank( $self->[ suit($src) + 4 ][0] ) ) {
218 0           push @moves, [ [ $c, 0, suit($src) + 4, -1, 'fh' ] ]; # free->home
219             }
220 0 0         if ( @empty > 0 ) {
221 0           push @moves, [ [ $c, 0, $empty[0], 0, 'fe' ] ]; # free->empty
222             }
223 0           foreach my $j ( 0 .. 7 ) {
224 0 0         next unless $z[$j];
225 0           my $dst = $self->[$j][ $z[$j] ];
226 0 0 0       if ( rank($src) + 1 == rank($dst)
227             && opposite_colors( $src, $dst ) )
228             {
229 0           push @moves, [ [ $c, 0, $j, $z[$j], 'fc' ] ]; # free -> col
230             }
231             }
232             }
233            
234 0           foreach my $c ( 0 .. 7 ) {
235 0 0         next unless $z[$c]; # any cards in src col?
236 0           my $src = $self->[$c][ $z[$c] ]; # then get last one;
237 0 0         if ( rank($src) - 1 == rank( $self->[ suit($src) + 4 ][0] ) ) {
238 0           push @moves,
239             [ [ $c, $z[$c], suit($src) + 4, -1, 'ch' ] ]; # col->home
240             }
241 0 0         if ( @free > 0 ) {
242 0           push @moves, [ [ $c, $z[$c], $free[0], -1, 'cf' ] ]; # col->free
243             }
244 0 0 0       if ( @empty > 0
245             && $z[$c] > 1 )
246             {
247 0           push @moves, [ [ $c, $z[$c], $empty[0], 0, 'ce' ] ]; # col->empty
248             }
249            
250 0           my $flag = 1;
251 0           foreach my $j ( 0 .. 7 ) {
252 0 0         next if $c == $j;
253 0 0         next unless $z[$j];
254            
255             # my $src = $self->[$c][$z[$c]]; # then get last one;
256 0           my $dst = $self->[$j][ $z[$j] ];
257            
258 0 0 0       if ( rank($src) + 1 == rank($dst)
259             && opposite_colors( $src, $dst ) )
260             {
261 0           push @moves, [ [ $c, $z[$c], $j, $z[$j], 'cc' ] ]; # col->col
262             }
263            
264             # super move
265 0 0         if ( $z[$c] > 1 ) {
266 0           foreach my $k ( reverse 1 .. $z[$c] - 1 ) {
267 0           my $srx = $self->[$c][$k];
268 0 0 0       unless ( rank($srx) - 1 == rank( $self->[$c][ $k + 1 ] )
269             && opposite_colors( $srx, $self->[$c][ $k + 1 ] ) )
270             {
271 0           last;
272             }
273 0 0 0       if ( @empty > 0
    0 0        
      0        
274             && $k > 1
275             && $flag == 1
276             && ( $conf{winxp_opt} ? min( 1, scalar @empty ) : @empty ) *
277             ( @free + 1 ) >= ( @_ = $k .. $z[$c] ) )
278             { # e*(f+1)
279 0           my $x = 0;
280 0           push @moves,
281 0           [ map { [ $c, $_, $empty[0], $x++, 'sce' ] }
282             $k .. $z[$c] ]; # col->empty
283             }
284 0 0 0       if ( rank($srx) + 1 == rank($dst)
    0 0        
285             && opposite_colors( $srx, $dst )
286             && (
287             ( $conf{winxp_opt} ? min( 1, scalar @empty ) : @empty ) + 1 )
288             * ( @free + 1 ) >= ( @_ = $k .. $z[$c] ) )
289             { # (e+1)*(f+1)
290 0           my $x = $z[$j];
291 0           push @moves,
292 0           [ map { [ $c, $_, $j, $x++, 'scc' ] } $k .. $z[$c] ]
293             ; # col->col
294             }
295             }
296 0           $flag = 0;
297             }
298             }
299             }
300 0           \@moves;
301             }
302            
303             sub to_card {
304 0     0 1   qw(0 A 2 3 4 5 6 7 8 9 T J Q K) [ rank( $_[0] ) ]
305             . qw(D C H S) [ suit( $_[0] ) ];
306             }
307            
308             sub to_string {
309 0     0 1   my $self = shift;
310 0           my ( $x, $result ) = 0;
311 0           while (1) {
312 0           my @r = map {
313 0           my $card = $_->[$x];
314 0 0         $card == 0 ? " " : to_card($card) . " ";
315             } @$self;
316 0           $result .= sprintf "%s\n", join "", @r;
317 0 0 0       last if $x++ > 0 && 8 == grep $_ eq " ", @r;
318             }
319 0           $result;
320             }
321            
322             sub notation {
323 0     0 1   my $self = dclone shift;
324             my (
325 0           $i, $super_cnt, $super_orig, $std_src,
326             $std_dst, @dsc_src, $dsc_dst, %auto,
327             @z, @empty, @free
328             ) = ( 0, 0, "" );
329            
330 0           map {
331 0           my ( $src_col, $src_row, $dst_col, $dst_row, $origin ) = @$_;
332            
333             # build both standard and descriptive notation
334            
335 0 0         if ( $i == 0 ) {
336 0 0         $std_src =
    0          
337             ( $src_row > 0 ? $src_col + 1
338             : $src_col > 3 ? "h"
339             : qw(a b c d) [$src_col] );
340 0 0         $std_dst =
    0          
341             ( $dst_row > -1 ? $dst_col + 1
342             : $dst_col > 3 ? "h"
343             : qw(a b c d) [$dst_col] );
344 0 0         $dsc_dst =
    0          
    0          
345             $dst_row == 0 ? "empty column"
346             : $std_dst =~ /\d/ ? to_card( $self->[$dst_col][$dst_row] )
347             : $std_dst =~ /h/ ? "home"
348             : "freecell";
349             }
350            
351             # gather move card cnt for super move
352            
353 0 0         if ( $origin =~ /^s/ ) {
354 0 0         if ( $super_cnt == 0 ) {
355 0           $super_orig = $origin;
356 0           @empty = grep !$self->[$_][1], 0 .. 7;
357 0           @free = grep !$self->[$_][0], 0 .. 3;
358             }
359 0           $super_cnt++;
360             }
361            
362             # build descriptive source notation
363            
364 0           my $num = $self->[$src_col][$src_row];
365 0 0         if ( $origin =~ /^a/ ) {
366 0           $auto{ suit($num) }[ rank($num) ] = to_card($num);
367             }
368             else {
369 0           push @dsc_src, to_card($num);
370             }
371 0           $self->play($_);
372 0           $i++;
373 0           } @{ $_[0] }; # node array
374            
375             # if a super move, is it valid for XP ?
376            
377 0 0 0       if (
378             $super_cnt
379             && !(
380             ( min( 1, scalar @empty ) + $super_orig =~ /c$/ ) * ( @free + 1 )
381             >= $super_cnt
382             )
383             )
384             {
385 0           $conf{winxp_warn} = 1;
386             }
387            
388             # output notation
389            
390 0           $std_src . $std_dst, # standard notation
391             $dsc_src[0] . ( @dsc_src == 1 ? "" : "-" . $dsc_src[-1] ),
392             $dsc_dst, # descriptive notation
393             join ", ", map {
394 0 0         my @h = grep $_, @{ $auto{$_} }; # autoplay notation
  0            
395 0 0         $h[0] . ( @h == 1 ? "" : "-" . $h[-1] );
396             } sort keys %auto;
397             }
398            
399             sub heuristic {
400 0     0 1   my ($self) = @_;
401 0           my $score = 64;
402 0           my @z = map { scalar grep $_, @$_[ 1 .. 20 ] } @$self;
  0            
403 0           map $score -= rank( $self->[$_][0] ), 4 .. 7; # -sum home
404 0           $score -= grep !$self->[$_][1], 0 .. 7; # -empty
405 0           $score -= grep !$self->[$_][0], 0 .. 3; # -free
406            
407 0           my $seq = 0;
408 0           foreach my $c ( 0 .. 7 ) { # +sum column sequence breaks
409 0 0         next unless $z[$c] > 1;
410 0           foreach my $r ( 1 .. ( $z[$c] - 1 ) ) {
411 0           my $src0 = $self->[$c][$r];
412 0           my $src1 = $self->[$c][ $r + 1 ];
413 0   0       my $brk = !opposite_colors( $src1, $src0 )
414             || rank($src1) + 1 != rank($src0);
415 0 0         if ($brk) {
416 0           $score += $brk; # algorithn 1
417 0           $seq += $src1 >= $src0; # algorithm 2 - major seq break
418             }
419             }
420             }
421 0           [ $score, $score + $seq ];
422             }
423            
424             __END__