File Coverage

blib/lib/Games/Solitaire/BlackHole/Solver/App/Base.pm
Criterion Covered Total %
statement 21 258 8.1
branch 0 76 0.0
condition 0 5 0.0
subroutine 7 30 23.3
pod n/a
total 28 369 7.5


line stmt bran cond sub pod time code
1             package Games::Solitaire::BlackHole::Solver::App::Base;
2             $Games::Solitaire::BlackHole::Solver::App::Base::VERSION = '0.4.0';
3 1     1   587 use Moo;
  1         3  
  1         5  
4 1     1   1032 use Getopt::Long qw/ GetOptions /;
  1         12644  
  1         6  
5 1     1   723 use Pod::Usage qw/ pod2usage /;
  1         49829  
  1         93  
6 1     1   518 use Math::Random::MT ();
  1         1065  
  1         32  
7 1     1   8 use List::Util qw/ any /;
  1         3  
  1         3038  
8              
9             extends('Exporter');
10              
11             has [
12             '_active_record', '_active_task',
13             '_board_cards', '_board_lines',
14             '_board_values', '_init_foundation',
15             '_init_queue', '_init_tasks_configs',
16             '_is_good_diff', '_prelude',
17             '_prelude_iter', '_prelude_string',
18             '_talon_cards', '_positions',
19             '_quiet', '_output_handle',
20             '_output_fn', '_tasks',
21             '_tasks_by_names', '_task_idx',
22             ] => ( is => 'rw' );
23             our %EXPORT_TAGS = ( 'all' => [qw($card_re)] );
24             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
25              
26             my @ranks = ( "A", 2 .. 9, qw(T J Q K) );
27             my %ranks_to_n = ( map { $ranks[$_] => $_ } 0 .. $#ranks );
28              
29             sub _RANK_KING
30             {
31 0     0     return $ranks_to_n{'K'};
32             }
33              
34             my $card_re_str = '[' . join( "", @ranks ) . '][HSCD]';
35             our $card_re = qr{$card_re_str};
36              
37             sub _get_rank
38             {
39 0     0     shift;
40 0           return $ranks_to_n{ substr( shift(), 0, 1 ) };
41             }
42              
43             sub _calc_lines
44             {
45 0     0     my $self = shift;
46 0           my $filename = shift;
47              
48 0           my @lines;
49 0 0         if ( $filename eq "-" )
50             {
51 0           @lines = ;
52             }
53             else
54             {
55 0 0         open my $in, "<", $filename
56             or die
57             "Could not open $filename for inputting the board lines - $!";
58 0           @lines = <$in>;
59 0           close($in);
60             }
61 0           chomp @lines;
62 0           $self->_board_lines( \@lines );
63 0           return;
64             }
65              
66             sub _trace_solution
67             {
68 0     0     my ( $self, $final_state ) = @_;
69 0           my $output_handle = $self->_output_handle;
70 0           $output_handle->print("Solved!\n");
71              
72 0 0         return if $self->_quiet;
73              
74 0           my $state = $final_state;
75 0           my ( $prev_state, $col_idx );
76              
77 0           my @moves;
78             LOOP:
79 0           while ( ( $prev_state, $col_idx ) = @{ $self->_positions->{$state} } )
  0            
80             {
81 0 0         last LOOP if not defined $prev_state;
82             push @moves,
83             (
84 0 0         ( $col_idx == @{ $self->_board_cards } )
  0            
85             ? "Deal talon " . $self->_talon_cards->[ vec( $prev_state, 1, 8 ) ]
86             : $self->_board_cards->[$col_idx]
87             [ vec( $prev_state, 4 + $col_idx, 4 ) - 1 ]
88             );
89             }
90             continue
91             {
92 0           $state = $prev_state;
93             }
94 0           print {$output_handle} map { "$_\n" } reverse(@moves);
  0            
  0            
95              
96 0           return;
97             }
98              
99             sub _my_exit
100             {
101 0     0     my ( $self, $verdict, ) = @_;
102 0           my $output_handle = $self->_output_handle;
103              
104 0 0         if ( !$verdict )
105             {
106 0           $output_handle->print("Unsolved!\n");
107             }
108              
109 0 0         if ( defined( $self->_output_fn ) )
110             {
111 0           close($output_handle);
112             }
113              
114 0           exit( !$verdict );
115              
116 0           return;
117             }
118              
119             sub _parse_board
120             {
121 0     0     my ($self) = @_;
122 0           my $lines = $self->_board_lines;
123              
124 0           my $found_line = shift(@$lines);
125              
126 0           my $init_foundation;
127 0 0         if ( my ($card) = $found_line =~ m{\AFoundations: ($card_re)\z} )
128             {
129 0           $init_foundation = $self->_get_rank($card);
130             }
131             else
132             {
133 0           die "Could not match first foundation line!";
134             }
135 0           $self->_init_foundation($init_foundation);
136              
137 0           $self->_board_cards( [ map { [ split /\s+/, $_ ] } @$lines ] );
  0            
138             $self->_board_values(
139             [
140             map {
141 0           [ map { $self->_get_rank($_) } @$_ ]
  0            
142 0           } @{ $self->_board_cards }
  0            
143             ]
144             );
145 0           return;
146             }
147              
148             sub _set_up_initial_position
149             {
150 0     0     my ( $self, $talon_ptr ) = @_;
151              
152 0           my $init_state = "";
153              
154 0           vec( $init_state, 0, 8 ) = $self->_init_foundation;
155 0           vec( $init_state, 1, 8 ) = $talon_ptr;
156              
157 0           my $board_values = $self->_board_values;
158 0           foreach my $col_idx ( keys @$board_values )
159             {
160             vec( $init_state, 4 + $col_idx, 4 ) =
161 0           scalar( @{ $board_values->[$col_idx] } );
  0            
162             }
163              
164             # The values of $positions is an array reference with the 0th key being the
165             # previous state, and the 1th key being the column of the move.
166 0           $self->_positions( +{ $init_state => [ undef, undef, 1, 0, ], } );
167              
168 0           $self->_init_queue( [$init_state] );
169              
170 0           return;
171             }
172              
173             sub _shuffle
174             {
175 0     0     my ( $self, $gen, $arr ) = @_;
176              
177 0           my $i = $#$arr;
178 0           while ( $i > 0 )
179             {
180 0           my $j = int( $gen->rand( $i + 1 ) );
181 0 0         if ( $i != $j )
182             {
183 0           @$arr[ $i, $j ] = @$arr[ $j, $i ];
184             }
185 0           --$i;
186             }
187 0           return;
188             }
189              
190             my $TASK_NAME_RE = qr/[A-Za-z0-9_]+/;
191             my $TASK_ALLOC_RE = qr/[0-9]+\@$TASK_NAME_RE/;
192              
193             sub _process_cmd_line
194             {
195 0     0     my ( $self, $args ) = @_;
196              
197 0           my $quiet = '';
198 0           my $output_fn;
199 0           my ( $help, $man, $version );
200 0           my @tasks;
201              
202             my $push_task = sub {
203 0     0     push @tasks,
204             +{
205             name => undef(),
206             seed => 0,
207             };
208 0           return;
209 0           };
210 0           $push_task->();
211             GetOptions(
212             "o|output=s" => \$output_fn,
213             "quiet!" => \$quiet,
214             "next-task" => sub {
215 0     0     $push_task->();
216 0           return;
217             },
218             "prelude=s" => sub {
219 0     0     my ( undef, $val ) = @_;
220 0 0         if ( $val !~ /\A$TASK_ALLOC_RE(?:,$TASK_ALLOC_RE)*\z/ )
221             {
222 0           die "Invalid prelude string '$val' !";
223             }
224 0           $self->_prelude_string($val);
225 0           return;
226             },
227             "task-name=s" => sub {
228 0     0     my ( undef, $val ) = @_;
229 0 0         if ( $val !~ /\A$TASK_NAME_RE\z/ )
230             {
231 0           die "Invalid task name '$val' - must be alphanumeric!";
232             }
233 0           $tasks[-1]->{name} = $val;
234 0           return;
235             },
236             "seed=i" => sub {
237 0     0     my ( undef, $val ) = @_;
238 0           $tasks[-1]->{seed} = $val;
239 0           return;
240             },
241             'help|h|?' => \$help,
242             'man' => \$man,
243             'version' => \$version,
244 0 0         %{ $args->{extra_flags} },
  0            
245             ) or pod2usage(2);
246 0 0         if ( @tasks == 1 )
247             {
248 0           $tasks[-1]{name} = 'default';
249             }
250 0 0   0     if ( any { !defined $_->{name} } @tasks )
  0            
251             {
252 0           die "You did not specify the task-names for some tasks";
253             }
254 0           $self->_init_tasks_configs( \@tasks );
255              
256 0 0         pod2usage(1) if $help;
257 0 0         pod2usage( -exitstatus => 0, -verbose => 2 ) if $man;
258              
259 0 0         if ($version)
260             {
261 0           print
262             "black-hole-solve version $Games::Solitaire::BlackHole::Solver::App::Base::VERSION\n";
263 0           exit(0);
264             }
265              
266 0           $self->_quiet($quiet);
267 0           my $output_handle;
268              
269 0 0         if ( defined($output_fn) )
270             {
271 0 0         open( $output_handle, ">", $output_fn )
272             or die "Could not open '$output_fn' for writing";
273             }
274             else
275             {
276 0           open( $output_handle, ">&STDOUT" );
277             }
278 0           $self->_output_fn($output_fn);
279 0           $self->_output_handle($output_handle);
280 0           $self->_calc_lines( shift(@ARGV) );
281              
282 0           return;
283             }
284              
285             sub _set_up_tasks
286             {
287 0     0     my ($self) = @_;
288              
289 0           my @tasks;
290             my %tasks_by_names;
291 0           foreach my $task_rec ( @{ $self->_init_tasks_configs } )
  0            
292             {
293 0           my $iseed = $task_rec->{seed};
294 0           my $name = $task_rec->{name};
295 0           my $_task_idx = @tasks;
296             my $task_obj =
297             Games::Solitaire::BlackHole::Solver::App::Base::Task->new(
298             {
299             _name => $name,
300 0   0       _queue => [ @{ $self->_init_queue } ],
  0            
301             _seed => $iseed,
302             _gen => Math::Random::MT->new( $iseed || 1 ),
303             _remaining_iters => 100,
304             _task_idx => $_task_idx,
305             }
306             );
307 0           push @tasks, $task_obj;
308 0 0         if ( exists $tasks_by_names{$name} )
309             {
310 0           die "Duplicate task-name '$name'!";
311             }
312 0           $tasks_by_names{$name} = $task_obj;
313             }
314 0           $self->_task_idx(0);
315 0           $self->_tasks( \@tasks );
316 0           $self->_tasks_by_names( \%tasks_by_names );
317 0           my @prelude;
318             my $process_item = sub {
319 0     0     my $s = shift;
320 0 0         if ( my ( $quota, $name ) = $s =~ /\A([0-9]+)\@($TASK_NAME_RE)\z/ )
321             {
322 0 0         if ( not exists $self->_tasks_by_names->{$name} )
323             {
324 0           die "Unknown task name $name in prelude!";
325             }
326 0           my $task_obj = $self->_tasks_by_names->{$name};
327 0           return Games::Solitaire::BlackHole::Solver::App::Base::PreludeItem
328             ->new(
329             {
330             _quota => $quota,
331             _task => $task_obj,
332             _task_idx => $task_obj->_task_idx,
333             _task_name => $task_obj->_name,
334             }
335             );
336             }
337             else
338             {
339 0           die "foo";
340             }
341 0           };
342 0 0         if ( my $_prelude_string = $self->_prelude_string )
343             {
344             push @prelude,
345             (
346 0           map { $process_item->($_) }
  0            
347             split /,/, $_prelude_string
348             );
349             }
350 0           $self->_prelude( \@prelude );
351 0           $self->_prelude_iter(0);
352 0 0         if ( @{ $self->_prelude } )
  0            
353             {
354 0           $self->_next_task;
355             }
356 0           return;
357             }
358              
359             sub _next_task
360             {
361 0     0     my ($self) = @_;
362 0 0         if ( $self->_prelude_iter < @{ $self->_prelude } )
  0            
363             {
364 0           my $alloc = $self->_prelude->[ $self->{_prelude_iter}++ ];
365 0           my $task = $alloc->_task;
366 0 0         if ( !@{ $task->_queue } )
  0            
367             {
368 0           return $self->_next_task;
369             }
370 0           $task->_remaining_iters( $alloc->_quota );
371 0           $self->_active_task($task);
372 0           return 1;
373             }
374 0           my $tasks = $self->_tasks;
375 0 0         return if !@$tasks;
376 0 0         if ( !@{ $tasks->[ $self->_task_idx ]->_queue } )
  0            
377             {
378 0           splice @$tasks, $self->_task_idx, 1;
379 0           return $self->_next_task;
380             }
381 0           my $task = $tasks->[ $self->_task_idx ];
382 0           $self->_task_idx( ( $self->_task_idx + 1 ) % @$tasks );
383 0           $task->_remaining_iters(100);
384 0           $self->_active_task($task);
385              
386 0           return 1;
387             }
388              
389             sub _get_next_state
390             {
391 0     0     my ($self) = @_;
392              
393 0           return pop( @{ $self->_active_task->_queue } );
  0            
394             }
395              
396             sub _get_next_state_wrapper
397             {
398 0     0     my ($self) = @_;
399              
400 0           my $positions = $self->_positions;
401              
402 0           while ( my $state = $self->_get_next_state )
403             {
404 0           my $rec = $positions->{$state};
405 0           $self->_active_record($rec);
406 0 0         return $state if $rec->[2];
407             }
408 0           return;
409             }
410              
411             sub _process_pending_items
412             {
413 0     0     my ( $self, $_pending, $state ) = @_;
414              
415 0           my $rec = $self->_active_record;
416 0           my $task = $self->_active_task;
417              
418 0 0         if (@$_pending)
419             {
420 0 0         $self->_shuffle( $task->_gen, $_pending ) if $task->_seed;
421 0           push @{ $task->_queue }, map { $_->[0] } @$_pending;
  0            
  0            
422 0           $rec->[3] += ( scalar grep { !$_->[1] } @$_pending );
  0            
423             }
424             else
425             {
426 0           my $parent = $state;
427 0           my $parent_rec = $rec;
428 0           my $positions = $self->_positions;
429              
430             PARENT:
431 0   0       while ( ( !$parent_rec->[3] ) or ( ! --$parent_rec->[3] ) )
432             {
433 0           $parent_rec->[2] = 0;
434 0           $parent = $parent_rec->[0];
435 0 0         last PARENT if not defined $parent;
436 0           $parent_rec = $positions->{$parent};
437             }
438             }
439 0 0         if ( not --$task->{_remaining_iters} )
440             {
441 0           return $self->_next_task;
442             }
443 0           return 1;
444             }
445              
446             sub _find_moves
447             {
448 0     0     my ( $self, $_pending, $state, $no_cards ) = @_;
449 0           my $board_values = $self->_board_values;
450 0           my $fnd = vec( $state, 0, 8 );
451 0           my $positions = $self->_positions;
452 0           my $_is_good_diff = $self->_is_good_diff;
453 0           foreach my $col_idx ( keys @$board_values )
454             {
455 0           my $pos = vec( $state, 4 + $col_idx, 4 );
456              
457 0 0         if ($pos)
458             {
459 0           $$no_cards = 0;
460              
461 0           my $card = $board_values->[$col_idx][ $pos - 1 ];
462 0 0         if ( exists( $_is_good_diff->{ $card - $fnd } ) )
463             {
464 0           my $next_s = $state;
465 0           vec( $next_s, 0, 8 ) = $card;
466 0           --vec( $next_s, 4 + $col_idx, 4 );
467 0           my $exists = exists( $positions->{$next_s} );
468 0           my $to_add = 0;
469 0 0         if ( !$exists )
    0          
470             {
471 0           $positions->{$next_s} = [ $state, $col_idx, 1, 0 ];
472 0           $to_add = 1;
473             }
474             elsif ( $positions->{$next_s}->[2] )
475             {
476 0           $to_add = 1;
477             }
478 0 0         if ($to_add)
479             {
480 0           push( @$_pending, [ $next_s, $exists ] );
481             }
482             }
483             }
484             }
485              
486 0           return;
487             }
488              
489             sub _set_up_solver
490             {
491 0     0     my ( $self, $talon_ptr, $diffs ) = @_;
492              
493 0           $self->_parse_board;
494 0           $self->_set_up_initial_position($talon_ptr);
495 0           $self->_set_up_tasks;
496 0           $self->_is_good_diff( +{ map { $_ => 1 } map { $_, -$_ } @$diffs, } );
  0            
  0            
497              
498 0           return;
499             }
500              
501             package Games::Solitaire::BlackHole::Solver::App::Base::Task;
502             $Games::Solitaire::BlackHole::Solver::App::Base::Task::VERSION = '0.4.0';
503 1     1   10 use Moo;
  1         3  
  1         8  
504              
505             has [ '_queue', '_gen', '_task_idx', '_name', '_remaining_iters', '_seed', ] =>
506             ( is => 'rw' );
507              
508             package Games::Solitaire::BlackHole::Solver::App::Base::PreludeItem;
509             $Games::Solitaire::BlackHole::Solver::App::Base::PreludeItem::VERSION = '0.4.0';
510 1     1   469 use Moo;
  1         3  
  1         4  
511              
512             has [ '_quota', '_task', '_task_idx', '_task_name', ] => ( is => 'rw' );
513              
514             1;
515              
516             __END__