File Coverage

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


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.1';
3 1     1   631 use Moo;
  1         3  
  1         5  
4 1     1   1143 use Getopt::Long qw/ GetOptions /;
  1         13285  
  1         6  
5 1     1   807 use Pod::Usage qw/ pod2usage /;
  1         52450  
  1         99  
6 1     1   605 use Math::Random::MT ();
  1         1114  
  1         37  
7 1     1   8 use List::Util 1.34 qw/ any /;
  1         21  
  1         3193  
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              
117             sub _parse_board
118             {
119 0     0     my ($self) = @_;
120 0           my $lines = $self->_board_lines;
121              
122 0           my $found_line = shift(@$lines);
123              
124 0           my $init_foundation;
125 0 0         if ( my ($card) = $found_line =~ m{\AFoundations: ($card_re)\z} )
126             {
127 0           $init_foundation = $self->_get_rank($card);
128             }
129             else
130             {
131 0           die "Could not match first foundation line!";
132             }
133 0           $self->_init_foundation($init_foundation);
134              
135 0           $self->_board_cards( [ map { [ split /\s+/, $_ ] } @$lines ] );
  0            
136             $self->_board_values(
137             [
138             map {
139 0           [ map { $self->_get_rank($_) } @$_ ]
  0            
140 0           } @{ $self->_board_cards }
  0            
141             ]
142             );
143 0           return;
144             }
145              
146             sub _set_up_initial_position
147             {
148 0     0     my ( $self, $talon_ptr ) = @_;
149              
150 0           my $init_state = "";
151              
152 0           vec( $init_state, 0, 8 ) = $self->_init_foundation;
153 0           vec( $init_state, 1, 8 ) = $talon_ptr;
154              
155 0           my $board_values = $self->_board_values;
156 0           foreach my $col_idx ( keys @$board_values )
157             {
158             vec( $init_state, 4 + $col_idx, 4 ) =
159 0           scalar( @{ $board_values->[$col_idx] } );
  0            
160             }
161              
162             # The values of $positions is an array reference with the 0th key being the
163             # previous state, and the 1th key being the column of the move.
164 0           $self->_positions( +{ $init_state => [ undef, undef, 1, 0, ], } );
165              
166 0           $self->_init_queue( [$init_state] );
167              
168 0           return;
169             }
170              
171             sub _shuffle
172             {
173 0     0     my ( $self, $gen, $arr ) = @_;
174              
175 0           my $i = $#$arr;
176 0           while ( $i > 0 )
177             {
178 0           my $j = int( $gen->rand( $i + 1 ) );
179 0 0         if ( $i != $j )
180             {
181 0           @$arr[ $i, $j ] = @$arr[ $j, $i ];
182             }
183 0           --$i;
184             }
185 0           return;
186             }
187              
188             my $TASK_NAME_RE = qr/[A-Za-z0-9_]+/;
189             my $TASK_ALLOC_RE = qr/[0-9]+\@$TASK_NAME_RE/;
190              
191             sub _process_cmd_line
192             {
193 0     0     my ( $self, $args ) = @_;
194              
195 0           my $quiet = '';
196 0           my $output_fn;
197 0           my ( $help, $man, $version );
198 0           my @tasks;
199              
200             my $push_task = sub {
201 0     0     push @tasks,
202             +{
203             name => undef(),
204             seed => 0,
205             };
206 0           return;
207 0           };
208 0           $push_task->();
209             GetOptions(
210             "o|output=s" => \$output_fn,
211             "quiet!" => \$quiet,
212             "next-task" => sub {
213 0     0     $push_task->();
214 0           return;
215             },
216             "prelude=s" => sub {
217 0     0     my ( undef, $val ) = @_;
218 0 0         if ( $val !~ /\A$TASK_ALLOC_RE(?:,$TASK_ALLOC_RE)*\z/ )
219             {
220 0           die "Invalid prelude string '$val' !";
221             }
222 0           $self->_prelude_string($val);
223 0           return;
224             },
225             "task-name=s" => sub {
226 0     0     my ( undef, $val ) = @_;
227 0 0         if ( $val !~ /\A$TASK_NAME_RE\z/ )
228             {
229 0           die "Invalid task name '$val' - must be alphanumeric!";
230             }
231 0           $tasks[-1]->{name} = $val;
232 0           return;
233             },
234             "seed=i" => sub {
235 0     0     my ( undef, $val ) = @_;
236 0           $tasks[-1]->{seed} = $val;
237 0           return;
238             },
239             'help|h|?' => \$help,
240             'man' => \$man,
241             'version' => \$version,
242 0 0         %{ $args->{extra_flags} },
  0            
243             ) or pod2usage(2);
244 0 0         if ( @tasks == 1 )
245             {
246 0           $tasks[-1]{name} = 'default';
247             }
248 0 0   0     if ( any { !defined $_->{name} } @tasks )
  0            
249             {
250 0           die "You did not specify the task-names for some tasks";
251             }
252 0           $self->_init_tasks_configs( \@tasks );
253              
254 0 0         pod2usage(1) if $help;
255 0 0         pod2usage( -exitstatus => 0, -verbose => 2 ) if $man;
256              
257 0 0         if ($version)
258             {
259 0           print
260             "black-hole-solve version $Games::Solitaire::BlackHole::Solver::App::Base::VERSION\n";
261 0           exit(0);
262             }
263              
264 0           $self->_quiet($quiet);
265 0           my $output_handle;
266              
267 0 0         if ( defined($output_fn) )
268             {
269 0 0         open( $output_handle, ">", $output_fn )
270             or die "Could not open '$output_fn' for writing";
271             }
272             else
273             {
274 0           open( $output_handle, ">&STDOUT" );
275             }
276 0           $self->_output_fn($output_fn);
277 0           $self->_output_handle($output_handle);
278 0           $self->_calc_lines( shift(@ARGV) );
279              
280 0           return;
281             }
282              
283             sub _set_up_tasks
284             {
285 0     0     my ($self) = @_;
286              
287 0           my @tasks;
288             my %tasks_by_names;
289 0           foreach my $task_rec ( @{ $self->_init_tasks_configs } )
  0            
290             {
291 0           my $iseed = $task_rec->{seed};
292 0           my $name = $task_rec->{name};
293 0           my $_task_idx = @tasks;
294             my $task_obj =
295             Games::Solitaire::BlackHole::Solver::App::Base::Task->new(
296             {
297             _name => $name,
298 0   0       _queue => [ @{ $self->_init_queue } ],
  0            
299             _seed => $iseed,
300             _gen => Math::Random::MT->new( $iseed || 1 ),
301             _remaining_iters => 100,
302             _task_idx => $_task_idx,
303             }
304             );
305 0           push @tasks, $task_obj;
306 0 0         if ( exists $tasks_by_names{$name} )
307             {
308 0           die "Duplicate task-name '$name'!";
309             }
310 0           $tasks_by_names{$name} = $task_obj;
311             }
312 0           $self->_task_idx(0);
313 0           $self->_tasks( \@tasks );
314 0           $self->_tasks_by_names( \%tasks_by_names );
315 0           my @prelude;
316             my $process_item = sub {
317 0     0     my $s = shift;
318 0 0         if ( my ( $quota, $name ) = $s =~ /\A([0-9]+)\@($TASK_NAME_RE)\z/ )
319             {
320 0 0         if ( not exists $self->_tasks_by_names->{$name} )
321             {
322 0           die "Unknown task name $name in prelude!";
323             }
324 0           my $task_obj = $self->_tasks_by_names->{$name};
325 0           return Games::Solitaire::BlackHole::Solver::App::Base::PreludeItem
326             ->new(
327             {
328             _quota => $quota,
329             _task => $task_obj,
330             _task_idx => $task_obj->_task_idx,
331             _task_name => $task_obj->_name,
332             }
333             );
334             }
335             else
336             {
337 0           die "foo";
338             }
339 0           };
340 0 0         if ( my $_prelude_string = $self->_prelude_string )
341             {
342             push @prelude,
343             (
344 0           map { $process_item->($_) }
  0            
345             split /,/, $_prelude_string
346             );
347             }
348 0           $self->_prelude( \@prelude );
349 0           $self->_prelude_iter(0);
350 0 0         if ( @{ $self->_prelude } )
  0            
351             {
352 0           $self->_next_task;
353             }
354 0           return;
355             }
356              
357             sub _next_task
358             {
359 0     0     my ($self) = @_;
360 0 0         if ( $self->_prelude_iter < @{ $self->_prelude } )
  0            
361             {
362 0           my $alloc = $self->_prelude->[ $self->{_prelude_iter}++ ];
363 0           my $task = $alloc->_task;
364 0 0         if ( !@{ $task->_queue } )
  0            
365             {
366 0           return $self->_next_task;
367             }
368 0           $task->_remaining_iters( $alloc->_quota );
369 0           $self->_active_task($task);
370 0           return 1;
371             }
372 0           my $tasks = $self->_tasks;
373 0 0         return if !@$tasks;
374 0 0         if ( !@{ $tasks->[ $self->_task_idx ]->_queue } )
  0            
375             {
376 0           splice @$tasks, $self->_task_idx, 1;
377 0           return $self->_next_task;
378             }
379 0           my $task = $tasks->[ $self->_task_idx ];
380 0           $self->_task_idx( ( $self->_task_idx + 1 ) % @$tasks );
381 0           $task->_remaining_iters(100);
382 0           $self->_active_task($task);
383              
384 0           return 1;
385             }
386              
387             sub _get_next_state
388             {
389 0     0     my ($self) = @_;
390              
391 0           return pop( @{ $self->_active_task->_queue } );
  0            
392             }
393              
394             sub _get_next_state_wrapper
395             {
396 0     0     my ($self) = @_;
397              
398 0           my $positions = $self->_positions;
399              
400 0           while ( my $state = $self->_get_next_state )
401             {
402 0           my $rec = $positions->{$state};
403 0           $self->_active_record($rec);
404 0 0         return $state if $rec->[2];
405             }
406 0           return;
407             }
408              
409             sub _process_pending_items
410             {
411 0     0     my ( $self, $_pending, $state ) = @_;
412              
413 0           my $rec = $self->_active_record;
414 0           my $task = $self->_active_task;
415              
416 0 0         if (@$_pending)
417             {
418 0 0         $self->_shuffle( $task->_gen, $_pending ) if $task->_seed;
419 0           push @{ $task->_queue }, map { $_->[0] } @$_pending;
  0            
  0            
420 0           $rec->[3] += ( scalar grep { !$_->[1] } @$_pending );
  0            
421             }
422             else
423             {
424 0           my $parent = $state;
425 0           my $parent_rec = $rec;
426 0           my $positions = $self->_positions;
427              
428             PARENT:
429 0   0       while ( ( !$parent_rec->[3] ) or ( ! --$parent_rec->[3] ) )
430             {
431 0           $parent_rec->[2] = 0;
432 0           $parent = $parent_rec->[0];
433 0 0         last PARENT if not defined $parent;
434 0           $parent_rec = $positions->{$parent};
435             }
436             }
437 0 0         if ( not --$task->{_remaining_iters} )
438             {
439 0           return $self->_next_task;
440             }
441 0           return 1;
442             }
443              
444             sub _find_moves
445             {
446 0     0     my ( $self, $_pending, $state, $no_cards ) = @_;
447 0           my $board_values = $self->_board_values;
448 0           my $fnd = vec( $state, 0, 8 );
449 0           my $positions = $self->_positions;
450 0           my $_is_good_diff = $self->_is_good_diff;
451 0           foreach my $col_idx ( keys @$board_values )
452             {
453 0           my $pos = vec( $state, 4 + $col_idx, 4 );
454              
455 0 0         if ($pos)
456             {
457 0           $$no_cards = 0;
458              
459 0           my $card = $board_values->[$col_idx][ $pos - 1 ];
460 0 0         if ( exists( $_is_good_diff->{ $card - $fnd } ) )
461             {
462 0           my $next_s = $state;
463 0           vec( $next_s, 0, 8 ) = $card;
464 0           --vec( $next_s, 4 + $col_idx, 4 );
465 0           my $exists = exists( $positions->{$next_s} );
466 0           my $to_add = 0;
467 0 0         if ( !$exists )
    0          
468             {
469 0           $positions->{$next_s} = [ $state, $col_idx, 1, 0 ];
470 0           $to_add = 1;
471             }
472             elsif ( $positions->{$next_s}->[2] )
473             {
474 0           $to_add = 1;
475             }
476 0 0         if ($to_add)
477             {
478 0           push( @$_pending, [ $next_s, $exists ] );
479             }
480             }
481             }
482             }
483              
484 0           return;
485             }
486              
487             sub _set_up_solver
488             {
489 0     0     my ( $self, $talon_ptr, $diffs ) = @_;
490              
491 0           $self->_parse_board;
492 0           $self->_set_up_initial_position($talon_ptr);
493 0           $self->_set_up_tasks;
494 0           $self->_is_good_diff( +{ map { $_ => 1 } map { $_, -$_ } @$diffs, } );
  0            
  0            
495              
496 0           return;
497             }
498              
499             package Games::Solitaire::BlackHole::Solver::App::Base::Task;
500             $Games::Solitaire::BlackHole::Solver::App::Base::Task::VERSION = '0.4.1';
501 1     1   10 use Moo;
  1         2  
  1         9  
502              
503             has [ '_queue', '_gen', '_task_idx', '_name', '_remaining_iters', '_seed', ] =>
504             ( is => 'rw' );
505              
506             package Games::Solitaire::BlackHole::Solver::App::Base::PreludeItem;
507             $Games::Solitaire::BlackHole::Solver::App::Base::PreludeItem::VERSION = '0.4.1';
508 1     1   485 use Moo;
  1         2  
  1         4  
509              
510             has [ '_quota', '_task', '_task_idx', '_task_name', ] => ( is => 'rw' );
511              
512             1;
513              
514             __END__