File Coverage

blib/lib/Games/Cryptoquote.pm
Criterion Covered Total %
statement 202 241 83.8
branch 45 68 66.1
condition 7 15 46.6
subroutine 16 17 94.1
pod 0 14 0.0
total 270 355 76.0


line stmt bran cond sub pod time code
1             # Cryptoquote.pm -- Solves Cryptoquote puzzles.
2             #
3             # Copyright (C) 1999-2002 Bob O'Neill
4             # All rights reserved.
5             #
6             # Thanks to Adam Foxson for the prodding and know-how that made
7             # it possible for this module to make it to CPAN.
8             #
9             # This program is free software; you can redistribute it and/or
10             # modify it under the terms of the GNU General Public License
11             # as published by the Free Software Foundation; either version 2
12             # of the License, or (at your option) any later version.
13             #
14             # This program is distributed in the hope that it will be useful,
15             # but WITHOUT ANY WARRANTY; without even the implied warranty of
16             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17             # GNU General Public License for more details.
18             #
19              
20             package Games::Cryptoquote;
21              
22 1     1   7842 use strict;
  1         2  
  1         39  
23 1     1   6 use vars qw($VERSION);
  1         2  
  1         48  
24 1     1   6 use Carp;
  1         7  
  1         3311  
25              
26             $VERSION = '1.30';
27             local $^W = 1;
28              
29             sub new
30             {
31 1     1 0 48 my $type = shift;
32 1   33     8 my $class = ref($type) || $type;
33 1         5 return bless {}, $class;
34             }
35              
36             sub quote
37             {
38 3     3 0 15 my $self = shift;
39 3 100       12 $self->{'quote'} = shift if @_;
40 3         16 return $self->{'quote'}
41             }
42              
43             sub source
44             {
45 3     3 0 8 my $self = shift;
46 3 100       12 $self->{'source'} = shift if @_;
47 3         9 return $self->{'source'}
48             }
49              
50             sub timeout
51             {
52 2     2 0 7 my $self = shift;
53 2 100       9 $self->{'timeout'} = shift if @_;
54 2         6 return $self->{'timeout'}
55             }
56              
57             sub build_dictionary
58             {
59 1     1 0 8 my $self = shift;
60              
61 1 50       5 croak "Invalid arguments (expecting even number)" if @_ % 2;
62 1         7 my %options = @_;
63 1         5 my @bad_opts = grep { !/^(?:type|file)$/ } keys %options;
  2         13  
64 1 50       6 croak "Invalid argument(s): (@bad_opts)" if @bad_opts;
65              
66 1         3 my $file = $options{'file'};
67 1         3 my $type = $options{'type'};
68              
69 1 50       7 if ($type eq 'dict')
    50          
70             {
71 0 0       0 if (open DICT, "$file")
72             {
73 0         0 my ( $words, $file );
74 0         0 while ( )
75             {
76 0         0 chomp;
77 0         0 $self->{'dict_patterns'}{&build_pattern($_)}{$_} = 1;
78             }
79 0         0 close DICT;
80 0         0 return 1;
81             }
82             else
83             {
84 0         0 my ($caller, $line) = (caller(0))[1..2];
85 0         0 carp "Couldn't read $file: $! (called by $caller line $line)";
86 0         0 return 0;
87             }
88             }
89             elsif ($type eq 'patterns')
90             {
91 1 50       43 if (open DICT, "$file")
92             {
93 1         2 my ( $words, $pattern );
94 1         49 while ( )
95             {
96 9440         13176 chomp;
97 9440         42051 ($pattern, $words) = split /:/;
98 9440         62756 for ( split /\|/, $words )
99             {
100 45425         332639 $self->{'dict_patterns'}{$pattern}{$_} = 1;
101             }
102             }
103 1         46 close DICT;
104 1         13 return 1;
105             }
106             else
107             {
108 0         0 my ($caller, $line) = (caller(0))[1..2];
109 0         0 carp "Couldn't read $file: $! (called by $caller line $line)";
110 0         0 return 0;
111             }
112             }
113             else
114             {
115 0         0 croak "Invalid dictionary type ($type)";
116             }
117             }
118              
119             sub write_patterns
120             {
121 0     0 0 0 my $self = shift;
122              
123 0 0       0 croak "Invalid arguments (expecting even number)" if @_ % 2;
124 0         0 my %options = @_;
125 0         0 my @bad_opts = grep { !/^(?:dict_file|pattern_file)$/ } keys %options;
  0         0  
126 0 0       0 croak "Invalid argument(s): (@bad_opts)" if @bad_opts;
127              
128 0         0 my $dict_file = $options{'dict_file'};
129 0         0 my $pattern_file = $options{'pattern_file'};
130              
131 0         0 $self->build_dictionary(file => $dict_file, type => 'dict');
132              
133 0         0 my %patterns_hash = %{ $self->{'dict_patterns'} };
  0         0  
134              
135 0 0       0 if (-e $pattern_file)
136             {
137 0         0 carp "$pattern_file exists. I won't overwrite it.\n";
138 0         0 carp "You probably want to remove the call to write_patterns.\n";
139             }
140              
141 0 0       0 open PATTERNS, ">$pattern_file" or croak "Couldn't write $pattern_file: $!";
142 0         0 for my $pattern (sort keys %patterns_hash)
143             {
144 0         0 print PATTERNS "$pattern:";
145              
146 0         0 for my $word (sort keys %{ $patterns_hash{$pattern} })
  0         0  
147             {
148 0         0 print PATTERNS "$word|";
149             }
150              
151 0         0 print PATTERNS "\n";
152             }
153 0         0 close PATTERNS;
154             }
155              
156             sub solve
157             {
158 1     1 0 6 my $self = shift;
159              
160 1   50     3 my $quote = $self->quote() || '';
161 1   50     6 my $source = $self->source() || '';
162 1   50     3 my $timeout = $self->timeout() || 0;
163 1 50       17 croak "Invalid timeout value $timeout" unless $timeout =~ /^\d+$/;
164              
165 1         4 $self->{'let_let'} = ();
166 1         2 $self->{'bad_let_let'} = ();
167              
168             # We will have to do away with the uc() for the quote that's saved
169             # (currently, globalquote or globalquote2).
170 1         3 $self->{'globalquote'} = $quote;
171 1         4 $self->{'globalquote'} = uc $quote;
172              
173 1         12 for ( split /\s/, $self->{'globalquote'} )
174             {
175 12         26 s/[^a-z]//gi;
176 12 50       33 next if exists $self->{'word_word'}{$_};
177 12         26 $self->{'word_word'}{$_} = $self->{'dict_patterns'}{&build_pattern($_)};
178             }
179              
180 1         4 my $last_num_poss = -1;
181 1         11 my $time_in = time;
182 1         3 my $solution = {};
183 1         4 while ( 1 )
184             {
185 4         19 my ($current_num_poss,$quote_soln,$hash_ref) = $self->narrow_possibilities();
186 4 100 66     34 if ( $current_num_poss == $last_num_poss or $current_num_poss == 0 )
187             {
188 1         2 my $source_soln;
189 1 50       13 if ($quote_soln !~ /\|/)
190             {
191 1         6 $quote_soln = &apply_mapping($self->quote(), $hash_ref);
192 1         6 $source_soln = &apply_mapping($self->source(), $hash_ref);
193             }
194 1         82 $self->{'solution'}{'quote'} = $quote_soln;
195 1         5 $self->{'solution'}{'source'} = $source_soln;
196 1         15 return 1;
197             }
198 3         7 $last_num_poss = $current_num_poss;
199              
200 3 50 33     51 if ($timeout and time - $time_in > $timeout)
201             {
202 0         0 $self->{'solution'}{'quote'} = '';
203 0         0 $self->{'solution'}{'source'} = '';
204 0         0 return 0;
205             }
206             }
207             }
208              
209             sub narrow_possibilities
210             {
211 4     4 0 35 my $self = shift;
212 4         12 $self->{'let_word_let'} = {};
213 4         232 $self->{'word_let_let_word'} = {};
214 4         527 $self->{'best_word_word'} = {};
215              
216 4         16 $self->algorithm_one();
217 4         25 $self->algorithm_two();
218 4         24 $self->algorithm_three();
219              
220 4         18 $self->{'word_word'} = {};
221 4         58 $self->{'word_word'} = $self->{'best_word_word'};
222              
223 4         10 my $num_poss = 0;
224 4         6 my $soln = '';
225 4         10 my %good_let_let;
226 4         62 for my $word1 ( split /\s/, $self->{'globalquote'} )
227             {
228 48         102 $word1 =~ s/[^a-z]//gi;
229              
230 48         47 my @temp = keys %{$self->{'best_word_word'}{$word1}};
  48         198  
231 48         127 $soln .= join( '|', (@temp)) . ' ';
232 48 50       100 $soln .= " #$word1#" if $#temp == -1;
233 48         64 $num_poss += $#temp;
234              
235 48         152 my @chars = split('',$word1);
236 48         78 for my $poss (@temp) # <-- possibilities for $word1
237             {
238 128         324 my @poss_chars = split('',$poss);
239 128         229 for my $i (0..$#chars)
240             {
241 597         1313 $good_let_let{$chars[$i]}{$poss_chars[$i]} = 1;
242             }
243             }
244             }
245              
246 4         28 return ($num_poss, $soln, \%good_let_let);
247             }
248              
249             sub algorithm_one
250             {
251 4     4 0 5 my $self = shift;
252 4         8 for my $word1 ( sort {scalar(keys %{$self->{'word_word'}{$a}}) <=> scalar(keys %{$self->{'word_word'}{$b}})} keys %{$self->{'word_word'}} )
  123         120  
  123         191  
  123         466  
  4         42  
253             {
254 48         58 WORD2:for my $word2 ( keys %{$self->{'word_word'}{$word1}} )
  48         3426  
255             {
256 14945         22515 for my $i ( 0..length($word2) - 1)
257             {
258 33306 100       90196 next WORD2 if exists $self->{'bad_let_let'}{substr($word1,$i,1)}{substr($word2,$i,1)};
259             }
260              
261 1036         1779 for my $i ( 0..length($word2) - 1)
262             {
263 5081         6753 my $char1 = substr($word1,$i,1);
264 5081         6313 my $char2 = substr($word2,$i,1);
265              
266 5081         10559 $self->{'let_word_let'}{$char1}{$word1}{$char2} = 1;
267 5081         7638 $self->{'let_let'}{$char1}{$char2} = 1;
268 5081         15326 $self->{'word_let_let_word'}{$word1}{$char1}{$char2}{$word2} = 1;
269             }
270             }
271              
272 48         1142 for my $char1 ( keys %{$self->{'let_let'}} )
  48         233  
273             {
274 880         877 CHAR2:for my $char2 ( keys %{$self->{'let_let'}{$char1}} )
  880         2623  
275             {
276 3851         3460 for my $word1 ( keys %{$self->{'let_word_let'}{$char1}} )
  3851         8668  
277             {
278 4172 100       12618 unless ( exists $self->{'let_word_let'}{$char1}{$word1}{$char2} )
279             {
280 537         1103 $self->{'bad_let_let'}{$char1}{$char2} = 1;
281 537         861 delete $self->{'let_let'}{$char1}{$char2};
282 537         1043 next CHAR2;
283             }
284             }
285             }
286             }
287             }
288             }
289              
290             sub algorithm_two
291             {
292 4     4 0 8 my $self = shift;
293 4         7 my $took_out = 0;
294              
295 4         5 for my $word1 ( keys %{$self->{'word_let_let_word'}} )
  4         24  
296             {
297 48         59 for my $char1 ( keys %{$self->{'word_let_let_word'}{$word1}} )
  48         135  
298             {
299 228         231 my @chars2 = keys %{$self->{'word_let_let_word'}{$word1}{$char1}};
  228         814  
300              
301 228 100       666 if ( $#chars2 == 0 )
302             {
303 183         189 for my $word3 ( keys %{$self->{'word_let_let_word'}} )
  183         587  
304             {
305 2196         2127 for my $char3 ( keys %{$self->{'word_let_let_word'}{$word3}} )
  2196         5531  
306             {
307 10431 100       27741 next unless exists $self->{'word_let_let_word'}{$word3}{$char3}{$chars2[0]};
308 841 100       1333 if ( $char1 eq $char3 )
309             {
310 690 100       666 if ( scalar(keys %{$self->{'word_let_let_word'}{$word3}{$char3}}) > 1 )
  690         2380  
311             {
312 15         35 my $temp = $self->{'word_let_let_word'}{$word3}{$char3}{$chars2[0]};
313 15         25 delete $self->{'word_let_let_word'}{$word3}{$char3};
314 15         40 $self->{'word_let_let_word'}{$word3}{$char3}{$chars2[0]} = $temp;
315 15         266 $took_out++;
316             }
317             }
318             else
319             {
320 151         594 delete $self->{'word_let_let_word'}{$word3}{$char3}{$chars2[0]};
321 151         241 $took_out++;
322             }
323             }
324             }
325             }
326             }
327             }
328             }
329              
330             sub algorithm_three
331             {
332 4     4 0 10 my $self = shift;
333              
334 4         8 for my $word1 ( keys %{$self->{'word_let_let_word'}} )
  4         18  
335             {
336 48         68 my $const1 = ( keys %{$self->{'word_let_let_word'}{$word1}} )[0];
  48         143  
337            
338 48         69 for my $const2 ( keys %{$self->{'word_let_let_word'}{$word1}{$const1}} )
  48         135  
339             {
340 118         123 WORD2:for my $word2 ( keys %{$self->{'word_let_let_word'}{$word1}{$const1}{$const2}} )
  118         514  
341             {
342 589         683 CHAR1:for my $char1 ( keys %{$self->{'word_let_let_word'}{$word1}} )
  589         1513  
343             {
344 1738         1734 for my $char2 ( keys %{$self->{'word_let_let_word'}{$word1}{$char1}} )
  1738         5747  
345             {
346 11529 100       30929 next CHAR1 if exists $self->{'word_let_let_word'}{$word1}{$char1}{$char2}{$word2};
347             }
348 461         1263 next WORD2;
349             }
350 128         520 $self->{'best_word_word'}{$word1}{$word2} = 1;
351             }
352             }
353             }
354             }
355              
356             sub build_pattern
357             {
358 12     12 0 75 my @chars = split '', shift;
359 12         20 my $pattern = '';
360 12         14 my $string = '';
361              
362 12         26 for my $i ( 0..$#chars )
363             {
364 65 100       1196 if ($string =~ /$chars[$i]/)
365             {
366 8         24 $pattern .= (index( $string, $chars[$i] ) + 1) . '|';
367             }
368             else
369             {
370 57         424 $pattern .= ( $i + 1 ) . '|';
371 57         118 $string .= $chars[$i];
372             }
373             }
374              
375 12         18 chop $pattern; # <-- chop trailing '|'
376 12         71 return $pattern;
377             }
378              
379             sub apply_mapping
380             {
381 2     2 0 5 my $ciphertext = shift;
382 2         3 my $mapping_ref = shift;
383              
384 2 50       9 return "No solution.\n" unless ref $mapping_ref eq 'HASH';
385              
386             # Clean up the mapping, reducing us to
387             # one solution, but that's ok for now.
388 2         2 my %mapping;
389 2         12 for my $from (keys %$mapping_ref)
390             {
391 40         39 for my $to (keys %{$mapping_ref->{$from}})
  40         83  
392             {
393 40         56 $mapping{$from} = $to;
394 40         60 last;
395             }
396             }
397              
398 2         6 my @my_chars;
399 2         5 for my $i (0..length($ciphertext))
400             {
401 90         115 my $crypt_char = substr($ciphertext,$i,1);
402              
403 90 100       215 if ($crypt_char =~ /^[a-z]$/)
    100          
404             {
405 70         107 $my_chars[$i] = '%';
406             }
407             elsif ($crypt_char =~ /^[A-Z]$/)
408             {
409 4         9 $my_chars[$i] = '#';
410             }
411             else
412             {
413 16         29 $my_chars[$i] = $crypt_char;
414             }
415             }
416 2         11 my $plaintext = join('',@my_chars);
417              
418 2         11 for my $letter (keys %mapping)
419             {
420 40 50       96 next unless $mapping{$letter};
421              
422 40         37 my @my_locations;
423 40         60 for my $j (0..length($ciphertext))
424             {
425 1800 100       6153 if (substr($ciphertext,$j,1) =~ /^$letter$/i)
426             {
427 74         128 push @my_locations, $j;
428             }
429             }
430 40         493 my @new_chars = split('',$plaintext);
431 40         149 for my $i (0..$#my_locations)
432             {
433 74         87 my $index = $my_locations[$i];
434              
435 74 100       126 if ($new_chars[$index] eq '#')
436             {
437             # It's uppercase.
438 4         12 $new_chars[$index] = $mapping{$letter};
439             }
440             else
441             {
442             # It's lowercase, so keep it that way.
443 70         144 $new_chars[$index] = lc $mapping{$letter};
444             }
445             }
446 40         302 $plaintext = join('',@new_chars);
447             }
448              
449 2         24 return $plaintext;
450             }
451              
452             sub get_solution
453             {
454 2     2 0 10 my $self = shift;
455 2         4 my $type = shift;
456 2 50       15 croak "Invalid solution type: $type" unless $type =~ /^(quote|source)$/;
457              
458 2         16 return $self->{'solution'}{$type};
459             }
460              
461             1; # because Perl is number one.
462              
463             __END__