File Coverage

blib/lib/Games/Sudoku/PatternSolver/Generator.pm
Criterion Covered Total %
statement 135 146 92.4
branch 37 68 54.4
condition 15 35 42.8
subroutine 18 19 94.7
pod 2 8 25.0
total 207 276 75.0


line stmt bran cond sub pod time code
1             package Games::Sudoku::PatternSolver::Generator;
2              
3 2     2   140596 use strict;
  2         5  
  2         99  
4 2     2   12 use warnings;
  2         2  
  2         167  
5              
6             require 5.06.0;
7              
8 2     2   620 use Games::Sudoku::PatternSolver::Patterns qw( init_patterns );
  2         7  
  2         170  
9 2     2   618 use Games::Sudoku::PatternSolver qw( solve print_grid );
  2         8  
  2         241  
10              
11 2     2   16 use Bit::Vector;
  2         3  
  2         73  
12 2     2   8 use List::Util qw( shuffle );
  2         2  
  2         118  
13 2     2   9 use Time::HiRes ();
  2         3  
  2         3320  
14              
15             our @ISA = qw( Exporter );
16             our @EXPORT_OK = qw( get_sudoku_builder get_grid_builder print_grid );
17             our %EXPORT_TAGS = (
18             'all' => \@EXPORT_OK,
19             );
20              
21             our $VERBOSE = 0;
22             our $LOOSE_MODE = 0; # whether to allow puzzles to have < 8 different givens (still with an unique solution)
23              
24             ###########################################################
25              
26             {
27             my $grid_builder;
28              
29             # the grid at start doesn't matter for quality of the individual puzzle generated,
30             # but all puzzles generated from a single static grid share the same solution
31             sub get_sudoku_builder {
32 1     1 1 1276 my ($start_grid_string, $start_with, $shuffle_symbols) = @_;
33              
34 1         5 set_solver_config();
35              
36 1 50       5 if ($start_grid_string) {
37 1 50       6 my $sudoku = solve($start_grid_string)
38             or return 0;
39              
40 1         3 my $sc = $sudoku->{solutionCount};
41              
42 1 50       3091 if ($sc != 1) {
43 0 0       0 printf "No way to reduce: Start grid %s and is invalid!\n", ($sc > 1) ? 'already has more than one solutions' : 'has NO solution';
44 0         0 return 0;
45             }
46             }
47              
48 1 50 33     18 my $check_reducibility = ($start_grid_string && $start_grid_string =~ /[^1-9]/) ? 1 : 0;
49              
50 1 50       18 $start_with = 40 unless defined($start_with);
51 1 50       4 $shuffle_symbols = 1 unless defined($shuffle_symbols);
52              
53 1   33     10 $grid_builder ||= get_grid_builder();
54 1   33     38 my $next_grid_str = $start_grid_string || &$grid_builder($shuffle_symbols);
55              
56             return sub {
57             # given a possible solution, reduce the given values in random order until a proper sudoku is found that cannot be further reduced
58             # (searching for a rating > 0.65 may take a while)
59              
60             # my $old_handler = $SIG{INT};
61             # local $SIG{INT} = set_exit_handler(sub {print "BOOO!\n", &$old_handler() if ref($old_handler);});
62             #my $solve_tries = 0;
63              
64 1     1   6 while (1) {
65 2 50       6 print ">>> '$next_grid_str'\n" if $VERBOSE;
66             # the initially given grid may have been fleshed out already
67 2         63 my $grid = [split //, $next_grid_str];
68 2 50       19 my @start_positions = map { $grid->[$_] =~ /[1-9]/ ? $_ : () } (0..$#$grid);
  162         463  
69             #@start_positions >= 17 or die "Trying to reduce ", scalar(@start_positions), " givens from '$next_grid_str' is pretty much senseless.\n";
70 2         11 my $to_drop = @start_positions - $start_with;
71 2 50       22 $to_drop = 0 if $to_drop < 0;
72              
73 2 50       32 my $fields_to_try = $check_reducibility ? [@start_positions] : [shuffle @start_positions];
74              
75             # to get going, start from a certain depth
76 2         6 my $dropped_fields = [];
77 2 50 33     12 ($to_drop && !$check_reducibility) and do {
78 2 50       6 drop_values($grid, $to_drop, $fields_to_try, $dropped_fields)
79             or die "Couldn't drop $to_drop givens from the grid (@$grid).\n"
80             };
81              
82 2         4 my $to_be_reduced;
83 2         5 my $solution_count = 0;
84              
85 2         7 while ($solution_count < 2) {
86 82 100       356 unless (drop_values($grid, 1, $fields_to_try, $dropped_fields)) {
87             # tried to drop any single field that was left so far (bar 17), all leading to > 1 solutions => not further reducible => start over
88 2 100 33     14 if ($to_be_reduced) {
    50          
89 1   33     6 $next_grid_str = $start_grid_string || &$grid_builder($shuffle_symbols);
90 1         3 $check_reducibility = 0;
91              
92             # this returns always a multiple of 40 (40 - 320 .. ??) why ???
93             #print $solve_tries, " tries to solve were needed.\n";
94              
95 1         71 return $to_be_reduced;
96              
97             } elsif ($check_reducibility && !$to_be_reduced) {
98 0         0 print "$solution_count: Start grid is minimal (could not be reduced): '$next_grid_str'\n";
99 0         0 return 0;
100             }
101 1         2 last;
102             }
103              
104 80 50       359 my $sudoku = solve($grid)
105             or last;
106             #$solve_tries++;
107 80         285 $solution_count = $sudoku->{solutionCount};
108 80 50       271 print "sc=$solution_count\n" if $VERBOSE;
109              
110 80 100       328 if ($solution_count == 1) {
111 16 50 66     165 if ((!$to_be_reduced) || ($to_be_reduced->{uniqueGivens} > $sudoku->{uniqueGivens}) || ($to_be_reduced->{givensCount} >= $sudoku->{givensCount})) {
      66        
112             # most interesting (reduced) puzzle found in the given start grid so far,
113             # still a subject to possible minimization
114 16         360 $to_be_reduced = $sudoku;
115             }
116 16         86 next;
117             }
118              
119 64 50       225 unless ($solution_count == 2) {
120 0         0 warn "The current grid seems unsolvable!\n"; # . Dumper($sudoku);
121 0         0 return 0;
122             }
123              
124             # return to the former state (solution count == 1)
125             # and try to achieve further reduction by dropping another value instead
126 64         374 reinsert_last_value($grid, $dropped_fields);
127 64         796 $solution_count = 1;
128             }
129              
130 1 50       3 if ($check_reducibility) {
131 0         0 $check_reducibility = 0;
132 0         0 print "Reducibility check finished.\n";
133 0         0 return 0;
134             }
135              
136 1   33     36 $next_grid_str = $start_grid_string || &$grid_builder($shuffle_symbols);
137             }
138              
139 0         0 return 0
140 1         16 };
141             }
142             }
143              
144             sub drop_values {
145 84     84 0 373 my ($grid, $to_drop, $fields_to_try, $dropped) = @_;
146              
147 84         172 my $dropped_count = 0;
148 84         276 while ( $dropped_count < $to_drop ) {
149 164         338 my $field_index = shift @$fields_to_try;
150 164 100       396 return $dropped_count unless defined($field_index);
151 162         312 my $symbol = $grid->[$field_index];
152 162 50       376 print "dropped $symbol from $field_index\n" if $VERBOSE;
153 162 50 33     822 die "Unexpected symbol '$symbol' in position $field_index!" if (!$symbol || $symbol eq '.');
154 162         331 $grid->[$field_index] = '.';
155 162         482 push @$dropped, [$field_index, $symbol];
156 162         448 $dropped_count++;
157             }
158              
159 82         321 return $dropped_count;
160             }
161              
162             sub reinsert_last_value {
163             # only puts the value back into the grid, does not return the index into the queue of drop candidates
164 64     64 0 235 my ($grid, $dropped) = @_;
165              
166 64 50       122 my ($field_index, $symbol) = @{pop @$dropped} or die "No more grid positions to restore!";
  64         392  
167 64         293 $grid->[$field_index] = $symbol;
168 64 50       232 print "restored $symbol at $field_index\n" if $VERBOSE;
169             }
170              
171             sub set_solver_config {
172             # fastest way to find out if a sudoku has 0, 1, or more solutions
173 1     1 0 3 $Games::Sudoku::PatternSolver::VERBOSE = $VERBOSE;
174 1         25 $Games::Sudoku::PatternSolver::MAX_SOLUTIONS = 2;
175 1         3 $Games::Sudoku::PatternSolver::LOOSE_MODE = $LOOSE_MODE;
176 1         3 $Games::Sudoku::PatternSolver::USE_LOGIC = 1;
177             }
178              
179             ###########################################################
180              
181             {
182             my $patterns_by_field;
183              
184             sub get_grid_builder {
185              
186 2   66 2 1 215832 $patterns_by_field ||= (init_patterns())[0];
187              
188 2         11 shuffle_pattern_arrays($patterns_by_field);
189              
190             # creating 81 iterators; each one iterates over a circular list with 5184 vectors in random order, which all have the specified field bit in common
191 2         10 my %pattern_iterators = ();
192 2         34 foreach my $field_index (keys %$patterns_by_field) {
193 162         323 my $list = $patterns_by_field->{$field_index};
194 162         360 my $index = -1;
195             $pattern_iterators{$field_index} = sub {
196 8728     8728   13696 $index++;
197 8728 50       19139 $index = 0 if $index > $#$list;
198 8728         19600 return $list->[ $index ];
199 162         747 };
200             }
201              
202 2         52 my $test_vector = Bit::Vector->new(81);
203              
204             return sub {
205 1   50 1   15 my $shuffle_symbols = shift() || 0;
206              
207 1         6 local $SIG{'INT'} = set_exit_handler();
208              
209 1         3 while (1) {
210 1         2 my @placed_vectors = ();
211 1         8 my $coverage_vector = Bit::Vector->new(81);
212 1         2 my $placed = 0;
213            
214 1         5 while ($placed < 9) {
215 9         63 $test_vector->Not($coverage_vector);
216 9         40 my $any_empty_field = $test_vector->Min();
217              
218 9         13 my $could_place = 0;
219 9         27 for (my $i = 0; $i < 5184; $i++) {
220 8728         17490 my $pattern_vector = $pattern_iterators{$any_empty_field}();
221 8728         29648 $test_vector->And($coverage_vector, $pattern_vector);
222              
223 8728 100       30077 if ($test_vector->is_empty()) {
224 9         35 $coverage_vector->Or($coverage_vector, $pattern_vector);
225 9         29 push @placed_vectors, $pattern_vector;
226 9         193 $placed++;
227 9         19 $could_place = 1;
228 9         23 last;
229             }
230             }
231 9 50       41 $could_place or last;
232             }
233              
234 1 50       12 ($placed == 9) and return join '', prepare_grid(\@placed_vectors, $shuffle_symbols);
235             }
236 2         55 };
237             }
238              
239             sub prepare_grid {
240             # an arrayref with 9 complementing bit vectors
241 1     1 0 30 my ($pattern_stack, $do_shuffle) = @_;
242              
243             # having 1-9 assigned to the 1st row makes spotting eventual repetitions more easy
244 1 50       10 my @symbols = $do_shuffle ? (shuffle 1..9) : 1..9;
245            
246 1         4 my @chars = ();
247 1         5 foreach my $pattern_vector (@$pattern_stack) {
248 9         17 my $symbol = shift @symbols;
249 9         96 $chars[$_] = $symbol for $pattern_vector->Index_List_Read();
250             }
251              
252             return @chars
253 1         109 }
254              
255             sub shuffle_pattern_arrays {
256 2     2 0 7 my ($HashOfArrays) = @_;
257              
258 2         8 my $t1 = Time::HiRes::time();
259 2         63 foreach my $key (keys %$HashOfArrays) {
260 162         796 @{$HashOfArrays->{$key}} = shuffle( @{$HashOfArrays->{$key}} );
  162         227044  
  162         48782  
261             }
262 2 50       68 printf("Shuffling patterns took %0.4f secs.\n", Time::HiRes::time() - $t1) if $VERBOSE;
263             }
264             }
265              
266             sub set_exit_handler {
267 1     1 0 4 my ($sub_to_execute) = @_;
268              
269             return $Games::Sudoku::PatternSolver::exitHandler = sub {
270             #print "Exit on user request.\n";
271 0 0   0     &$sub_to_execute if $sub_to_execute;
272             #Time::HiRes::sleep 0.2;
273             #CORE::exit(0);
274 1         26 };
275             }
276              
277             1
278              
279             __END__