File Coverage

blib/lib/Games/Solitaire/Verify/State.pm
Criterion Covered Total %
statement 284 296 95.9
branch 75 86 87.2
condition n/a
subroutine 54 55 98.1
pod 18 18 100.0
total 431 455 94.7


line stmt bran cond sub pod time code
1             package Games::Solitaire::Verify::State;
2             $Games::Solitaire::Verify::State::VERSION = '0.2401';
3 8     8   237555 use warnings;
  8         46  
  8         272  
4 8     8   43 use strict;
  8         15  
  8         204  
5              
6              
7 8     8   992 use parent 'Games::Solitaire::Verify::Base';
  8         605  
  8         48  
8              
9 8     8   1866 use Games::Solitaire::Verify::Exception ();
  8         31  
  8         149  
10 8     8   1643 use Games::Solitaire::Verify::Card ();
  8         19  
  8         178  
11 8     8   1575 use Games::Solitaire::Verify::Column ();
  8         19  
  8         167  
12 8     8   1381 use Games::Solitaire::Verify::Move ();
  8         18  
  8         165  
13 8     8   3825 use Games::Solitaire::Verify::Freecells ();
  8         24  
  8         192  
14 8     8   3718 use Games::Solitaire::Verify::Foundations ();
  8         19  
  8         221  
15 8     8   2398 use Games::Solitaire::Verify::VariantParams ();
  8         21  
  8         181  
16 8     8   2432 use Games::Solitaire::Verify::VariantsMap ();
  8         24  
  8         209  
17              
18 8     8   52 use List::Util qw(first);
  8         15  
  8         515  
19 8     8   3352 use POSIX qw();
  8         34845  
  8         27884  
20              
21             __PACKAGE__->mk_acc_ref(
22             [
23             qw(
24             _columns
25             _freecells
26             _foundations
27             _variant
28             _variant_params
29             _temp_move
30             )
31             ]
32             );
33              
34              
35             sub set_freecells
36             {
37 274     274 1 562 my ( $self, $freecells ) = @_;
38              
39 274         583 $self->_freecells($freecells);
40              
41 274         480 return;
42             }
43              
44             sub _assign_freecells_from_string
45             {
46 272     272   470 my $self = shift;
47 272         671 my $string = shift;
48              
49 272         657 $self->set_freecells(
50             Games::Solitaire::Verify::Freecells->new(
51             {
52             count => $self->num_freecells(),
53             string => $string,
54             }
55             )
56             );
57              
58 272         623 return;
59             }
60              
61              
62             sub add_column
63             {
64 2246     2246 1 4092 my ( $self, $col ) = @_;
65              
66 2246         3285 push @{ $self->_columns() }, $col;
  2246         5150  
67              
68 2246         5635 return;
69             }
70              
71              
72             sub set_foundations
73             {
74 281     281 1 536 my ( $self, $foundations ) = @_;
75              
76 281         605 $self->_foundations($foundations);
77              
78 281         467 return;
79             }
80              
81             sub _get_suits_seq
82             {
83 0     0   0 my $class = shift;
84              
85 0         0 return Games::Solitaire::Verify::Card->get_suits_seq();
86             }
87              
88             sub _from_string
89             {
90 266     266   568 my ( $self, $str ) = @_;
91              
92 266         497 my $rank_re = '[0A1-9TJQK]';
93              
94 266 50       1452 if ( $str !~ m{\A(Foundations:[^\n]*)\n}gms )
95             {
96 0         0 Games::Solitaire::Verify::Exception::Parse::State::Foundations->throw(
97             error => "Wrong Foundations", );
98             }
99 266         830 my $founds_s = $1;
100              
101 266         682 $self->set_foundations(
102             Games::Solitaire::Verify::Foundations->new(
103             {
104             num_decks => $self->num_decks(),
105             string => $founds_s,
106             }
107             )
108             );
109              
110 266 50       1392 if ( $str !~ m{\G(Freecells:[^\n]*)\n}gms )
111             {
112 0         0 Games::Solitaire::Verify::Exception::Parse::State::Freecells->throw(
113             error => "Wrong Freecell String", );
114             }
115 266         805 $self->_assign_freecells_from_string($1);
116              
117 266         620 foreach my $col_idx ( 0 .. ( $self->num_columns() - 1 ) )
118             {
119 2128 100       9908 if ( $str !~ m{\G(:[^\n]*)\n}msg )
120             {
121 2         27 Games::Solitaire::Verify::Exception::Parse::State::Column->throw(
122             error => "Cannot parse column",
123             index => $col_idx,
124             );
125             }
126 2126         5495 my $column_str = $1;
127              
128 2126         6753 $self->add_column(
129             Games::Solitaire::Verify::Column->new(
130             {
131             string => $column_str,
132             }
133             )
134             );
135             }
136              
137 264         804 return;
138             }
139              
140             sub _fill_non_custom_variant
141             {
142 272     272   463 my $self = shift;
143 272         415 my $variant = shift;
144              
145 272         766 my $variants_map = Games::Solitaire::Verify::VariantsMap->new();
146              
147 272         658 my $params = $variants_map->get_variant_by_id($variant);
148              
149 272 50       659 if ( !defined($params) )
150             {
151 0         0 Games::Solitaire::Verify::Exception::Variant::Unknown->throw(
152             error => "Unknown/Unsupported Variant",
153             variant => $variant,
154             );
155             }
156 272         1002 $self->_variant_params($params);
157 272         641 $self->_variant($variant);
158              
159 272         661 return;
160             }
161              
162             sub _set_variant
163             {
164 281     281   470 my $self = shift;
165 281         418 my $args = shift;
166              
167 281         498 my $variant = $args->{variant};
168              
169 281 100       593 if ( $variant eq "custom" )
170             {
171 9         42 $self->_variant($variant);
172 9         34 $self->_variant_params( $args->{variant_params} );
173             }
174             else
175             {
176 272         546 $self->_fill_non_custom_variant($variant);
177             }
178              
179 281         528 return;
180             }
181              
182             sub _init
183             {
184 281     281   583 my ( $self, $args ) = @_;
185              
186             # Set the variant
187 281         836 $self->_set_variant($args);
188              
189 281         651 $self->_columns( [] );
190              
191 281 100       711 if ( exists( $args->{string} ) )
192             {
193 272         704 return $self->_from_string( $args->{string} );
194             }
195             }
196              
197              
198             sub get_freecell
199             {
200 1939     1939 1 3444 my ( $self, $index ) = @_;
201              
202 1939         5110 return $self->_freecells()->cell($index);
203             }
204              
205              
206             sub set_freecell
207             {
208 590     590 1 1198 my ( $self, $index, $card ) = @_;
209              
210 590         1569 return $self->_freecells->assign( $index, $card );
211             }
212              
213              
214             sub get_foundation_value
215             {
216 866     866 1 1755 my ( $self, $suit, $idx ) = @_;
217              
218 866         2524 return $self->_foundations()->value( $suit, $idx );
219             }
220              
221              
222             sub increment_foundation_value
223             {
224 833     833 1 1703 my ( $self, $suit, $idx ) = @_;
225              
226 833         2595 $self->_foundations()->increment( $suit, $idx );
227              
228 833         1229 return;
229             }
230              
231              
232             sub num_decks
233             {
234 1066     1066 1 1904 my $self = shift;
235              
236 1066         4464 return $self->_variant_params->num_decks();
237             }
238              
239              
240             sub num_freecells
241             {
242 487     487 1 852 my $self = shift;
243              
244 487         2378 return $self->_variant_params->num_freecells();
245             }
246              
247              
248             sub num_empty_freecells
249             {
250 489     489 1 741 my $self = shift;
251              
252 489         1466 return $self->_freecells->num_empty();
253             }
254              
255              
256             sub num_columns
257             {
258 1168     1168 1 2053 my $self = shift;
259              
260 1168         4145 return $self->_variant_params->num_columns();
261             }
262              
263              
264             sub get_column
265             {
266 13203     13203 1 19516 my $self = shift;
267 13203         18631 my $index = shift;
268              
269 13203         33123 return $self->_columns->[$index];
270             }
271              
272              
273             sub num_empty_columns
274             {
275 452     452 1 746 my $self = shift;
276              
277 452         707 my $count = 0;
278              
279 452         940 foreach my $idx ( 0 .. ( $self->num_columns() - 1 ) )
280             {
281 3776 100       6162 if ( !$self->get_column($idx)->len() )
282             {
283 328         646 ++$count;
284             }
285             }
286 452         1447 return $count;
287             }
288              
289              
290             sub clone
291             {
292 7     7 1 35 my $self = shift;
293              
294 7         15 my $variant = $self->_variant;
295 7 100       34 my $copy = Games::Solitaire::Verify::State->new(
296             {
297             variant => $variant,
298             (
299             ( $variant eq "custom" )
300             ? ( variant_params => $self->_variant_params() )
301             : ()
302             ),
303             }
304             );
305              
306 7         22 foreach my $idx ( 0 .. ( $self->num_columns() - 1 ) )
307             {
308 56         116 $copy->add_column( $self->get_column($idx)->clone() );
309             }
310              
311 7         26 $copy->set_foundations( $self->_foundations()->clone() );
312              
313 7         27 $copy->_freecells( $self->_freecells()->clone() );
314              
315 7         23 return $copy;
316             }
317              
318              
319             # Returns 0 on success, else the error.
320             sub _can_put_into_empty_column
321             {
322 225     225   456 my ( $self, $card ) = @_;
323              
324 225 100       1843 if ( $self->_variant_params->empty_stacks_filled_by() eq "kings" )
325             {
326 13 100       46 if ( $card->rank() != 13 )
327             {
328             return
329 2         48 Games::Solitaire::Verify::Exception::Move::Dest::Col::OnlyKingsCanFillEmpty
330             ->new(
331             error => "Non-king on an empty stack",
332             move => $self->_temp_move(),
333             );
334             }
335             }
336 223         624 return 0;
337             }
338              
339             sub _is_matching_color
340             {
341 1422     1422   2438 my ( $self, $parent, $child ) = @_;
342              
343 1422         2917 my $rules = $self->_variant_params()->rules();
344 1422         2825 my $sbb = $self->_variant_params()->seq_build_by();
345              
346 1422 50       4206 my $verdict = (
    100          
    100          
347             ( $rules eq "simple_simon" ) ? 0
348             : ( $sbb eq "alt_color" ) ? ( $parent->color() eq $child->color() )
349             : ( $sbb eq "suit" ) ? ( $parent->suit() ne $child->suit() )
350             : 0
351             );
352              
353 1422 100       3010 if ($verdict)
354             {
355             return
356 3         62 Games::Solitaire::Verify::Exception::Move::Dest::Col::NonMatchSuits
357             ->new(
358             seq_build_by => $sbb,
359             move => $self->_temp_move(),
360             );
361             }
362              
363 1419         3141 return 0;
364             }
365              
366             sub _can_put_on_top
367             {
368 1425     1425   2737 my ( $self, $parent, $child ) = @_;
369              
370 1425 100       4287 if ( $parent->rank() != $child->rank() + 1 )
371             {
372             return
373 3         68 Games::Solitaire::Verify::Exception::Move::Dest::Col::RankMismatch
374             ->new(
375             error => "Rank mismatch between parent and child cards",
376             move => $self->_temp_move(),
377             );
378             }
379              
380 1422 100       2790 if ( my $ret = $self->_is_matching_color( $parent, $child ) )
381             {
382 3         3189 return $ret;
383             }
384              
385 1419         3313 return 0;
386             }
387              
388             sub _can_put_on_column
389             {
390 1088     1088   2140 my ( $self, $col_idx, $card ) = @_;
391              
392             return (
393 1088 100       2152 ( $self->get_column($col_idx)->len() == 0 )
394             ? $self->_can_put_into_empty_column($card)
395             : $self->_can_put_on_top( $self->get_column($col_idx)->top(), $card )
396             );
397             }
398              
399             sub _calc_freecell_max_seq_move
400             {
401 451     451   787 my ( $self, $args ) = @_;
402 451 50       1069 my $to_empty = ( defined( $args->{to_empty} ) ? $args->{to_empty} : 0 );
403              
404 451         968 return ( ( $self->num_empty_freecells() + 1 )
405             << ( $self->num_empty_columns() - $to_empty ) );
406             }
407              
408             sub _calc_empty_stacks_filled_by_any_card_max_seq_move
409             {
410 451     451   857 my ( $self, $args ) = @_;
411              
412 451         943 return $self->_calc_freecell_max_seq_move($args);
413             }
414              
415             sub _calc_max_sequence_move
416             {
417 615     615   1312 my ( $self, $args ) = @_;
418              
419 615 100       2568 return +( $self->_variant_params->sequence_move() eq "unlimited" )
    100          
420             ? POSIX::INT_MAX()
421             : ( $self->_variant_params->empty_stacks_filled_by() eq "any" )
422             ? $self->_calc_empty_stacks_filled_by_any_card_max_seq_move($args)
423             : ( $self->num_empty_freecells() + 1 );
424             }
425              
426             sub _is_sequence_in_column
427             {
428 625     625   1362 my ( $self, $source_idx, $num_cards, $num_seq_components_ref ) = @_;
429              
430 625         1177 my $col = $self->get_column($source_idx);
431 625         1414 my $len = $col->len();
432              
433 625         1561 my $rules = $self->_variant_params()->rules();
434              
435 625         958 my $num_comps = 1;
436              
437 625         1652 foreach my $card_idx ( 0 .. ( $num_cards - 2 ) )
438             {
439 562         1337 my $parent = $col->pos( $len - 1 - $card_idx - 1 );
440 562         1334 my $child = $col->pos( $len - 1 - $card_idx );
441              
442 562 100       1070 if ( $self->_can_put_on_top( $parent, $child ) )
443             {
444             return
445 1         881 Games::Solitaire::Verify::Exception::Move::Src::Col::NonSequence
446             ->new(
447             move => $self->_temp_move(),
448             pos => $card_idx,
449             );
450             }
451              
452             $num_comps += (
453 561 100       1412 ( $rules eq "simple_simon" )
    100          
454             ? ( ( $parent->suit() ne $child->suit() ) ? 1 : 0 )
455             : 1
456             );
457             }
458              
459 624         978 ${$num_seq_components_ref} = $num_comps;
  624         1109  
460              
461 624         1595 return 0;
462             }
463              
464              
465             sub clear_freecell
466             {
467 578     578 1 1163 my ( $self, $index ) = @_;
468              
469 578         1686 return $self->_freecells->clear($index);
470             }
471              
472             sub verify_and_perform_move
473             {
474 2458     2458 1 16620 my ( $self, $move ) = @_;
475              
476 2458 50       11701 if ( my $method =
477             $self->can( "_mv_" . $move->source_type . "_to_" . $move->dest_type ) )
478             {
479 2458         5352 $self->_temp_move($move);
480 2458         4714 my $ret = $method->($self);
481 2458         13126 $self->_temp_move( undef() );
482 2458         7760 return $ret;
483             }
484             else
485             {
486 0         0 die "Cannot handle this move type";
487             }
488             }
489              
490             sub _mv_stack_to_foundation
491             {
492 657     657   1040 my $self = shift;
493              
494 657         1100 my $move = $self->_temp_move();
495              
496 657         1161 my $col_idx = $move->source();
497 657         1304 my $card = $self->get_column($col_idx)->top();
498              
499 657         1388 my $rank = $card->rank();
500 657         1169 my $suit = $card->suit();
501              
502 657     657   1539 my $f_idx = first { $self->get_foundation_value( $suit, $_ ) == $rank - 1 }
503 657         3171 ( 0 .. ( $self->num_decks() - 1 ) );
504              
505 657 100       2369 if ( defined($f_idx) )
506             {
507 656         1325 $self->get_column($col_idx)->pop();
508 656         1742 $self->increment_foundation_value( $suit, $f_idx );
509 656         2045 return 0;
510             }
511             else
512             {
513             return
514 1         10 Games::Solitaire::Verify::Exception::Move::Dest::Foundation->new(
515             move => $move );
516             }
517             }
518              
519             sub _mv_stack_seq_to_foundation
520             {
521 7     7   19 my $self = shift;
522              
523 7         27 my $move = $self->_temp_move();
524              
525 7         25 my $rules = $self->_variant_params()->rules();
526              
527 7 100       32 if ( $rules ne "simple_simon" )
528             {
529 1         19 return Games::Solitaire::Verify::Exception::Move::Variant::Unsupported
530             ->new( move => $move );
531             }
532              
533 6         18 my $col_idx = $move->source();
534              
535 6         11 my $num_seq_components;
536 6         46 my $verdict =
537             $self->_is_sequence_in_column( $col_idx, 13, \$num_seq_components, );
538              
539 6 50       19 if ($verdict)
540             {
541 0         0 return $verdict;
542             }
543              
544 6 100       22 if ( $num_seq_components != 1 )
545             {
546 1         31 return Games::Solitaire::Verify::Exception::Move::Src::Col::NotTrueSeq
547             ->new( move => $move );
548             }
549              
550 5         13 my $card = $self->get_column($col_idx)->top();
551              
552 5         14 my $suit = $card->suit();
553              
554 5     5   19 my $f_idx = first { $self->get_foundation_value( $suit, $_ ) == 0 }
555 5         36 ( 0 .. ( $self->num_decks() - 1 ) );
556              
557 5 50       25 if ( defined($f_idx) )
558             {
559 5         16 foreach my $card_idx ( 1 .. 13 )
560             {
561 65         109 $self->get_column($col_idx)->pop();
562 65         126 $self->increment_foundation_value( $suit, $f_idx );
563             }
564 5         19 return 0;
565             }
566             else
567             {
568             return
569 0         0 Games::Solitaire::Verify::Exception::Move::Dest::Foundation->new(
570             move => $move );
571             }
572             }
573              
574             sub _mv_stack_to_freecell
575             {
576 589     589   940 my $self = shift;
577 589         1004 my $move = $self->_temp_move();
578              
579 589         1066 my $col_idx = $move->source();
580 589         1017 my $fc_idx = $move->dest();
581              
582 589 100       1318 if ( !$self->get_column($col_idx)->len() )
583             {
584             return
585 1         37 Games::Solitaire::Verify::Exception::Move::Src::Col::NoCards->new(
586             move => $move, );
587             }
588              
589 588 100       1393 if ( defined( $self->get_freecell($fc_idx) ) )
590             {
591 1         28 return Games::Solitaire::Verify::Exception::Move::Dest::Freecell->new(
592             move => $move, );
593             }
594              
595 587         1286 $self->set_freecell( $fc_idx, $self->get_column($col_idx)->pop() );
596              
597 587         1127 return 0;
598             }
599              
600             sub _mv_stack_to_stack
601             {
602 620     620   989 my $self = shift;
603 620         1086 my $move = $self->_temp_move();
604              
605 620         1184 my $source = $move->source();
606 620         1027 my $dest = $move->dest();
607 620         1121 my $num_cards = $move->num_cards();
608              
609             # Source column
610 620         1258 my $sc = $self->get_column($source);
611 620         1353 my $dc = $self->get_column($dest);
612              
613 620         1648 my $source_len = $sc->len();
614              
615 620 100       1541 if ( $source_len < $num_cards )
616             {
617             return
618 1         20 Games::Solitaire::Verify::Exception::Move::Src::Col::NotEnoughCards
619             ->new( move => $move, );
620             }
621              
622 619         922 my $num_seq_components;
623 619 100       1454 if (
624             my $verdict = $self->_is_sequence_in_column(
625             $source, $num_cards, \$num_seq_components,
626             )
627             )
628             {
629 1         747 return $verdict;
630             }
631              
632 618 100       1642 if (
633             my $verdict = $self->_can_put_on_column(
634             $dest, $sc->pos( $source_len - $num_cards )
635             )
636             )
637             {
638 3         2194 return $verdict;
639             }
640              
641             # Now let's see if we have enough resources
642             # to move the cards.
643              
644 615 100       1582 if (
645             $num_seq_components > $self->_calc_max_sequence_move(
646             {
647             to_empty => ( $dc->len() == 0 ),
648             }
649             )
650             )
651             {
652             return
653 3         65 Games::Solitaire::Verify::Exception::Move::NotEnoughEmpties->new(
654             move => $move, );
655             }
656              
657             # Now let's actually move them.
658 612         1966 $dc->append_cards( $sc->popN($num_cards) );
659              
660 612         1390 return 0;
661             }
662              
663             sub _mv_freecell_to_foundation
664             {
665 114     114   208 my $self = shift;
666 114         202 my $move = $self->_temp_move();
667              
668 114         211 my $fc_idx = $move->source();
669 114         272 my $card = $self->get_freecell($fc_idx);
670              
671 114 100       296 if ( !defined($card) )
672             {
673             return
674 1         5 Games::Solitaire::Verify::Exception::Move::Src::Freecell::Empty
675             ->new( move => $move, );
676             }
677              
678 113         236 my $rank = $card->rank();
679 113         232 my $suit = $card->suit();
680              
681 113     113   275 my $f_idx = first { $self->get_foundation_value( $suit, $_ ) == $rank - 1 }
682 113         576 ( 0 .. ( $self->num_decks() - 1 ) );
683              
684 113 100       431 if ( defined($f_idx) )
685             {
686 112         319 $self->clear_freecell($fc_idx);
687 112         297 $self->increment_foundation_value( $suit, $f_idx );
688 112         354 return 0;
689             }
690             else
691             {
692             return
693 1         28 Games::Solitaire::Verify::Exception::Move::Dest::Foundation->new(
694             move => $move );
695             }
696             }
697              
698             sub _mv_freecell_to_stack
699             {
700 471     471   799 my $self = shift;
701 471         846 my $move = $self->_temp_move();
702              
703 471         875 my $fc_idx = $move->source();
704 471         901 my $col_idx = $move->dest();
705              
706 471         1098 my $card = $self->get_freecell($fc_idx);
707              
708 471 100       1116 if ( !defined($card) )
709             {
710 1         25 return Games::Solitaire::Verify::Exception::Move::Src::Freecell::Empty
711             ->new( move => $move, );
712             }
713              
714 470 100       1102 if ( my $verdict = $self->_can_put_on_column( $col_idx, $card ) )
715             {
716 4         1488 return $verdict;
717             }
718              
719 466         1006 $self->get_column($col_idx)->push($card);
720 466         1346 $self->clear_freecell($fc_idx);
721              
722 466         865 return 0;
723             }
724              
725              
726             my @SS = ( @{ Games::Solitaire::Verify::Card->get_suits_seq() } );
727              
728             sub verify_contents
729             {
730 16     16 1 40 my ( $self, $args ) = @_;
731              
732 16         36 my $MAX_RANK = $args->{max_rank};
733 16         32 my $found = {};
734             my $register = sub {
735 832     832   1196 my $card = shift;
736 832 50       1619 if ( $card->rank > $MAX_RANK )
737             {
738 0         0 die Games::Solitaire::Verify::Exception::State::TooHighRank->new(
739             cards => [$card], );
740             }
741 832         1485 my $s = $card->fast_s;
742 832 50       2188 if ( ( ++$found->{$s} ) > 1 )
743             {
744 0         0 die Games::Solitaire::Verify::Exception::State::ExtraCards->new(
745             cards => [$card], );
746             }
747 832         1521 return;
748 16         90 };
749 16         45 for my $fc ( 0 .. $self->num_freecells - 1 )
750             {
751 64         118 my $card = $self->get_freecell($fc);
752 64 100       151 if ( defined $card )
753             {
754 14         28 $register->($card);
755             }
756             }
757 16         53 foreach my $suit (@SS)
758             {
759 64         129 for my $rank ( 1 .. $self->get_foundation_value( $suit, 0 ) )
760             {
761 0         0 $register->(
762             Games::Solitaire::Verify::Card->new(
763             {
764             string => (
765             Games::Solitaire::Verify::Card->rank_to_string(
766             $rank)
767             . $suit
768             )
769             }
770             ),
771             );
772             }
773             }
774              
775 16         60 foreach my $idx ( 0 .. ( $self->num_columns() - 1 ) )
776             {
777 132         234 my $col = $self->get_column($idx);
778 132         282 for my $pos ( 0 .. $col->len - 1 )
779             {
780 818         1544 $register->( $col->pos($pos) );
781             }
782             }
783              
784 16 50       78 if ( scalar( keys %$found ) != $MAX_RANK * 4 )
785             {
786 0         0 die Games::Solitaire::Verify::Exception::State::MissingCards->new(
787             cards => [], );
788             }
789              
790 16         188 return;
791             }
792              
793              
794             sub to_string
795             {
796 2226     2226 1 3609 my $self = shift;
797              
798             return join(
799             "\n",
800             (
801 22606         43688 map { $_->to_string() } $self->_foundations(),
802             $self->_freecells(),
803 2226         4576 @{ $self->_columns() }
  2226         4563  
804             ),
805             ""
806             );
807             }
808              
809             1; # End of Games::Solitaire::Verify::State
810              
811             __END__