File Coverage

blib/lib/Games/Sudoku/PatternSolver/Patterns.pm
Criterion Covered Total %
statement 112 173 64.7
branch 14 48 29.1
condition 1 9 11.1
subroutine 12 17 70.5
pod 0 9 0.0
total 139 256 54.3


line stmt bran cond sub pod time code
1             package Games::Sudoku::PatternSolver::Patterns;
2              
3 4     4   54 use strict;
  4         12  
  4         160  
4 4     4   23 use warnings;
  4         11  
  4         539  
5              
6             require 5.06.0;
7              
8             our @ISA = qw( Exporter );
9             our @EXPORT_OK = qw( init_patterns init_fields build_groups print_grid_from_vector get_intersections field_coordinates );
10             our %EXPORT_TAGS = (
11             'all' => \@EXPORT_OK,
12             );
13              
14 4     4   522 use Bit::Vector;
  4         1354  
  4         277  
15 4     4   38 use Time::HiRes;
  4         7  
  4         34  
16              
17             my $VERBOSE = 0;
18              
19             my $file_name = 'patterns_9x9.bin';
20              
21             #####################################
22              
23             my (%pos_to_offset, %offset_to_pos);
24              
25             {
26             my (%pattern_vectors_by_field, @all_patterns);
27              
28             sub init_patterns {
29             # load from file or ask whether to create the patterns file
30              
31 5 100   5 0 27 return (\%pattern_vectors_by_field, \@all_patterns) if %pattern_vectors_by_field;
32              
33 4         10 my $module_path = __FILE__;
34 4         92 $module_path =~ s|[^/]*$||;
35              
36 4         11 my $existing_file;
37 4 50       213 if (-f './' . $file_name) {
    50          
38 0         0 $existing_file = './' . $file_name;
39              
40             } elsif (-f $module_path . $file_name) {
41 4         17 $existing_file = $module_path . $file_name;
42             }
43              
44 4 50       12 if ($existing_file) {
45 4         31 $file_name = $existing_file;
46              
47             } else {
48 0         0 print "No pattern file '$file_name' exists in '$module_path' or in the current directory.\n";
49              
50 0 0       0 if (-w $module_path) {
51 0         0 $file_name = $module_path . 'symbol_patterns.bin';
52              
53             } else {
54 0 0       0 my $elevated = ($^O =~ /Win/i ? 'administrator' : 'sudo');
55            
56 0         0 print "Pls. consider to exit now and restart the program as $elevated, to have the file created in '$module_path' once and for all.\n";
57 0         0 print "Or proceed to have it created in the current directory.\n";
58 0         0 $file_name = './' . 'symbol_patterns.bin';
59             }
60              
61 0         0 print "\nCreating the patterns should take < 1 minute and takes 0.5MB space.\nProceed? [Yn]:";
62 0         0 my $answer = ;
63 0 0       0 exit 0 unless $answer =~ /Y/;
64              
65 0         0 my $t1 = Time::HiRes::time();
66 0         0 my $fields = init_fields();
67              
68             # prevent an annoying and false OOM message, happening on windows when Ctrl-C is signalled while executing a nested loop
69 0         0 my $old_handler = $SIG{'INT'};
70             $SIG{'INT'} = sub {
71 0     0   0 print "Exit on user request.\n";
72 0         0 CORE::exit(0);
73 0         0 };
74              
75 0 0       0 open my $Pattern_Handle, '>', $file_name or die $!;
76 0         0 binmode $Pattern_Handle;
77              
78 0         0 my $written_count = 0;
79 0         0 create_patterns($Pattern_Handle, 0, 0, Bit::Vector->new(81), Bit::Vector->new(81), \$written_count, $fields);
80 0         0 close $Pattern_Handle;
81              
82 0         0 $SIG{'INT'} = $old_handler;
83 0         0 printf("Creating and storing %d patterns took %.2f secs.\n", $written_count, Time::HiRes::time() - $t1);
84             }
85              
86 4         59 my $file_size = -s $file_name;
87 4 50 33     59 unless ($file_size and ! ($file_size % 46656)) {
88 0         0 die "$file_name seems corrupted.";
89             }
90              
91 4         15 my $bytes_per_pattern = $file_size / 46656;
92              
93 4 50       192 open my $Bin_File, '<', $file_name or die "Could not open the patterns binary at '$file_name':\n" . $!;
94 4         16 binmode $Bin_File;
95 4         10 my $load_count = 0;
96 4         8 my $buffer = '';
97 4         11 my $bytes = 0;
98              
99 4         57 while ($bytes = sysread $Bin_File, $buffer, $bytes_per_pattern) {
100 186624 50       405644 $bytes == $bytes_per_pattern or die "Unexpected number of $bytes bytes read (expected $bytes_per_pattern) after $load_count Patterns loaded.\n";
101 186624         620449 my $vector = Bit::Vector->new(81);
102 186624         498362 $vector->Block_Store($buffer);
103              
104 186624         515066 my @fields = $vector->Index_List_Read();
105 186624         330240 foreach my $field_index (@fields) {
106 1679616         2313400 push @{$pattern_vectors_by_field{$field_index}}, $vector;
  1679616         3751492  
107             }
108 186624         324690 push @all_patterns, $vector;
109 186624         1480443 $load_count++;
110             }
111 4         96 close $Bin_File;
112              
113 4 50       74 unless ($load_count == 46656) {
114 0         0 die "Expected to load 46656 pattern vectors from '$file_name' (got $load_count instead).\n";
115             }
116              
117 4 50       46 print("$load_count patterns loaded from $file_name\n") if $VERBOSE;
118              
119 4         54 return (\%pattern_vectors_by_field, \@all_patterns)
120             }
121             }
122              
123             # a recursive function that creates all possible distribution patterns and writes them to a binary file
124             sub create_patterns {
125 0     0 0 0 my ($file_hdl, $start_index, $positioned_count, $positioned_vector, $coverage_vector, $written_count, $fields) = @_;
126              
127             # once we end up with an empty first row, we're finished
128 0 0 0     0 return 0 if ($positioned_count == 0 && $start_index > 8);
129              
130             # the return value signals to the former/upper call whether proceeding with the recursion or to return
131 0         0 my $return_value = 1;
132             # find the next unobstructed cell, starting with the given index
133 0         0 for (my $current_index = $start_index; $current_index <= 80; $current_index++) {
134 0 0       0 next if $coverage_vector->bit_test($current_index);
135              
136             # a free cell was found - put it on the grid
137 0         0 $positioned_vector->Bit_On($current_index);
138 0         0 $positioned_count++;
139              
140 0 0       0 if ($positioned_count == 9) {
141             # with 9 occupied cells the pattern is complete
142 0         0 save_pattern($file_hdl, $positioned_vector);
143 0         0 $$written_count++;
144 0 0 0     0 if ($VERBOSE && ! ($$written_count % 1000)) {
145 0         0 print $$written_count, ": ";
146 0         0 print_grid_from_vector($positioned_vector);
147             }
148              
149             } else {
150 0         0 my $combined_vector = Bit::Vector->new(81);
151 0         0 $combined_vector->Or($coverage_vector, $fields->[$current_index]{covers});
152              
153 0 0       0 $return_value = create_patterns($file_hdl, $current_index+1, $positioned_count, $positioned_vector, $combined_vector, $written_count, $fields)
154             or last;
155             }
156              
157 0         0 $positioned_vector->Bit_Off($current_index);
158 0         0 $positioned_count--;
159             }
160              
161 0         0 return $return_value
162             }
163              
164             sub print_grid_from_vector {
165 0     0 0 0 my ($vector, $symbol) = @_;
166 0 0       0 $symbol = 'X' unless defined($symbol);
167              
168 0         0 my $bits = $vector->to_Bin();
169 0         0 $bits =~ s/0/ /g;
170 0         0 $bits =~ s/1/$symbol/g;
171 0         0 my @bits = reverse split //, $bits;
172              
173 0         0 print "\n-----------------\n";
174 0         0 while (my @row = splice @bits, 0, 9) {
175 0         0 print "@row\n";
176             }
177 0         0 print "-----------------\n";
178             }
179              
180             sub save_pattern {
181 0     0 0 0 my ($Out_File, $vector) = @_;
182              
183             # default string length for 81 bits is 12 bytes on Windows (3 words), 16 bytes on linux (2 words)
184             # we unify by cutting to the minimum
185 0         0 my $buffer = substr($vector->Block_Read(), 0, 11);
186 0         0 my $written = syswrite($Out_File, $buffer);
187 0 0       0 unless ($written == 11) {
188 0         0 die "Unexpected number of $written bytes written (expected 11)\n";
189             }
190             }
191              
192             sub init_fields {
193             # return arrayref with 81 field hashes, field->{covers} has Bit::Vector with constraint coverage field indexes set (row, column and block in one)
194              
195 4     4 0 11 my $offset = 0;
196              
197 4 50       22 unless (%pos_to_offset) {
198 4         18 foreach my $row ('A' .. 'I') {
199 36         98 foreach my $column ('1' .. '9') {
200 324         851 $pos_to_offset{$row . $column} = $offset++;
201             }
202             }
203 4         292 %offset_to_pos = reverse %pos_to_offset;
204             }
205              
206 4         174 my %block_ranges = (
207             A => ['A' .. 'C'],
208             B => ['A' .. 'C'],
209             C => ['A' .. 'C'],
210             D => ['D' .. 'F'],
211             E => ['D' .. 'F'],
212             F => ['D' .. 'F'],
213             G => ['G' .. 'I'],
214             H => ['G' .. 'I'],
215             I => ['G' .. 'I'],
216             1 => ['1' .. '3'],
217             2 => ['1' .. '3'],
218             3 => ['1' .. '3'],
219             4 => ['4' .. '6'],
220             5 => ['4' .. '6'],
221             6 => ['4' .. '6'],
222             7 => ['7' .. '9'],
223             8 => ['7' .. '9'],
224             9 => ['7' .. '9'],
225             );
226              
227 4         12 my @fields = ();
228 4         9 $offset = 0;
229 4         15 foreach my $row ('A' .. 'I') {
230 36         103 foreach my $column ('1' .. '9') {
231 324         926 push @fields, new_sudo_field($row, $column, \%pos_to_offset, \%block_ranges);
232 324         976 $offset++;
233             }
234             }
235              
236             return \@fields
237 4         107 }
238              
239             sub field_coordinates {
240 0     0 0 0 return $offset_to_pos{$_[0]}
241             }
242              
243             {
244             # used in logic mode only
245             my @groups;
246              
247             # returns an array of 27 arrays, each with the 9 indexes for any region (row, column or box)
248             sub build_groups {
249 4     4   10682 use integer;
  4         71  
  4         32  
250              
251 4 50   4 0 29 return \@groups if @groups;
252              
253 4         114 foreach my $i (0..80) {
254 324         537 my $ri = ($i / 9);
255 324         546 push @{$groups[$ri]}, $i;
  324         648  
256 324         607 my $ci = $i % 9 + 9;
257 324         479 push @{$groups[$ci]}, $i;
  324         729  
258 324         612 my $bi = 3 * ($i / 27) + ($i % 9 / 3) + 18;
259 324         505 push @{$groups[$bi]}, $i;
  324         732  
260             }
261              
262             return \@groups
263 4         24 }
264              
265             my @intersections;
266              
267             # returns an array of 54 arrays, each with 3 defining vectors of size 81:
268             # [
269             # [intersection of a box with a row or column],
270             # [excluded subset of same box],
271             # [excluded subset of the row or column]
272             # ]
273             sub get_intersections {
274 4     4   806 use integer;
  4         10  
  4         19  
275              
276 4 50   4 0 37 return \@intersections if @intersections;
277              
278 4 50       29 @groups or build_groups();
279              
280 4         19 foreach my $i (0..8) {
281             # 1 box
282 36         140 my $box_vector = Bit::Vector->new(81);
283 36         72 $box_vector->Index_List_Store(@{$groups[$i + 18]});
  36         144  
284             # 3 intersecting rows
285 36         74 my $row_index = ($i / 3) * 3;
286 36         124 my @rows = @groups[$row_index, $row_index + 1, $row_index + 2];
287             # 3 intersecting columns
288 36         82 my $column_index = ($i * 3 % 9) + 9;
289 36         88 my @columns = @groups[$column_index, $column_index + 1, $column_index + 2];
290              
291 36         85 foreach my $line (@rows, @columns) {
292 216         592 my $line_vector = Bit::Vector->new(81);
293 216         722 $line_vector->Index_List_Store(@$line);
294              
295 216         3697 my $intersection_vector = Bit::Vector->new(81);
296 216         710 $intersection_vector->Intersection($box_vector, $line_vector);
297 216         639 my $negation_vector_1 = Bit::Vector->new(81);
298 216         668 $negation_vector_1->Difference($box_vector, $intersection_vector);
299 216         676 my $negation_vector_2 = Bit::Vector->new(81);
300 216         655 $negation_vector_2->Difference($line_vector, $intersection_vector);
301            
302 216         1151 push @intersections, [$intersection_vector, $negation_vector_1, $negation_vector_2];
303             }
304             }
305              
306             return \@intersections
307 4         24 }
308             }
309              
310             #####################################################
311              
312             sub new_sudo_field {
313 324     324 0 997 my ($row, $col, $position_to_offset, $block_ranges) = @_;
314              
315             return {
316 324         1146 offset => $position_to_offset->{$row . $col},
317             covers => _get_covered_cells_vector($row, $col, $position_to_offset, $block_ranges),
318             candidates => {},
319             value => undef,
320             }
321             }
322              
323             sub _get_covered_cells_vector {
324 324     324   705 my ($row, $col, $position_to_offset, $block_ranges) = @_;
325              
326 324         655 my %conflicts = ();
327 324         1017 foreach my $ri ('A' .. 'I') {
328 2916         8126 $conflicts{$ri . $col} = $position_to_offset->{$ri . $col};
329             }
330 324         787 foreach my $ci ('1' .. '9') {
331 2916         11284 $conflicts{$row . $ci} = $position_to_offset->{$row . $ci};
332             }
333 324         566 foreach my $ri (@{$block_ranges->{$row}}) {
  324         787  
334 972         1554 foreach my $ci (@{$block_ranges->{$col}}) {
  972         2021  
335 2916         7939 $conflicts{$ri . $ci} = $position_to_offset->{$ri . $ci};
336             }
337             }
338              
339 324         1739 my $covered_fields_vector = Bit::Vector->new(81);
340 324         1690 $covered_fields_vector->Index_List_Store(sort {$a <=> $b} values %conflicts);
  22241         36344  
341              
342 324         3708 return $covered_fields_vector
343             }
344              
345             1
346              
347             __END__