File Coverage

lib/Games/Sudoku/Lite.pm
Criterion Covered Total %
statement 306 319 95.9
branch 76 96 79.1
condition 8 13 61.5
subroutine 30 30 100.0
pod 0 5 0.0
total 420 463 90.7


line stmt bran cond sub pod time code
1             # Games::Sudoku::Lite -- Fast and simple Sudoku puzzle solver
2             #
3             # Copyright (C) 2006 Bob O'Neill.
4             # All rights reserved.
5             #
6             # This code is free software; you can redistribute it and/or modify
7             # it under the same terms as Perl itself.
8             #
9              
10             package Games::Sudoku::Lite;
11              
12 6     6   224485 use strict;
  6         15  
  6         217  
13 6     6   35 use warnings;
  6         18  
  6         34363  
14              
15             our $VERSION = '0.41';
16              
17             my %Config = (
18             width => 9,
19             height => 9,
20             square_height => 3,
21             square_width => 3,
22             possible_values => [1..9],
23             DEBUG => 0,
24             );
25             # If we need to brute force a solution, we'll set $Retrying to 1.
26             # Certain warnings about inconsistent puzzle states, sent through
27             # my_warn(), will be skipped.
28             my $Retrying = 0;
29              
30             sub my_warn {
31 4678 50   4678 0 13892 warn @_ if not $Retrying;
32             }
33              
34             sub new {
35 20     20 0 29660 my $class = shift;
36 20         44 my $board = shift;
37 20         43 my $config = shift;
38 20 100 66     210 if (defined $config and ref $config eq 'HASH') {
39 19         101 for (keys %$config) {
40 33         122 $Config{$_} = $config->{$_};
41             }
42             }
43 20         69 my $self = {};
44 20         80 $self->{board} = _txt_to_array($board);
45              
46 20         138 return bless $self, $class;
47             }
48              
49             sub solve {
50 20     20 0 140 my $self = shift;
51              
52 20         84 my $success = $self->_algorithm();
53 20         40 for (2..@{$Config{possible_values}})
  20         70  
54             {
55 28 100       122 last if $success;
56 8         34 $success = $self->_retry($_);
57             }
58              
59 20         100 return $success;
60             }
61              
62             sub _algorithm {
63 106     106   194 my $self = shift;
64              
65             # Accurate naming at the expense of brevity
66 106         340 my $prev_possibilities = $Config{width} * $Config{height} * @{$Config{possible_values}};
  106         305  
67 106         308 my $possibilities_left = $self->_possibilities_left();
68              
69 106   100     571 while ($possibilities_left and $possibilities_left < $prev_possibilities)
70             {
71 524         2459 $self->_row_rule();
72 524         2955 $self->_column_rule();
73 524         2129 $self->_square_rule();
74              
75 524         1140 $prev_possibilities = $possibilities_left;
76 524         3229 $possibilities_left = $self->_possibilities_left();
77 524 50       4343 warn "Possibilities Remaining: $possibilities_left\n" if $Config{DEBUG};
78             }
79              
80             # Clarity at the expense of conciseness
81 106         219 my $success = ($possibilities_left == 0);
82 106         372 return $success;
83             }
84              
85             sub _retry {
86 8     8   13 my $self = shift;
87 8         18 my $limit = shift;
88 8         19 $Retrying = 1;
89              
90             # Start guessing.
91 8         13 my @coords;
92 8         12 my $y = 0;
93             # Make a list of all unknowns
94 8         15 for my $row (@{$self->{board}})
  8         19  
95             {
96 78         93 my $x = 0;
97 78         158 for my $cell (@$row)
98             {
99 792 100       1808 push @coords, [$x, $y] if ref $cell;
100 792         943 $x++;
101             }
102 78         112 $y++;
103             }
104             # For each undetermined point, make each possible guess and re-run
105             # the algorithm. This assumes that the puzzle is solvable with one
106             # particular correct guess and doesn't attempt to make multiple
107             # consecutive guesses.
108 8         35 my $saved_board = _copy($self->{board});
109 8         15 for my $point (@coords)
110             {
111 152         263 my ($x, $y) = @$point;
112              
113 152         278 my $point = $self->{board}[$y][$x];
114 152         156 my @choices;
115 152 50       323 if (ref $point eq 'ARRAY') {
116 152         164 @choices = @{ $point };
  152         387  
117             }
118             else {
119 0         0 @choices = $point;
120             }
121             # Only try the easiest guesses.
122 152 100       432 next unless @choices == $limit;
123              
124 45         98 for my $choice (@choices) {
125             # Make the guess.
126 86         246 $self->{board}[$x][$y] = $choice;
127              
128 86         376 my $success = $self->_algorithm();
129              
130 86 100       486 if ($success) {
131 52         198 my $errors = $self->validate();
132 52 100       140 if ($errors) {
133             # we'll have to guess again...
134 44         124 $self->{board} = _copy($saved_board);
135             }
136             else {
137             # We guessed right.
138 8         7682 return 1;
139             }
140             }
141             else {
142             # we'll have to guess again...
143 34         125 $self->{board} = _copy($saved_board);
144             }
145             }
146             }
147 0         0 $self->{board} = _copy($saved_board);
148             # No more guesses to make :(
149 0         0 return 0;
150             }
151              
152             sub solution {
153 2040     2040 0 2891 my $self = shift;
154 2040         6035 my $x = _array_to_txt($self->{board});
155 2040         6727 return $x;
156             }
157              
158             sub _array_to_txt {
159 2040   50 2040   6002 my $array = shift || [];
160 2040         2914 my $board = '';
161 2040         2943 my $j = 0;
162 2040         4021 for my $row (@$array)
163             {
164 20410         22544 $j++;
165 20410 50       46690 if ($Config{DEBUG})
166             {
167 0         0 for my $r (@$row) {
168             # Make a copy of $r so we don't change $self->{board}
169 0         0 my $string = $r;
170 0 0       0 $string = join '', @$r if ref $r;
171             # Print all remaining possible values for each column
172 0         0 my $width = @{$Config{possible_values}} + 1;
  0         0  
173 0         0 $board .= sprintf "%${width}s", $string;
174             }
175 0         0 $board .= "\n";
176             }
177             else
178             {
179 20410         31492 my $i = 0;
180 20410         32453 for my $r (@$row) {
181 214474         217437 $i++;
182             # Make a copy of $r so we don't change $self->{board}
183 214474         300007 my $string = $r;
184 214474 100       464699 $string = '.' if ref $string;
185 214474         252217 $board .= $string;
186             # Experimenting with another output style.
187 214474 50       534748 if ($Config{DEBUG}) {
188 0 0       0 $board .= '|' unless $i % $Config{square_width};
189             }
190             }
191 20410         39878 $board .= "\n";
192             # Experimenting with another output style.
193 20410 50       50574 if ($Config{DEBUG})
194             {
195 0 0       0 $board .= ('-' x ($Config{width} + $Config{width}/$Config{square_width})). "\n" unless $j % $Config{square_height};
196             }
197             }
198             }
199              
200 2040         7827 return $board;
201             }
202              
203             sub _txt_to_array {
204 20     20   45 my $board = shift;
205 20         30 my @array;
206 20         31 my $i = 0;
207 20         237 for my $line (split /\n/, $board)
208             {
209 190         997 my @row = split //, $line, $Config{width};
210 190         439 for my $i (0..@row-1)
211             {
212 1894         2359 my $cell = $row[$i];
213 1894 100       4918 if ($cell eq '.')
214             {
215 1220         1398 $cell = [@{$Config{possible_values}}];
  1220         7109  
216             }
217 1894         2962 $row[$i] = $cell;
218             }
219 190         706 push @array, [@row];
220              
221 190         223 $i++;
222 190 50       637 warn "ERROR: Too Many Rows in Board" if $i > $Config{height};
223             }
224 20         122 return \@array;
225             }
226              
227             sub _possibilities_left {
228 630     630   1264 my $self = shift;
229 630         909 my $possibilities_left = 0;
230 630         1110 for my $row (@{$self->{board}})
  630         1866  
231             {
232 6317         8905 for my $cell (@$row)
233             {
234 66695 100       137974 $possibilities_left += @$cell if ref $cell;
235             }
236             }
237 630         1500 return $possibilities_left;
238             }
239              
240             sub _row_rule {
241 524     524   791 my $self = shift;
242              
243 524         1305 for my $row_num (1..$Config{height})
244             {
245 5251         13142 my @row = $self->_get_row($row_num);
246 5251         18100 my %homes = _reduce_possibilities(\@row);
247 5251         33592 $self->_set_row($row_num, @row);
248 5251         16030 $self->_send_home(row_num => $row_num, homes => \%homes);
249             }
250 524         1130 return;
251             }
252              
253             sub _column_rule {
254 524     524   1133 my $self = shift;
255              
256 524         1706 for my $column_num (1..$Config{width})
257             {
258 5251         13136 my @column = $self->_get_column($column_num);
259 5251         15263 my %homes = _reduce_possibilities(\@column);
260 5251         25398 $self->_set_column($column_num, @column);
261 5251         14426 $self->_send_home(column_num => $column_num, homes => \%homes);
262             }
263 524         1068 return;
264             }
265              
266             sub _square_rule {
267 524     524   848 my $self = shift;
268              
269 524         1856 my $h_squares = $Config{width} / $Config{square_width};
270 524         1602 my $v_squares = $Config{height} / $Config{square_height};
271 524         1782 my $total_squares = $h_squares * $v_squares;
272              
273 524         1665 for my $square_num (1..$total_squares)
274             {
275 5251         13176 my $square = $self->_get_square($square_num);
276 5251         14185 my %homes = _reduce_possibilities($square);
277 5251         20443 $self->_set_square($square_num, $square);
278 5251         15604 $self->_send_home(square_num => $square_num, homes => \%homes);
279             }
280 524         1111 return;
281             }
282              
283              
284             #
285             # Inputs: A row, column or square of cells
286             # Does: Changes (in-place) the cells by removing from possibilities the
287             # values that are already:
288             # a) solved in this group of cells
289             # b) determined to be elsewhere
290             # Returns: The number of homes available for each digit
291             #
292             sub _reduce_possibilities {
293 15753     15753   24492 my $cells = shift;
294 15753         26919 my @known_values = grep { not ref $_ } @$cells;
  166161         358732  
295              
296             # a)
297 15753         26787 for my $cell (@$cells)
298             {
299 166161 50 33     766495 warn "blank cell?? '$cell'" if not defined $cell or $cell eq '';
300 166161 100       384661 if (not ref $cell) {
301 106803         158319 next;
302             }
303              
304 59358         259657 $cell = [_take_out($cell, [@known_values])];
305 59358 100       237094 my_warn "ERROR: No possibilities left for this cell" unless @$cell;
306 59358 100       172167 $cell = $cell->[0] if @$cell == 1; # Cell is solved.
307             }
308 15753         39849 my %homes = _compute_homes($cells);
309             # b)
310 15753         42759 my %appears;
311 15753         40368 for my $cell (@$cells)
312             {
313             # Skip solved cells.
314 166161 100       375481 next if not ref $cell;
315              
316             # Map values (1..9) to cell contents
317 57330 50       133567 if (ref $cell) {
318 57330         217655 my $values = join '|', sort @$cell;
319 57330         81005 for my $n (@$cell) {
320 182643         996956 $appears{$n}{$values}++;
321             }
322             }
323             }
324 15753         49699 for my $n (keys %appears) {
325 57880         82498 for my $values (keys %{$appears{$n}}) {
  57880         238410  
326              
327 159270         290204 my $appearances = $appears{$n}{$values};
328             # Could be [3,8],[3,8] but no other occurrences of 3 or 8.
329 159270 100       398863 next unless $appearances < $homes{$n};
330              
331 149672         14403766 my @values = split /\|/, $values;
332             # We don't have anything to do unless we see the same set
333             # of values at least twice.
334 149672 50       366887 next unless @values > 1;
335              
336 149672 100       595756 if ($appearances >= @values) {
337 709 100       1896 my_warn "Something's probably wrong ($appearances > ".@values.")"
338             if $appearances > @values;
339             # For example, '3' appears in two cells of two members,
340             # such as [3,8],[3,8].
341             #
342             # Therefore, we can remove '3' from the possibilities for
343             # every other cell in this group.
344             #
345             # We'll remove it from every cell that doesn't match these
346             # values.
347              
348 709         1308 for my $cell (@$cells) {
349 6971 100       15030 next if not ref $cell; # skip solved cells
350              
351 3143         7372 my $my_values = join '|', sort @$cell;
352 3143 100       7332 next if $my_values eq $values; # skip [3,8]
353 1687         2014 my $saved = @$cell;
354 1687         4836 $cell = [_take_out($cell, [$n])]; # [2,3,7] -> [2,7]
355             }
356             }
357             }
358             }
359              
360             # Recompute and return.
361 15753         39856 %homes = _compute_homes($cells);
362 15753         203225 return %homes;
363             }
364              
365             sub _compute_homes
366             {
367 31506     31506   54068 my $cells = shift;
368 31506         40042 my %homes;
369 31506         53896 for my $cell (@$cells)
370             {
371 332322 100       736904 if (not ref $cell) {
372 217662         431524 $homes{$cell}++;
373 217662         356205 next;
374             }
375 114660         526162 $homes{$_}++ for @$cell;
376             }
377 31506         44646622 return %homes;
378             }
379              
380             sub _take_out {
381 61045     61045   75045 my @old = @{shift()};
  61045         227198  
382 61045         79117 my @take_out = @{shift()};
  61045         221373  
383 61045         86293 my @new;
384 61045         94083 for my $o (@old) {
385 203530 100       3284796 push @new, $o unless grep /^$o$/, @take_out;
386             }
387 61045         421307 return @new;
388             }
389              
390             sub _send_home {
391 15753     15753   20735 my $self = shift;
392 15753         41824 my %params = @_;
393 15753         31468 my %homes = %{$params{homes}};
  15753         114799  
394 15753         36632 my $row_num = $params{row_num};
395 15753         22781 my $column_num = $params{column_num};
396 15753         23491 my $square_num = $params{square_num};
397              
398 15753 100       22467 if (not keys %homes == @{$Config{possible_values}})
  15753         48232  
399             {
400 2020         17680 my_warn "ERROR: missing value in ". join('|', keys %homes);
401 2020         22305 my_warn $self->solution();
402             }
403              
404 15753         51344 for my $n (keys %homes) {
405 163957 50       465310 my_warn "ERROR: no home for $n"
406             ." (row=$row_num; column=$column_num; square=$square_num)"
407             unless $homes{$n};
408              
409 163957 100       376168 if ($homes{$n} == 1) {
410 106297 100       410908 if ($row_num) {
    100          
    50          
411 34215         77387 my @row = $self->_get_row($row_num);
412 34215         135297 $self->_find_a_home($n, \@row);
413 34215         85578 $self->_set_row($row_num, @row);
414             }
415             elsif ($column_num) {
416 35699         75538 my @column = $self->_get_column($column_num);
417 35699         113974 $self->_find_a_home($n, \@column);
418 35699         108321 $self->_set_column($column_num, @column);
419             }
420             elsif ($square_num) {
421 36383         93166 my $square = $self->_get_square($square_num);
422 36383         102264 $self->_find_a_home($n, $square);
423 36383         93665 $self->_set_square($square_num, $square);
424             }
425             else {
426 0         0 my_warn "ERROR: missing row_num/column_num/square_num value";
427             }
428             }
429             }
430 15753         144071 return;
431             }
432              
433             sub _find_a_home {
434 106297     106297   198281 my $self = shift;
435 106297         131030 my $n = shift;
436 106297   50     248928 my $cells = shift || [];
437              
438 106297         189151 for my $cell (@$cells)
439             {
440 1193479 100       2638481 next if not ref $cell;
441 303913 100       2597352 if (grep /^$n$/, @$cell)
442             {
443             # Cell is solved.
444 1706         2716 $cell = $n;
445 1706         2983 last;
446             }
447             }
448 106297         218794 return;
449             }
450              
451             sub _get_row {
452 40139     40139   59095 my $self = shift;
453 40139         46072 my $row_num = shift;
454              
455 40139         44306 return @{$self->{board}[$row_num-1]};
  40139         260498  
456             }
457              
458             sub _set_row {
459 39466     39466   52652 my $self = shift;
460 39466         46627 my $row_num = shift;
461 39466         191646 my @row = @_;
462              
463 39466         122066 $self->{board}[$row_num-1] = \@row;
464 39466         230985 return;
465             }
466              
467             sub _get_column {
468 41623     41623   58296 my $self = shift;
469 41623         53894 my $column_num = shift;
470 41623         68233 my @column;
471              
472 41623         72028 for my $row (@{$self->{board}}) {
  41623         102874  
473 465271         839330 push @column, $row->[$column_num-1];
474             }
475              
476 41623         279554 return @column;
477             }
478              
479             sub _set_column {
480 40950     40950   61424 my $self = shift;
481 40950         56275 my $column_num = shift;
482 40950         166135 my @column = @_;
483              
484 40950         52867 my $i = 0;
485 40950         57529 for my $row (@{$self->{board}}) {
  40950         99189  
486 458670         910814 $row->[$column_num-1] = $column[$i++];
487             }
488 40950         200645 return;
489             }
490              
491             sub _get_square {
492 42307     42307   65388 my $self = shift;
493 42307         86939 return $self->_get_or_set_square(@_); # reduces duplication
494             }
495              
496             sub _set_square {
497 41634     41634   54950 my $self = shift;
498 41634         124583 $self->_get_or_set_square(@_); # ditto
499 41634         173404 return;
500             }
501              
502             sub _get_or_set_square {
503 83941     83941   105278 my $self = shift;
504 83941         110662 my $square_num = shift;
505 83941         106351 my $set_square = shift; # Pass a square in to set, otherwise will get
506              
507 83941         178873 my $h_squares = $Config{width} / $Config{square_width};
508 83941         126951 my $v_squares = $Config{height} / $Config{square_height};
509              
510 83941         173338 my $column_num = ($square_num - 1) % $h_squares + 1; # 1, 2, 3
511 83941         175941 my $row_num = _round_up($square_num/$h_squares); # 1, 2, 3
512              
513 83941         205087 my $x_min = ($column_num - 1) * $Config{square_width}; # 0..8
514 83941         188162 my $x_max = $x_min + $Config{square_width} - 1; # 0..8
515 83941         192702 my $y_min = ($row_num - 1) * $Config{square_height}; # 0..8
516 83941         148414 my $y_max = $y_min + $Config{square_height} - 1; # 0..8
517              
518 83941         94263 my @square;
519 83941         157812 for my $y ($y_min..$y_max)
520             {
521 254711         396017 for my $x ($x_min..$x_max)
522             {
523 937499 100       1931552 if ($set_square)
524             {
525 465449         686160 my $next = shift @$set_square;
526 465449         1095780 $self->{board}[$y][$x] = $next;
527             }
528 937499         2534078 push @square, $self->{board}[$y][$x];
529             }
530             }
531              
532 83941         272003 return \@square;
533             }
534              
535             sub _round_up {
536 83941     83941   156453 my $float = shift;
537 83941         105378 my $int_float = int $float;
538 83941 100       155955 if ($int_float == $float) {
539 28377         55526 return $int_float;
540             }
541             else {
542 55564         135223 return $int_float + 1;
543             }
544             }
545              
546             sub validate {
547 71     71 0 121 my $self = shift;
548 71         169 my $errors = '';
549             # validate rows.
550 71         236 for my $row_num (1..$Config{height})
551             {
552 673         2044 my @row = $self->_get_row($row_num);
553 673         2560 $errors .= _validate(\@row, "row $row_num");
554             }
555             # validate columns.
556 71         232 for my $column_num (1..$Config{width})
557             {
558 673         1451 my @column = $self->_get_column($column_num);
559 673         2024 $errors .= _validate(\@column, "column $column_num");
560             }
561             # validate squares.
562 71         225 my $h_squares = $Config{width} / $Config{square_width};
563 71         133 my $v_squares = $Config{height} / $Config{square_height};
564 71         154 my $total_squares = $h_squares * $v_squares;
565 71         162 for my $square_num (1..$total_squares)
566             {
567 673         1378 my $square = $self->_get_square($square_num);
568 673         1781 $errors .= _validate($square, "square $square_num");
569             }
570 71         450 return $errors;
571             }
572              
573             sub _validate {
574 2019     2019   2155 my @cells = @{shift()};
  2019         7081  
575 2019         2728 my $where = shift;
576 2019         2159 my $errors = '';
577 2019         1953 my %seen;
578 2019         13085 $seen{$_}++ for @cells;
579 2019         5970 for (keys %seen)
580             {
581 19265 100       41230 $errors .= "$where: Seen $_ too many times ($seen{$_} times)\n" if $seen{$_} != 1;
582             }
583 2019         3959 for (@{$Config{possible_values}})
  2019         4047  
584             {
585 19803 100       59557 $errors .= "$where: Didn't see $_\n" if not $seen{$_};
586             }
587 2019         11367 return $errors;
588             }
589              
590             sub _copy
591             {
592 5741     5741   6804 my $ref = shift;
593 5741 50       11767 warn "This is for copying references" if not ref $ref;
594              
595 5741 50       266716 if (ref $ref eq 'ARRAY')
596             {
597 5741         22644 my @values = @$ref;
598 5741         6038 my @new_array;
599 5741         7671 for my $value (@values)
600             {
601 26139 100       39205 if (ref $value)
602             {
603 5655         8815 push @new_array, _copy($value);
604             }
605             else
606             {
607 20484         7488909 push @new_array, $value;
608             }
609             }
610 5741         144024 return \@new_array;
611             }
612             }
613              
614             1; # of rings to rule them all.
615              
616             __END__