File Coverage

blib/lib/Games/Sudoku/PatternSolver.pm
Criterion Covered Total %
statement 228 258 88.3
branch 93 138 67.3
condition 9 30 30.0
subroutine 19 22 86.3
pod 3 15 20.0
total 352 463 76.0


line stmt bran cond sub pod time code
1             package Games::Sudoku::PatternSolver;
2              
3             our $VERSION = '0.07';
4              
5             require 5.06.0;
6              
7 4     4   401788 use Bit::Vector qw();
  4         3775  
  4         153  
8 4     4   26 use Time::HiRes qw();
  4         8  
  4         200  
9              
10 4     4   1846 use Games::Sudoku::PatternSolver::Patterns qw( init_patterns );
  4         11  
  4         339  
11              
12 4     4   28 use strict;
  4         8  
  4         109  
13 4     4   38 use warnings;
  4         8  
  4         16965  
14              
15             our @ISA = qw( Exporter );
16             our @EXPORT_OK = qw( solve print_solution print_grid $VERBOSE $MAX_SOLUTIONS $LOOSE_MODE $USE_LOGIC );
17             our %EXPORT_TAGS = (
18             'all' => \@EXPORT_OK,
19             );
20              
21             our $VERBOSE = 0; # Informative output about the algorithm at work
22             our $MAX_SOLUTIONS = 2; # If 0, will do an exhaustive search which may run into memory problems on invalid puzzles. (Time overhead isn't very much, usally.)
23             our $LOOSE_MODE = 1; # Finds the unique solution even for any sudoku with < 8 different givens, using letters as dropin symbols.
24             our $USE_LOGIC = 1; # Try to fill in more givens before starting with patterns? Only uses simple area constraints (naked / hidden singles).
25              
26             ####################################################################
27             our $exit_handler;
28              
29             {
30             my ($patterns_by_field, $all_patterns) = init_patterns();
31              
32             sub solve {
33 103 100   103 1 410897 my $str_puzzle = ((ref $_[0]) ? flatten_input(@_) : join('', @_))
    50          
34             or die " - dunno how to handle that input ... :-(\n";
35              
36 103 50       427 length($str_puzzle) == 81 or die "Puzzle input must be 81 chars long.\n";
37 103         498 $str_puzzle =~ s/[ 0]/./g; # dots, not zeros for free cells
38              
39 103 50       426 print_grid($str_puzzle) if $VERBOSE;
40              
41 103         365 my $start_time = Time::HiRes::time();
42              
43 103 100       411 my $puzzle = new_puzzle(
44             startTime => $start_time,
45             strPuzzle => $str_puzzle
46             ) or return 0;
47              
48 102 50       449 if ($USE_LOGIC) {
49 102         3758 require Games::Sudoku::PatternSolver::CPLogic;
50 102 50       745 Games::Sudoku::PatternSolver::CPLogic::apply_logic( $puzzle )
51             or return 0;
52              
53 102         584 my $logic_endtime = Time::HiRes::time();
54 102         438 my $logic_time = $logic_endtime - $start_time;
55              
56 102 100       554 if ($puzzle->{logicFilled}) {
57 99 50       364 printf("Logic mode provided %d more clues after %0.6f secs:", $puzzle->{logicFilled}, $logic_time) if $VERBOSE;
58              
59 99         309 $str_puzzle = $puzzle->{strPuzzleAfterLogic};
60 99 50       291 print_grid($str_puzzle) if $VERBOSE;
61             }
62              
63 102 100       355 if ($puzzle->{logicSolved}) {
64 22 50       59 print "Puzzle was solved by logic alone (no backtracking with patterns needed)!\n" if $VERBOSE;
65 22         50 $puzzle->{seconds} = $logic_time;
66 22         48 $puzzle->{endTime} = $logic_endtime;
67              
68              
69             ### TODO: Logic will only ever find the 1st solution. How to ensure a valid puzzle?
70 22         145 return $puzzle
71             }
72             }
73              
74 80         518 set_exit_handler($exit_handler);
75              
76 80 50       361 solve_puzzle($puzzle, $patterns_by_field, $all_patterns)
77             or return 0;
78              
79 80         429 restore_exit_handler();
80              
81 80         371 $puzzle->{endTime} = Time::HiRes::time();
82 80         313 $puzzle->{seconds} = $puzzle->{endTime} - $puzzle->{startTime};
83              
84 80         354 sweep($puzzle);
85              
86 80         1969 return $puzzle
87             }
88             }
89              
90             ####################################################################
91              
92             sub flatten_input {
93 80     80 0 167 my $input = $_[0];
94              
95 80 50       349 my $ref = ref($input) or return 0;
96              
97 80 50       291 if ($ref eq 'ARRAY') {
98 80 50 33     770 if ($#$input == 80 && (defined $input->[0]) && (not ref($input->[0]))) {
      33        
99 80         1266 return join '', @$input;
100             }
101              
102 0 0 0     0 if ($#$input == 8 && (defined $input->[0]) && (ref($input->[0]) eq 'ARRAY') && ($#$input->[0] == 8)) {
      0        
      0        
103             # AoA 9x9
104 0         0 return join( '', map {join( '', @$_)} @$input);
  0         0  
105             }
106             }
107              
108             return undef
109 0         0 }
110              
111             sub solve_puzzle {
112             # apply backtracking
113 80     80 0 229 my ($puzzle, $patterns_by_field, $all_patterns) = @_;
114              
115 80         272 my $symbol_vectors = $puzzle->{symbolVectors};
116 80         206 my $symbol_counts = $puzzle->{symbolCounts};
117              
118             # so far, only symbols with at least 1 given appear here
119 80 50       840 my @by_count = sort {((defined($symbol_counts->{$b}) ? $symbol_counts->{$b} : 0) <=> (defined($symbol_counts->{$a}) ? $symbol_counts->{$a} : 0)) || ($a cmp $b)} keys %$symbol_counts;
  1513 50       4947  
    50          
120              
121 80         221 my $no_clue_patterns;
122 80 50       229 if ($USE_LOGIC) {
123 80         272 foreach my $symbol (1 .. 9) {
124 692 100       1785 unless (exists $symbol_counts->{$symbol}) {
125             # all clueless symbols share the same candidate positions
126 5         15 my $no_clue_candidates = $puzzle->{candidateVectors}{$symbol};
127 5 100       1895 $no_clue_patterns = [map {$_->subset($no_clue_candidates) ? $_ : ()} @$all_patterns];
  233280         653332  
128 5         161 last;
129             }
130             }
131             }
132 80   66     512 $no_clue_patterns ||= $all_patterns;
133              
134 80 100 66     396 if ($LOOSE_MODE && @by_count < 8) {
135             # with less than 8 different givens, we have to stock up on distinguishing cell markers
136 4         35 my @dropins = ('A' .. 'I');
137 4         18 while (@by_count < 9) {
138 12         27 my $dropin_symbol = shift @dropins;
139 12         24 push @by_count, $dropin_symbol;
140 12         57 $symbol_counts->{$dropin_symbol} = 0;
141 12         83 $symbol_vectors->{$dropin_symbol} = Bit::Vector->new(81);
142             }
143             }
144 80         242 $puzzle->{maxDepth} = $#by_count;
145              
146 80         719 my $test_vector = Bit::Vector->new(81);
147              
148 80         213 my %possible_solutions = ();
149 80         185 my $invalid = 0;
150              
151 80         231 foreach my $symbol (@by_count) {
152 719 50       3084 my $symbol_vector = $symbol_vectors->{$symbol}
153             or die "No vector for givens '$symbol'!\n";
154              
155 719         2299 my $t2 = Time::HiRes::time();
156              
157 719         1215 my $pre_filtered;
158              
159 719 100       2685 if ($symbol_counts->{$symbol} == 9) {
    100          
160 334         721 $pre_filtered = [$symbol_vector];
161              
162             } elsif ($symbol_counts->{$symbol} > 0) {
163             # start with a reduced set of 5184 instead of 46656 possible distributions for this symbol
164 373         1664 my $any_given_field = $symbol_vector->Min();
165              
166 373 50       1029 if ($USE_LOGIC) {
167             # a chance to early prune patterns by subsetting them against their resp. candidate map
168 373         1994 my $allowed_positions = Bit::Vector->new(81);
169 373         2466 $allowed_positions->Or($symbol_vector, $puzzle->{candidateVectors}{$symbol});
170 373 100       660 $pre_filtered = [map {$_->subset($allowed_positions) ? $_ : ()} @{$patterns_by_field->{$any_given_field}}];
  1933632         5168576  
  373         11589  
171              
172             } else {
173 0         0 $pre_filtered = $patterns_by_field->{$any_given_field};
174             }
175              
176             } else {
177             # no givens -> a general prefilterung by shared candidates happened above
178 12         22 $pre_filtered = $no_clue_patterns;
179             }
180              
181 719         2129 my @solutions = ();
182 719         1724 my $found = 0;
183 719         1291 my $omitted = 0;
184              
185 719         1866 foreach my $pattern_vector (@$pre_filtered) {
186 3744 50       13336 if ($symbol_vector->subset($pattern_vector)) {
187 3744         5548 $found++;
188              
189             # early omittance of patterns wherever a non-conflicting pattern was not found for any of the former symbols
190 3744         5725 my $outer = 1;
191 3744         10789 foreach my $symbol_2 (keys %possible_solutions) {
192 19931         28198 my $inner = 0;
193 19931         27118 foreach my $pattern_vector_2 (@{$possible_solutions{$symbol_2}}) {
  19931         34845  
194 38607         99575 $test_vector->And($pattern_vector, $pattern_vector_2);
195 38607 100       90271 if ($test_vector->is_empty()) {
196             # fits to at least one of that other symbol's pattern candidates
197             # proceed to testing next symbol's candidates
198              
199 19702         28244 $inner = 1;
200 19702         27988 last;
201             }
202             }
203              
204 19931 100       40564 unless($inner) {
205 229         419 $outer = 0;
206 229         425 last;
207             }
208             }
209              
210 3744 100       9100 if ($outer) {
211 3515         9969 push @solutions, $pattern_vector;
212              
213             } else {
214 229         482 $omitted++;
215             }
216              
217             }
218             }
219              
220 719 50       2226 unless ($found - $omitted) {
221 0         0 print "Givens '$symbol' have no pattern match, puzzle is invalid!\n";
222 0         0 $invalid++;
223             }
224              
225 719         2430 $possible_solutions{$symbol} = \@solutions;
226             printf(" Symbol '%s' (%d givens) %5d assigned -> %4d kept (%4d omitted) in %f secs\n",
227 719 50       3544 $symbol, $symbol_counts->{$symbol}, $found, $found - $omitted, $omitted, Time::HiRes::time() - $t2) if $VERBOSE;
228             }
229              
230 80 50       337 return 0 if $invalid;
231              
232             # in a 2nd step, further reduce the number of patterns
233 80 50       283 print("Reverse elimination of pattern candidates:\n") if $VERBOSE;
234 80         457 reverse_filter(\@by_count, \%possible_solutions, $symbol_counts);
235              
236 80 50       342 printf("Start backtracking for possible pattern combinations after %0.5f secs:\n", Time::HiRes::time() - $puzzle->{startTime}) if $VERBOSE;
237 80         753 find_solutions(0, \@by_count, \%possible_solutions, Bit::Vector->new(81), {}, $puzzle);
238              
239 80         1599 return 1
240             }
241              
242             sub reverse_filter {
243             # try to eliminate even more patterns by doing the fitting test in reverse order
244 80     80 0 296 my ($by_count, $possible_solutions, $symbol_counts) = @_;
245              
246 80         566 my $test_vector = Bit::Vector->new(81);
247              
248             # starting out on patterns for the most abundant symbol, removing those who have no non-conflicting counterpart for any of the rarer symbols
249              
250 80         497 for (my $symbol_index_1 = 0; $symbol_index_1 < $#$by_count; $symbol_index_1++) {
251 639         1190 my $symbol_1 = $by_count->[$symbol_index_1];
252 639         1115 my $solutions_1 = $possible_solutions->{$symbol_1};
253 639         961 my $found = 0;
254 639         936 my $omitted = 0;
255              
256 639         1765 my $t2 = Time::HiRes::time();
257 639         1117 my @filtered_solutions = ();
258              
259 639         1177 foreach my $pattern_vector_1 (@$solutions_1) {
260 2709         3818 $found++;
261              
262 2709         3890 my $outer = 1;
263 2709         7668 for (my $symbol_index_2 = $symbol_index_1 + 1; $symbol_index_2 <= $#$by_count; $symbol_index_2++) {
264 8765         13686 my $symbol_2 = $by_count->[$symbol_index_2];
265 8765         13680 my $solutions_2 = $possible_solutions->{$symbol_2};
266              
267 8765         12069 my $inner = 0;
268 8765         14093 foreach my $pattern_vector_2 (@$solutions_2) {
269 21786         56557 $test_vector->And($pattern_vector_1, $pattern_vector_2);
270 21786 100       51624 if ($test_vector->is_empty()) {
271             # fits to at least one of that 2nd symbol's pattern candidates
272             # proceed to testing the outer symbol's next candidate
273 8701         12301 $inner = 1;
274 8701         12519 last;
275             }
276             }
277              
278 8765 100       23168 unless($inner) {
279 64         105 $outer = 0;
280 64         115 last;
281             }
282             }
283              
284 2709 100       5084 if ($outer) {
285 2645         5977 push @filtered_solutions, $pattern_vector_1;
286              
287             } else {
288 64         116 $omitted++;
289             }
290             }
291 639         1387 $possible_solutions->{$symbol_1} = \@filtered_solutions;
292             printf(" Symbol '%s' (%d givens) %4d -> %4d were kept (%4d omitted) in %f secs\n",
293 639 50       2942 $symbol_1, $symbol_counts->{$symbol_1}, $found, $found - $omitted, $omitted, Time::HiRes::time() - $t2) if $VERBOSE;
294             }
295             }
296              
297             # the essential recursive function of the solver
298             # the return value signals whether to abort (false) or proceed (true) with the backtracking
299             sub find_solutions {
300 22930     22930 0 49269 my ($depth, $symbols, $possibles, $coverage_vector, $current_pattern_vectors, $puzzle) = @_;
301              
302             $depth > $puzzle->{maxDepth}
303 22930 50       48405 and die sprintf("FIX ME: depth=$depth (maxDepth = %d)\n", $puzzle->{maxDepth});
304              
305 22930         42894 my $symbol = $symbols->[$depth];
306              
307 22930         68973 my $test_vector = Bit::Vector->new(81);
308 22930         35572 foreach my $pattern_vector (@{$possibles->{$symbol}}) {
  22930         51689  
309 839818         2118946 $test_vector->And($coverage_vector, $pattern_vector);
310              
311 839818 100       1912341 if ($test_vector->is_empty()) {
312             # no conflicts
313 24717         50285 $current_pattern_vectors->{$symbol} = $pattern_vector;
314 24717         67271 $coverage_vector->Or($coverage_vector, $pattern_vector);
315              
316 24717 100       54667 if ($depth == $puzzle->{maxDepth}) {
317             # is it a proper new solution?
318             # because patterns are tried in random order and one pattern can be tried for different symbols, repetitive solutions have to be avoided
319 1867         5009 my $newSolutionKey = solution_is_new($puzzle, $current_pattern_vectors);
320 1867 100       4648 if ($newSolutionKey) {
321 1842         4333 my $solution_string = prepare_solution_string($symbols, $current_pattern_vectors);
322 1842         6066 add_solution($puzzle, $newSolutionKey, $solution_string);
323 1842 50       4183 print_solution($solution_string, $puzzle->{solutionCount}, $puzzle->{startTime}) if $VERBOSE;
324 1842 100       4696 check_for_max_solutions($puzzle->{solutionCount}) or return 0;
325             }
326              
327             } else {
328             # descent, and bubble up a false return value
329 22850 100       55123 find_solutions ($depth+1, $symbols, $possibles, $coverage_vector, $current_pattern_vectors, $puzzle)
330             or return 0;
331             }
332             # remove this pattern and proceed on the same level
333 24123         49293 delete $current_pattern_vectors->{$symbol};
334 24123         66070 $coverage_vector->Xor($coverage_vector, $pattern_vector);
335             }
336             }
337              
338 22336         101870 return 1
339             }
340              
341             sub prepare_solution_string {
342 1842     1842 0 3899 my ($symbols, $pattern_vectors) = @_;
343              
344 1842         19906 my @chars = ('0') x 81;
345              
346 1842         5047 for (my $symbol_index = 0; $symbol_index < 9; $symbol_index++) {
347 16578         26912 my $symbol = $symbols->[$symbol_index];
348 16578 100       30317 defined($symbol) or last;
349 15945 50       33597 my $symbol_vector = $pattern_vectors->{$symbol} or last;
350 15945         96251 $chars[$_] = $symbol for $symbol_vector->Index_List_Read();
351             }
352              
353 1842         21333 return join '', @chars
354             }
355              
356             sub print_solution {
357 0     0 1 0 my ($solution, $solution_nr, $startTime) = @_;
358            
359 0 0 0     0 printf("\nSolution #%d after %f secs:", $solution_nr, Time::HiRes::time() - $startTime) if $VERBOSE && $solution_nr && $startTime;
      0        
360              
361 0         0 $solution =~ s/[^1-9A-I]/ /g;
362 0         0 print_grid($solution);
363             }
364              
365             sub print_grid {
366 0     0 1 0 my $grid_string = shift;
367 0         0 my @symbols = split //, $grid_string;
368              
369 0         0 my $row_count = 0;
370 0         0 print "\n-------------------------\n";
371 0         0 while (my @row = splice @symbols, 0, 9) {
372 0         0 while (my @triple = splice @row, 0, 3) {
373 0         0 print "| @triple ";
374             }
375 0 0       0 print(++$row_count % 3 ? "|\n" : "|\n-------------------------\n");
376             }
377             }
378              
379             sub check_for_max_solutions {
380 1842     1842 0 3052 my $solution_count = shift;
381              
382 1842 100       3667 if ($MAX_SOLUTIONS) {
383 135 100       346 if ($solution_count >= $MAX_SOLUTIONS) {
384 66 50       189 print "\$MAX_SOLUTIONS=$MAX_SOLUTIONS are reached - exiting.\n" if $VERBOSE;
385 66         1694 return 0;
386             }
387             }
388              
389 1776         4476 return 1
390             }
391              
392             {
393             # prevent error messages in windows, if user break happens inside a nested call
394             my $old_handler;
395              
396             sub set_exit_handler {
397 80     80 0 238 my ($sub_to_execute) = @_;
398 80 100       355 return if $Games::Sudoku::PatternSolver::exitHandler;
399 2 50       24 $old_handler and return;
400              
401 2         27 $old_handler = $SIG{'INT'};
402             $SIG{'INT'} = $Games::Sudoku::PatternSolver::exitHandler = sub {
403 0     0   0 print "Exit on user request.\n";
404 0 0       0 &$sub_to_execute if $sub_to_execute;
405 0         0 CORE::exit(0);
406 2         32 };
407             }
408              
409             sub restore_exit_handler {
410 80 50   80 0 338 if ($old_handler) {
411 0         0 $SIG{'INT'} = $old_handler;
412 0         0 $old_handler = undef;
413             }
414             }
415             }
416              
417             ###############################################################
418              
419             sub new_puzzle {
420              
421 103     103 0 2054 return _read_puzzle({
422             startTime => 0,
423             strPuzzle => undef,
424             @_,
425              
426             endTime => 0,
427             seconds => 0,
428              
429             # properties and state keeping
430             maxDepth => undef,
431             symbolVectors => undef,
432             givensCount => undef,
433             uniqueGivens => undef,
434             countsByGivens => undef, # at start, on puzzle init
435             symbolCounts => undef, # a copy that reflects the current state
436              
437             # logic mode related, might get used for ratings
438             logicFilled => 0,
439             candidatesDropped => 0,
440             logicSteps => [],
441             logicSolved => 0,
442              
443             # backtracking related
444             rejectedCount => 0,
445             knownSolutions => {},
446              
447             solutions => [],
448             solutionCount => 0,
449             })
450             }
451              
452             sub _read_puzzle {
453 103     103   378 my ($puzzle) = @_;
454              
455             my $puzzle_string = $puzzle->{strPuzzle}
456 103 50       434 or die "Named param 'strPuzzle' must be passed to new_puzzle()";
457              
458 103         243 my $givens = 0;
459 103         249 my %counts = ();
460 103         232 my %symbol_vectors = ();
461 103         1787 @symbol_vectors{1..9} = Bit::Vector->new(81, 9);
462              
463 103         324 my $field_offset = 0;
464 103         1931 foreach my $symbol (split //, $puzzle_string) {
465 8343 100       19149 if ($symbol =~ /[1-9]/) {
466 3517         4904 $givens++;
467 3517         5847 $counts{$symbol}++;
468 3517         7680 $symbol_vectors{$symbol}->Bit_On($field_offset);
469             }
470 8343         13490 $field_offset++;
471             }
472              
473 103         1165 $puzzle->{symbolVectors} = \%symbol_vectors;
474 103         242 $puzzle->{givensCount} = $givens;
475 103         317 $puzzle->{uniqueGivens} = scalar keys %counts;
476 103         258 $puzzle->{countsByGivens} = \%counts;
477 103         823 $puzzle->{symbolCounts} = {%counts};
478              
479 103 50       519 print("Puzzle has $puzzle->{uniqueGivens} different symbols.\n") if $VERBOSE;
480              
481 103 50 100     772 if ($puzzle->{uniqueGivens} < 5) {
    100          
482 0         0 warn "Puzzle has $puzzle->{uniqueGivens} different symbols and cannot have a unique solution.\n";
483 0         0 return 0;
484              
485             } elsif ($puzzle->{uniqueGivens} < 8 && ! $LOOSE_MODE) {
486 1         56 warn "Puzzle has $puzzle->{uniqueGivens} different symbols and may be solved with \$LOOSE_MODE=1 only.\n";
487 1         34 return 0;
488             }
489              
490 102 50       326 if ($puzzle->{givensCount} < 17) {
491 0         0 warn "Puzzle has $puzzle->{givensCount} givens while 17 is regarded as bare minimum for a unique solution.\n";
492 0         0 return 0;
493             }
494              
495 102         642 return $puzzle
496             }
497              
498             sub sweep {
499 80     80 0 244 my ($puzzle, $thorough) = @_;
500              
501 80 50       274 $thorough = 1 unless defined($thorough);
502              
503 80 50       250 if ($thorough) {
504 80         2629 delete $puzzle->{$_} for qw(
505             maxDepth
506             symbolVectors
507             symbolCounts
508             rejectedCount
509             knownSolutions
510             );
511              
512             } else {
513 0         0 $puzzle->{knownSolutions} = {};
514 0         0 $puzzle->{symbolVectors} = undef;
515             }
516              
517 80         232 return $puzzle
518             }
519              
520             sub add_solution {
521 1842     1842 0 4267 my ($puzzle, $key, $solution) = @_;
522              
523 1842         7641 $puzzle->{knownSolutions}{$key} = undef;
524 1842         3148 push @{$puzzle->{solutions}}, $solution;
  1842         5220  
525              
526 1842         3960 return $puzzle->{solutionCount}++
527             }
528              
529             sub solution_is_new {
530 1867     1867 0 3978 my ($puzzle, $vectors) = @_;
531              
532             # the hash values are the current Bit::Vector objects, found to satisfy the puzzle
533             # here we create a short, unique key with the addresses of these up to 9 vectors
534 1867         6383 my $key = join '|', sort map {/0x(.+?)\)$/} values %$vectors;
  16170         66373  
535              
536 1867 100       8581 if (exists $puzzle->{knownSolutions}{$key}) {
537 25         57 $puzzle->{rejectedCount}++;
538 25         69 return undef;
539             }
540              
541 1842         4610 return $key
542             }
543              
544             1
545