File Coverage

blib/lib/App/Greple/wordle.pm
Criterion Covered Total %
statement 44 164 26.8
branch 0 74 0.0
condition 0 3 0.0
subroutine 15 32 46.8
pod 0 15 0.0
total 59 288 20.4


line stmt bran cond sub pod time code
1             package App::Greple::wordle;
2 1     1   351898 use v5.14;
  1         5  
3 1     1   6 use warnings;
  1         3  
  1         78  
4 1     1   737 use utf8;
  1         365  
  1         7  
5              
6             our $VERSION = "0.13";
7              
8 1     1   775 use Data::Dumper;
  1         10726  
  1         104  
9 1     1   9 use List::Util qw(shuffle max);
  1         2  
  1         77  
10 1     1   742 use Try::Tiny;
  1         3071  
  1         104  
11 1     1   622 use Getopt::EX::Colormap qw(colorize ansi_code);
  1         101846  
  1         125  
12 1     1   674 use Text::VisualWidth::PP 0.05 'vwidth';
  1         5456  
  1         89  
13 1     1   604 use App::Greple::wordle::game;
  1         5  
  1         54  
14 1     1   722 use App::Greple::wordle::util qw();
  1         5  
  1         34  
15              
16 1     1   836 use Getopt::EX::Hashed; {
  1         10682  
  1         11  
17             Getopt::EX::Hashed->configure( DEFAULT => [ is => 'rw' ] );
18             has data => ' =s ' , default => 'ORIGINAL' ;
19             has answer => ' =s ' , default => $ENV{WORDLE_ANSWER} ;
20             has index => ' n =i ' , default => $ENV{WORDLE_INDEX} ;
21             has trial => ' x =i ' , default => 6 ;
22             has total => ' =i ' , default => 30 ;
23             has random => ' ! ' , default => 0 ;
24             has series => ' s =i ' , default => 1 ;
25             has compat => ' ' , action => sub { $_->series = 0 } ;
26             has keymap => ' ! ' , default => 1 ;
27             has result => ' ! ' , default => 1 ;
28             has correct => ' =s ' , default => "\N{U+1F389}" ; # PARTY POPPER
29             has wrong => ' =s ' , default => "\N{U+1F4A5}" ; # COLLISION SYMBOL
30             has debug => ' ! ' ;
31             }
32 1     1   401 no Getopt::EX::Hashed;
  1         2  
  1         4  
33              
34             sub parseopt {
35 0     0 0   my $app = shift;
36 0           my $argv = shift;
37 1     1   930 use Getopt::Long qw(GetOptionsFromArray Configure);
  1         16221  
  1         6  
38 0           Configure qw(bundling no_getopt_compat pass_through);
39 0 0         $app->getopt($argv) || die "Option parse error.\n";
40 0           $app;
41             }
42              
43             sub _days {
44 1     1   1205 use Date::Calc qw(Delta_Days);
  1         10049  
  1         352  
45 0     0     my($mday, $mon, $year, $yday) = (localtime(time))[3,4,5,7];
46 0           Delta_Days(2021, 6, 19, $year + 1900, $mon + 1, $mday);
47             }
48              
49             my(@word_all, %word_all, @word_hidden);
50              
51             sub setup {
52 0     0 0   my $app = shift;
53 0           my $pkg = __PACKAGE__ . '::' . uc($app->data);
54 0           eval "use $pkg";
55 0 0         if ($@) {
56 0 0         die "$app->{data}: no such data set\n" if $@ =~ /Can't locate/;
57 0           die $@;
58             } else {
59 1     1   9 no strict 'refs';
  1         32  
  1         2911  
60 0           @word_all = @{"$pkg\::WORDS"};
  0            
61 0           @word_hidden = @{"$pkg\::HIDDEN"};
  0            
62             }
63 0           $word_all{$_} = 1 for @word_all;
64 0           for ($app->index) {
65 0 0         $_ = int rand @word_hidden if $app->random;
66 0   0       $_ //= _days;
67 0 0         $_ += _days if /^[-+]/;
68             }
69 0 0         if (my $answer = $app->answer) {
70 0           $app->index = undef;
71 0 0         $word_all{$answer} or die "$answer: wrong word\n";
72             } else {
73 0 0         if ($app->series > 0) {
74 0           srand($app->series);
75 0           @word_hidden = shuffle @word_hidden;
76             }
77 0 0         if ($app->index > $#word_hidden) {
78 0           warn sprintf "no data for %d, so pick a random answer from past data\n", $app->index;
79 0           srand($app->series);
80 0           $app->index = int rand @word_hidden;
81             }
82 0           $app->answer = $word_hidden[ $app->index ];
83             }
84             }
85              
86             sub patterns {
87 0     0 0   my $app = shift;
88 0           my $answer = $app->answer;
89             my @re = map
90 0           { sprintf "(?<=^.{%d})%s", $_, substr($answer, $_, 1) }
  0            
91             0 .. length($answer) - 1;
92 0           my $green = join '|', @re;
93 0           my $yellow = "[$answer]";
94 0           my $black = "(?=[a-z])[^$answer]";
95 0           map { ( '--re' => $_ ) } $green, $yellow, $black;
  0            
96             }
97              
98             sub title {
99 0     0 0   my $app = shift;
100 0           my $label = 'Greple::wordle';
101 0 0         return $label if not defined $app->index;
102 0 0         sprintf('%s %s%s',
103             $label,
104             $app->series == 0 ? '' : sprintf("%d-", $app->series),
105             $app->index);
106             }
107              
108             ######################################################################
109              
110             my $app = __PACKAGE__->new or die;
111             my $game;
112             my $interactive;
113              
114             sub prompt {
115 0     0 0   sprintf '%d: ', $game->attempt + 1;
116             }
117              
118             sub initialize {
119 0     0 0   my($mod, $argv) = @_;
120 0           $app->parseopt($argv)->setup;
121 0           $game = App::Greple::wordle::game->new(answer => $app->answer);
122 0           push @$argv, $app->patterns;
123 0 0         if ($interactive = -t STDIN) {
124 0           push @$argv, '--interactive', ('/dev/stdin') x $app->total;
125 0           select->autoflush;
126 0           say $app->title;
127 0           print prompt();
128             }
129             }
130              
131             sub respond {
132 0     0 0   local $_ = $_;
133 0           my $chomped = chomp;
134 0 0         print ansi_code("{CHA}{CUU}") if $chomped;
135 0           print ansi_code(sprintf("{CHA(%d)}",
136             max(11, vwidth($_) + length(prompt()) + 2)));
137 0           print s/(?<=.)\z/\n/r for @_;
138             }
139              
140             sub show_answer {
141 0     0 0   say colorize('#6aaa64', uc $game->answer);
142             }
143              
144             sub show_result {
145 0     0 0   printf "\n%s %d/%d\n\n", $app->title, $game->attempt, $app->trial;
146 0           say $game->result;
147             }
148              
149             sub check {
150 0     0 0   my $word = lc s/\n//r;
151 0 0         if (not $word_all{$word}) {
152 0 0         command($word) or respond $app->wrong;
153 0           $_ = '';
154             } else {
155 0           $game->try($word);
156             }
157             }
158              
159             sub command {
160 0     0 0   my $word = shift;
161 0 0         my @cmd = split ' ', $word or return;
162 0           my @word = @word_all;
163 0           state @remember;
164 0 0         $cmd[0] =~ /^u(niq)?$/ and unshift @cmd, 'hint';
165              
166 0           while (@cmd) {
167 0           local $_ = shift @cmd;
168             try {
169 0 0   0     if ($_ eq '|') {}
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
170             elsif (/^d$/) {
171 0           $app->debug ^= 1;
172 0 0         printf 'Debug %s', $app->debug ? 'on' : 'off';
173 0           return;
174             }
175 0           elsif (/^\?$/) { help(); return }
  0            
176 0           elsif (/^!!$/) { @word = @remember }
177 0           elsif (/^h(int)?$/) { @word = choose($game->hint, @word) }
178 0           elsif (/^u(niq)?$/) { @word = grep { !/(.).*\1/i } @word }
  0            
179 0           elsif (/^=(.+)/) { @word = choose(includes($1), @word) }
180 0           elsif (/^!(.+)/) { @word = choose("^(?!.*[$1])", @word) }
181 0           elsif (/\W/) { @word = choose($_, @word); }
182 0           else { return }
183 0           1;
184 0 0         } or do {
185 0 0         warn "ERROR: $_" if $app->debug;
186 0 0         return /^[a-z]+$/i ? 0 : 1;
187             };
188             }
189 0 0         if (@word == 0) {
190 0           warn "No match\n";
191 0           return 1;
192             }
193 0           @remember = @word;
194 0           do {
195 0           local $, = ' ';
196 0           say $game->hint_color(@word);
197             };
198 0           1;
199             }
200              
201             sub help {
202 0     0 0   my $message = << " END";
203             # d debug
204             ? help
205             h show hint
206             u uniq
207             !! repeat last result
208             =<str> include characters
209             !<str> exclude characters
210             END
211 0           print $message =~ s/^\s*(#.*)\n//gr;
212             }
213              
214             sub includes {
215 0     0 0   '^' . join '', map { "(?=.*$_)" } $_[0] =~ /./g;
  0            
216             }
217              
218             sub choose {
219 0     0 0   my $p = shift;
220 0           $p =~ s/([A-Z])/[^$1]/g;
221 0 0         warn "> $p\n" if $app->debug;
222 0           grep /$p/, @_;
223             }
224              
225             sub inspect {
226 0 0   0 0   if ($game->solved) {
227 0           respond $app->correct x ($app->trial - $game->attempt + 1);
228 0 0         show_result if $app->result;
229 0           exit 0;
230             }
231 0 0         if (length) {
232 0 0         if ($game->attempt >= $app->trial) {
233 0           show_answer;
234 0           exit 1;
235             }
236 0 0         $app->keymap and respond $game->keymap;
237             }
238 0           print prompt();
239             }
240              
241             1;
242              
243             __DATA__
244              
245             mode function
246              
247             define GREEN #6aaa64
248             define YELLOW #c9b458
249             define BLACK #787c7e
250              
251             option default \
252             -i --need 1 --no-filename \
253             --cm 555/GREEN \
254             --cm 555/YELLOW \
255             --cm 555/BLACK
256              
257             # --interactive is set in initialize() when stdin is a tty
258              
259             option --interactive \
260             --if 'head -1' \
261             --begin __PACKAGE__::check \
262             --end __PACKAGE__::inspect \
263             --epilogue __PACKAGE__::show_answer