File Coverage

blib/lib/Games/Sudoku/PatternSolver/CPLogic.pm
Criterion Covered Total %
statement 195 202 96.5
branch 60 76 78.9
condition 18 21 85.7
subroutine 11 11 100.0
pod 0 7 0.0
total 284 317 89.5


line stmt bran cond sub pod time code
1             package Games::Sudoku::PatternSolver::CPLogic;
2              
3 4     4   1069 use strict;
  4         9  
  4         176  
4 4     4   19 use warnings;
  4         1964  
  4         418  
5              
6             require 5.06.0;
7              
8 4     4   26 use Bit::Vector;
  4         7  
  4         293  
9 4     4   2363 use Algorithm::Combinatorics qw(combinations);
  4         18205  
  4         11633  
10              
11             # array with 81 hashes representing the fields; field->{covers} returns Bit::Vector with constraint coverage field indexes set (row, column and block in one)
12             my $Fields = Games::Sudoku::PatternSolver::Patterns::init_fields();
13             # an array of 27 arrays, holding the 9 indexes sharing any row, column or box
14             my $Groups = Games::Sudoku::PatternSolver::Patterns::build_groups();
15             my $Group_Vectors = [map {my $v = Bit::Vector->new(81); $v->Index_List_Store(@$_); $v} @$Groups];
16              
17             # an array of 54 (9x6) arrays with 3 Bit::Vector(s) each: [intersection of box with row or column], [remainder of box], [remainder of row or column]
18             my $Intersections = Games::Sudoku::PatternSolver::Patterns::get_intersections();
19             # to be initialized with each symbol's candidates map as a Bit::Vector
20              
21             # ugly: a 2nd data structure with the candidates, that has to be created and maintained
22             # only for use by advanced logic (locked_sets, locked_candidates)
23             my %Candidate_Vectors = ();
24             my $steps_taken;
25             my $RECORD_STEPS = 0;
26              
27             sub apply_logic {
28 102     102 0 324 my ($puzzle) = @_;
29              
30 102         343 my $puzzle_string = $puzzle->{strPuzzle};
31 102         3152 $puzzle_string =~ s/[^1-9]/0/g;
32            
33 102         13402 $_->{candidates} = {}, $_->{value} = undef for @$Fields; # clear Field objects
34              
35 102         381 my $index_arrays_by_symbol = {}; # symbol -> array with all fields (offset) where the symbol is the given value
36 102         240 my $coverage_vectors_by_symbol = {}; # do we really need this? currently only for populating and deleting the candidates
37              
38 102         278 my $missing_count = 81;
39 102         283 my $field_index = -1;
40 102         1452 foreach my $symbol (split //, $puzzle_string) {
41 8262         11999 $field_index++;
42 8262 100       21088 $symbol =~ /[1-9]/ or next;
43 3493         4913 $missing_count--;
44 3493         4747 push @{$index_arrays_by_symbol->{$symbol}}, $field_index;
  3493         7437  
45 3493         7320 $Fields->[$field_index]{value} = $symbol;
46             }
47              
48 102         1266 foreach my $symbol (keys %$index_arrays_by_symbol) {
49 904         2657 my $covers = Bit::Vector->new(81);
50 904         7996 $covers->Or($covers, $Fields->[$_]{covers}) for $puzzle->{symbolVectors}{$symbol}->Index_List_Read();
51 904         2468 $coverage_vectors_by_symbol->{$symbol} = $covers;
52             }
53              
54 102         551 foreach my $symbol (1..9) {
55             # rem: $coverage_vectors_by_symbol used only here and in put_value(), would be nice to drop
56 918   66     3231 my $area_vector = $coverage_vectors_by_symbol->{$symbol} ||= Bit::Vector->new(81);
57 918         3125 my $candidate_vector = Bit::Vector->new(81);
58              
59 918         2037 foreach my $field (@$Fields) {
60 74358 100       153263 next if $field->{value};
61 42921 100       109882 unless ($area_vector->bit_test($field->{offset})) {
62 14791         28924 $field->{candidates}{$symbol} = 1;
63 14791         33965 $candidate_vector->Bit_On($field->{offset});
64             }
65             }
66 918         4976 $Candidate_Vectors{$symbol} = $candidate_vector;
67             }
68              
69 102         232 my $filled_total = 0;
70 102         213 my $dropped_total = 0;
71 102         252 my $dropping_candidates_did_help = 0;
72 102         271 $steps_taken = [];
73              
74 102         406 while ($missing_count) {
75 370         802 my $last_filled = 0;
76              
77 370         1307 $last_filled += hidden_singles($puzzle);
78 370         1506 $last_filled += naked_singles($puzzle);
79              
80 370 100 66     1472 unless ($last_filled) {
81             #### last; # skip further efforts to reduce the candidates (advanced methods)
82              
83 118         292 my $last_dropped = 0;
84              
85 118         501 $last_dropped += locked_sets($puzzle);
86 118         519 $last_dropped += locked_candidates($puzzle);
87              
88 118 100       502 $last_dropped or last;
89              
90 39         133 $dropped_total += $last_dropped;
91              
92             } elsif ($dropped_total) {
93             $dropping_candidates_did_help = 1;
94             }
95              
96 291         544 $filled_total += $last_filled;
97 291         994 $missing_count -= $last_filled;
98             }
99              
100 102         284 my $puzzle_string_2;
101 102 100       373 if ($filled_total) {
102 99 100       465 $puzzle_string_2 = join '', map {$Fields->[$_]{value} || '.'} (0..80);
  8019         23972  
103              
104             # are 8 symbols completely filled in and 1 symbol entirely missing?
105 99 50 66     1084 if ($missing_count == 9 && $puzzle->{uniqueGivens} == 8) {
106 0         0 for (1..9) {
107 0 0       0 next if $puzzle_string_2 =~ /$_/;
108              
109 0         0 my $added = $puzzle_string_2 =~ s/\./$_/g;
110 0         0 $filled_total += $added;
111 0         0 $missing_count -= $added;
112             # did not yet find a sudoku where that happend
113 0         0 warn "'$puzzle_string': Logic mode finished by adding '$_' as the last symbol.\n";
114              
115 0         0 last;
116             }
117             }
118              
119 99         467 $puzzle->{strPuzzleAfterLogic} = $puzzle_string_2;
120             }
121              
122 102         409 $puzzle->{logicFilled} = $filled_total;
123 102         450 $puzzle->{candidatesDropped} = $dropped_total;
124 102         476 $puzzle->{candidateVectors} = \%Candidate_Vectors;
125 102         377 $puzzle->{droppingCandidatesDidHelp} = $dropping_candidates_did_help;
126 102         370 $puzzle->{logicSteps} = $steps_taken;
127              
128 102 100       492 if ($missing_count) {
    100          
129 79         274 $puzzle->{logicSolved} = 0;
130              
131             } elsif ($filled_total) {
132              
133 22         55 $puzzle->{logicSolved} = 1;
134 22         88 push @{$puzzle->{solutions}}, $puzzle_string_2;
  22         100  
135 22         62 $puzzle->{solutionCount} = 1;
136              
137             } else {
138 1 50       4 print "Puzzle was no puzzle!\n" if $Games::Sudoku::PatternSolver::VERBOSE;
139             }
140              
141 102         3089 return $puzzle
142             }
143              
144             sub hidden_singles {
145 370     370 0 907 my ($puzzle) = @_;
146              
147 370 50       1114 print "hidden_singles()\n" if $Games::Sudoku::PatternSolver::VERBOSE;
148              
149 370         976 my $symbol_counts = $puzzle->{symbolCounts};
150 370         767 my $filled = 0;
151             # moving symbols with most givens upfront probably wouldn't yield
152 370         1073 foreach my $symbol (1..9) {
153 3330 100 100     17394 next if (exists($symbol_counts->{$symbol}) && ($symbol_counts->{$symbol} == 9));
154              
155             GROUP:
156 2614         5145 foreach my $g (@$Groups) {
157 70578         101927 my $only_position = undef;
158 70578         116526 foreach my $field_index (@$g) {
159 520410 100       1192046 next unless exists $Fields->[$field_index]{candidates}{$symbol};
160 61332 100       126138 next GROUP if defined $only_position;
161 31946         51752 $only_position = $field_index;
162             }
163            
164 41192 100       89554 if (defined $only_position) {
165 2560         8004 put_value($only_position, $symbol, $puzzle);
166 2560         4149 $filled++;
167 2560 50       7405 push @$steps_taken, 'HS_V_' . $symbol . '_' . Games::Sudoku::PatternSolver::Patterns::field_coordinates($only_position)
168             if $RECORD_STEPS;
169             }
170             }
171             }
172            
173 370         1390 return $filled;
174             }
175              
176             sub naked_singles {
177 370     370 0 1029 my ($puzzle) = @_;
178              
179 370 50       1054 print "naked_singles()\n" if $Games::Sudoku::PatternSolver::VERBOSE;
180              
181 370         924 my $filled = 0;
182 370         978 foreach my $field (@$Fields) {
183 29970 100       64111 $field->{value} and next;
184 9419         13477 my @candidates = keys %{$field->{candidates}};
  9419         23459  
185 9419 100       23041 if (@candidates == 1) {
186 649         1414 my $field_index = $field->{offset};
187 649         1276 my $symbol = shift @candidates;
188 649         2022 put_value($field_index, $symbol, $puzzle);
189 649         1019 $filled++;
190 649 50       1967 push @$steps_taken, 'NS_V_' . $symbol . '_' . Games::Sudoku::PatternSolver::Patterns::field_coordinates($field_index)
191             if $RECORD_STEPS;
192             }
193             }
194            
195 370         1018 return $filled;
196             }
197              
198             # Because the naked and hidden sets of candidates occuring within a group always are mutually complementary
199             # we have to implement search for only one of the two types.
200             # Going for the hidden pairs, triples and quads seems like it can be done more easily with our candidates and groups vectors.
201             # example with a hidden quad set: 816573294392......4572.9..6941...5687854961236238...4.279.....1138....7.564....82
202             sub locked_sets {
203 118     118 0 360 my ($puzzle) = @_;
204              
205 118 50       395 print "locked_sets()\n" if $Games::Sudoku::PatternSolver::VERBOSE;
206              
207 118         335 my $symbol_counts = $puzzle->{symbolCounts};
208 118         290 my $dropped = 0;
209 118         1064 my $test_vector = Bit::Vector->new(81);
210              
211 118         530 for (my $group_index = 0; $group_index < 27; $group_index++) {
212 3186         24570 my $group_array = $Groups->[$group_index];
213 3186         6216 my $group_vector = $Group_Vectors->[$group_index];
214              
215 3186         5499 my %candidate_counts = ();
216 3186         4948 my %candidate_vectors = ();
217 3186         5785 foreach my $field_index (@$group_array) {
218 28674 100       66860 $Fields->[$field_index]{value} and next;
219 9390         13149 foreach my $candidate_symbol (keys %{$Fields->[$field_index]{candidates}}) {
  9390         23795  
220 27331         49384 $candidate_counts{$candidate_symbol}++;
221             }
222             }
223              
224             # stay with candidates which occupy 2-4 cells in the group
225 3186         6922 foreach my $candidate_symbol (keys %candidate_counts) {
226 9390         14616 my $candidate_count = $candidate_counts{$candidate_symbol};
227 9390 100 100     26960 if ($candidate_count > 4 || $candidate_count == 1) {
228 905         1907 delete $candidate_counts{$candidate_symbol};
229              
230             } else {
231 8485         22008 my $candidate_in_group_vector = Bit::Vector->new(81);
232 8485         25445 $candidate_in_group_vector->And($group_vector, $Candidate_Vectors{$candidate_symbol});
233 8485         17039 $candidate_vectors{$candidate_symbol} = $candidate_in_group_vector;
234             }
235             }
236 3186         8003 my @candidates_in_group = keys %candidate_counts;
237 3186 100       13278 next if @candidates_in_group < 4;
238            
239             # iterate over all possible combinations of the 2-4 different candidates found above
240 1222         2265 foreach my $k_size (4, 3, 2) {
241 3666 100       24298 next if @candidates_in_group <= $k_size;
242 3151         8725 my $combination_iterator = combinations(\@candidates_in_group, $k_size);
243 3151         136852 while (my $candidates_combination = $combination_iterator->next) {
244 41546         373423 $test_vector->Empty();
245 41546         66068 foreach my $candidate (@$candidates_combination) {
246 123863         257633 $test_vector->Or($test_vector, $candidate_vectors{$candidate});
247             }
248              
249             # do the k different candidates occupy exactly k cells?
250 41546         97993 my @combined_fields = $test_vector->Index_List_Read();
251 41546 100       127524 if (@combined_fields == $k_size) {
252 558 50       1468 print "group $group_index, hidden_set (@$candidates_combination) in cells: [@combined_fields]\n" if $Games::Sudoku::PatternSolver::VERBOSE;
253             # got a hidden set
254             # now we must look for any other candidates in the same cells, which we can eliminate
255 558         1051 my %hidden_candidates = ();
256 558         1552 @hidden_candidates{@$candidates_combination} = undef;
257              
258 558         1010 foreach my $field_index (@combined_fields) {
259 1430         2039 foreach my $candidate_symbol (keys %{$Fields->[$field_index]{candidates}}) {
  1430         4159  
260 3565 100       9134 unless (exists $hidden_candidates{$candidate_symbol}) {
261 356         951 drop_candidate($field_index, $candidate_symbol);
262 356         525 $dropped++;
263 356 50       982 push @$steps_taken, 'LS_D_' . $candidate_symbol . '_' . Games::Sudoku::PatternSolver::Patterns::field_coordinates($field_index)
264             if $RECORD_STEPS;
265             }
266             }
267             }
268             }
269             }
270             }
271             }
272              
273 118         1351 return $dropped;
274             }
275              
276             sub locked_candidates {
277 118     118 0 426 my ($puzzle) = @_;
278              
279 118 50       419 print "locked_candidates()\n" if $Games::Sudoku::PatternSolver::VERBOSE;
280              
281 118         448 my $symbol_counts = $puzzle->{symbolCounts};
282 118         266 my $dropped = 0;
283 118         447 my $in_box_vector = Bit::Vector->new(81);
284 118         464 my $in_line_vector = Bit::Vector->new(81);
285              
286 118         382 foreach my $set (@$Intersections) {
287 6372         15257 my ($intersection_vector, $boxremainder_vector, $lineremainder_vector) = @$set;
288              
289 6372         9870 my %candidates_in_intersection = ();
290 6372         16735 foreach my $field_index ($intersection_vector->Index_List_Read()) {
291 19116         31199 my $intersection_field = $Fields->[$field_index];
292 19116         28953 $candidates_in_intersection{$_}++ for keys %{$intersection_field->{candidates}};
  19116         54912  
293             }
294              
295 6372         14740 foreach my $symbol (keys %candidates_in_intersection) {
296 11833         19188 my $candidate_vector = $Candidate_Vectors{$symbol};
297              
298 11833         31218 $in_box_vector->And($candidate_vector, $boxremainder_vector);
299 11833         25080 my @further_box_matches = $in_box_vector->Index_List_Read();
300              
301 11833         29500 $in_line_vector->And($candidate_vector, $lineremainder_vector);
302 11833         25033 my @further_line_matches = $in_line_vector->Index_List_Read();
303              
304 11833 100 100     64636 if (@further_box_matches && ! @further_line_matches) {
    100 100        
305             # case of 'Claiming'
306 49         99 foreach my $field_index (@further_box_matches) {
307 84         216 drop_candidate($field_index, $symbol);
308 84         147 $dropped++;
309 84 50       276 push @$steps_taken, 'LC_D_' . $symbol . '_' . Games::Sudoku::PatternSolver::Patterns::field_coordinates($field_index)
310             if $RECORD_STEPS;
311             }
312            
313             } elsif (@further_line_matches && ! @further_box_matches) {
314             # case of 'Pointing' aka 'Box / Line reduction'
315 64         163 foreach my $field_index (@further_line_matches) {
316 109         293 drop_candidate($field_index, $symbol);
317 109         200 $dropped++;
318 109 50       320 push @$steps_taken, 'LC_D_' . $symbol . '_' . Games::Sudoku::PatternSolver::Patterns::field_coordinates($field_index)
319             if $RECORD_STEPS;
320             }
321             }
322             }
323             }
324            
325 118         878 return $dropped;
326             }
327              
328             sub drop_candidate {
329 549     549 0 1070 my ($index, $value) = @_;
330              
331 549 50       1240 print "Dropping candidate '$value' from $index\n" if $Games::Sudoku::PatternSolver::VERBOSE;
332              
333 549         956 my $field = $Fields->[$index];
334 549         1156 delete $field->{candidates}{$value};
335 549         1646 $Candidate_Vectors{$value}->Bit_Off($index);
336             }
337              
338             sub put_value {
339 3209     3209 0 9719 my ($index, $value, $puzzle) = @_;
340              
341 3209 50       6964 print "Putting '$value' in $index\n" if $Games::Sudoku::PatternSolver::VERBOSE;
342              
343 3209         5716 my $field = $Fields->[$index];
344 3209         6593 $field->{value} = $value;
345 3209         10290 $field->{candidates} = {};
346              
347             # drop candidates
348 3209         6391 my $area_vector = $field->{covers};
349             # same value candidates from covered fields
350 3209         47186 delete $Fields->[$_]{candidates}{$value} for $area_vector->Index_List_Read();
351             # and also in the symbol's map of remaining candidates
352 3209         56822 $Candidate_Vectors{$value}->Bit_Off($_) for $area_vector->Index_List_Read();
353             # and in all candidate maps the one occupied place
354 3209         28013 $_->Bit_Off($index) for values %Candidate_Vectors;
355              
356 3209         11325 $puzzle->{symbolVectors}{$value}->Bit_On($index);
357 3209         8460 $puzzle->{symbolCounts}{$value}++;
358             }
359              
360             1