41f376f004103d891d56196c3567bd4cc5a42e41
[digits.git] / digits.pl
1 #!/usr/bin/perl
2 #
3 # Digits 1.0
4 #
5 # Copyright (C) 2013 Jason Self <j@jxself.org>
6 #
7 # This software's license gives you freedom; you can modify, propagate,
8 # and/or convey it under the terms of the GNU Affero General Public
9 # License as published by the Free Software Foundation (FSF); either 
10 # version 3 of the License, or (at your option) any later version 
11 # published by the FSF.
12 #
13 # This program is distributed in the hope that it will be useful, but WITHOUT
14 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
15 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
16 # for more details.
17 #
18 # You should have received a copy of the GNU Affero General Public License 
19 # along with this program in a file called 'AGPLv3.txt'. If not, see
20 # http://www.gnu.org/licenses/agpl-3.0-standalone.html or write to the: Free
21 # Software Foundation, Inc., 51 Franklin St, Fifth Floor Boston, MA 
22 # 02110-1301, USA.
23
24 use strict;
25 use warnings;
26 use Getopt::Long;
27
28 # Constants
29 my $QUIT = -1;
30 my $TRUE = 1;
31 my $FALSE = 0;
32 my $MAX_DIGITS = 8;
33
34 # Game Setup
35 my $DIGITS = 3;
36 my $DISTINCT = $TRUE;
37 my $MAX_GUESSES = 0;
38
39 # Game State
40 my $guessed;
41 my $tries;
42 my @digits;
43
44 # Checks that the guess is a number and has the same number of digits as
45 # specified by $DIGITS.
46 sub is_correct_format {
47         my $guess = shift;
48         
49         return ($guess =~ m/^\d{$DIGITS}$/);
50 }
51
52 # Checks that the guess is made up of distinct digits if $DISTINCT is 1.
53 sub is_distinct {
54         my $array_ref = shift;
55         my @guess_digits = @{$array_ref};
56         
57         for (my $i = 0; $i < $DIGITS; $i++) {
58                 for (my $j = $i+1; $j < $DIGITS; $j++) {
59                         if ($guess_digits[$i] eq $guess_digits[$j]) {
60                                 return $FALSE || !$DISTINCT;
61                         }
62                 }
63         }
64         
65         return $TRUE;
66 }
67
68 # This subroutine
69 # 1) checks that the digits entered are valid
70 # 2) checks the guess against the number to be guessed and prints hints.
71 sub check_digits {
72         my $guess = shift;
73         my $guessed = $TRUE;
74         
75         if (!defined($guess)) {
76                 return $FALSE;
77         }
78         elsif ($guess eq 'q' || $guess eq 'quit') {
79                 return $QUIT;
80         }
81         elsif (!is_correct_format($guess)) {
82                 print "Type $DIGITS digits please.\n";
83                 return $FALSE;
84         }
85         
86         my @guess_digits = ();
87         my $digit;
88         
89         for (my $i = 0; $i < $DIGITS; $i++) {
90                 $digit = chop($guess);
91                 unshift(@guess_digits, $digit);
92         }
93         
94         if (!is_distinct(\@guess_digits)) {
95                 print "Type $DIGITS different digits please.\n";
96                 return $FALSE;
97         }
98         
99         my $hint = '';
100         for (my $i = 0; $i < $DIGITS; $i++) {
101                 if ($guess_digits[$i] eq $digits[$i]) {
102                         $hint = $hint . 'X';
103                 }
104                 else {
105                         $guessed = $FALSE;
106                         my $no_match = $TRUE;
107                         
108                         for (my $j = 0; $j < $DIGITS; $j++) {
109                                 if ($guess_digits[$i] eq $digits[$j]) {
110                                         $hint = $hint . 'O';
111                                         $no_match = $FALSE;
112                                         last;
113                                 }
114                         }
115                         
116                         if ($no_match) {
117                                 $hint = $hint . '-';
118                         }
119                 }
120         }
121         
122         if ($hint) {
123                 print "For your guess of @guess_digits, I hint $hint.\n";
124                 $tries++;
125                 return $guessed;
126         }
127 }
128
129 # Reads a line of user input.
130 sub read_input {
131         my $input;
132         
133         $input = <STDIN>;
134         
135         if ($input) {
136                 chomp $input;
137         }
138         # Required for a graceful exit when user hits ^C.
139         else {
140                 exit;
141         }
142         
143         return $input;
144 }
145
146 # Contains various representations of 'Yes' used to parse user input.
147 sub affirmative {
148         my $input = shift;
149         
150         if ($input eq 'y') {
151                 return $TRUE;
152         }
153         elsif ($input eq 'Y') {
154                 return $TRUE;
155         }
156         elsif ($input eq 'yes') {
157                 return $TRUE;
158         }
159         elsif ($input eq 'Yes') {
160                 return $TRUE;
161         }
162         elsif ($input eq 'YES') {
163                 return $TRUE;
164         }
165         else {
166                 return $FALSE;
167         }
168 }
169
170 # Contains various representations of 'No' used to parse user input.
171 sub negative {
172         my $input = shift;
173         
174         if ($input eq 'n') {
175                 return $TRUE;
176         }
177         elsif ($input eq 'N') {
178                 return $TRUE;
179         }
180         elsif ($input eq 'no') {
181                 return $TRUE;
182         }
183         elsif ($input eq 'No') {
184                 return $TRUE;
185         }
186         elsif ($input eq 'NO') {
187                 return $TRUE;
188         }
189         else {
190                 return $FALSE;
191         }
192 }
193
194 # Asks player if they would like to play again.
195 sub play_again {
196         my $input;
197         my $recursion = shift;
198         
199         if ($recursion) {
200                 print "\nDo you want to play again? Please type a Y for yes or a N for No?\n";
201         }
202         else {
203                 print "\nDo you want to play again?\n";
204         }
205         
206         $input = read_input();
207         
208         if (affirmative($input)) {
209                 set_game_state();
210         }
211         elsif (negative($input)) {
212                 exit;
213         }
214         else {
215                 play_again(1);
216         }
217 }
218
219 # Clears screen and prints game title.
220 sub display_text {
221         $| = 1;
222         print "\033[2J";
223         print "\033[0;0H";
224     print "\t\t\t\t" . '*** DIGITS ***' . "\n\n";
225 print "Homepage: http://jxself.org/digits.shtml\n\n";
226 print "Copyright (C) 2013 Jason Self <j\@jxself.org>\n\n";
227
228 print "I will think of a number. The object of the game it to guess the number\n";
229 print "in as few tries as possible. Each time you guess I will provide a hint:\n\n";
230
231 print "For each digit in the correct position I will print an X\n";
232 print "For a correct digit with an incorrect position I will print an O\n";
233 print "For a totally incorrect digit I will print an -\n\n";
234
235 print "Let's begin. Here are some questions.\nYou can press return to accept the default setting.\n";
236
237 }
238
239 # Sets the following game state variables by reading command line arguments or 
240 # by prompting the player where required:
241 # 1) Length of combination, stored in $DIGITS
242 # 2) Whether of not the digits in the combination are distinct, stored in $DISTINCT
243 # 3) Number of guesses, stored in $MAX_GUESSES
244 # If the player accepts the defaults then the initial assignment of the above
245 # game state variable are used.
246 sub get_options {
247         my $digits;
248         my $distinct;
249         my $guesses;
250
251         GetOptions (
252                 'digits=i' => \$digits,
253                 'distinct=s' => \$distinct,
254                 'guesses=i' => \$guesses
255         );
256         
257         if (!defined($digits)) {
258                 get_digits();
259         }
260         elsif (is_number($digits)) {
261                 $DIGITS = $digits;
262         }
263         else {
264                 get_digits();
265         }
266         
267         if (!defined($distinct)) {
268                 get_distinct();
269         }
270         elsif (affirmative($distinct)) {
271                 $DISTINCT = $TRUE;
272         }
273         elsif (negative($distinct)) {
274                 $DISTINCT = $FALSE;
275         }
276         else {
277                 get_distinct();
278         }
279         
280         if (!defined($guesses)) {
281                 get_max_guesses();
282         }
283         elsif (is_number($guesses)) {
284                 $MAX_GUESSES = $guesses;
285         }
286         else {
287                 get_max_guesses();
288         }
289         
290         set_game_state();
291 }
292
293 # Checks if the input is a number.
294 sub is_number {
295         my $input = shift;
296         
297         return ($input =~ m/^\d+$/);
298 }
299
300 # Prompts the player to get number of digits or accept default of 3.
301 sub get_digits {
302         print "\nHow long should the combination be? [Default 3, Maximum 8]\n";
303         my $input = read_input();
304         
305         if ($input) {
306                 if (is_number($input)) {
307                         if ($input > $MAX_DIGITS) {
308                                 print "Please enter a number less than 8.\n";
309                                 get_digits();
310                                 return;
311                         }
312                         else {
313                                 $DIGITS = $input;
314                         }
315                 }
316                 else {
317                         print "Please enter a number.\n";
318                         get_digits();
319                         return;
320                 }
321         }
322 }
323
324 # Prompts the player about using distinct digits or not. Default is to use 
325 # distinct digits.
326 sub get_distinct {
327         print "\nShould each digit in the combination be different from the others? [Default Yes]\n";
328         my $input = read_input();
329         
330         if ($input) {
331                 if (affirmative($input)) {
332                         $DISTINCT = $TRUE;
333                 }
334                 elsif (negative($input)) {
335                         $DISTINCT = $FALSE;
336                 }
337                 else {
338                         print "Please enter a Y for yes or a N for No.\n";
339                         get_distinct();
340                         return;
341                 }
342         }
343 }
344
345 # Prompts the player to get number of guesses or accept default of unlimited.
346 sub get_max_guesses {
347         print "\nMaximum number of guesses? [Default unlimited]\n";
348         my $input = read_input();
349         
350         if ($input) {
351                 if (is_number($input)) {
352                         $MAX_GUESSES = $input;
353                 }
354                 else {
355                         print "Please enter a number.\n";
356                         get_max_guesses();
357                         return;
358                 }
359         }
360 }
361
362 # Sets game state for a new game.  Setting new game state includes
363 # 1) Setting whether or not the number has been guessed to $FALSE
364 # 2) Setting number of guesses/tries to 0
365 # 3) Setting the number to be guessed
366 sub set_game_state {
367         my $digit;
368         $guessed = $FALSE;
369         $tries = 0;
370         @digits = ();
371         
372         my $i = 0;
373         while ($i < $DIGITS) {
374                 $digit = int(rand(9));
375                 
376                 if ($DISTINCT) {
377                         my $no_match = $TRUE;
378                         
379                         for (my $j = 0; $j < $#digits; $j++) {
380                                 if ($digits[$j] == $digit) {
381                                         $no_match = $FALSE;
382                                         last;
383                                 }
384                         }
385                                 
386                         if ($no_match) {
387                                 push(@digits, $digit);
388                                 $i++;
389                         }
390                 }
391                 else {
392                         push(@digits, $digit);
393                         $i++;
394                 }
395                 
396         }
397         print "\nOkay, I've got a number...\n";
398 }
399
400 # The main/starting program for the game.
401 sub main {
402         my $guess;
403         
404         display_text();
405         get_options();
406         
407         while ($TRUE) {
408                 print "\nWhat is your guess?\n";
409                 $guess = read_input();
410                 $guessed = check_digits($guess);
411                 
412                 if ($MAX_GUESSES) {
413                         if ($tries == $MAX_GUESSES) {
414                                 if ($tries == 1) {
415                                         print "\nYou have not guessed my number in 1 guess.\n";
416                                 }
417                                 else {
418                                         print "\nYou have not guessed my number in $MAX_GUESSES guesses.\n";
419                                 }
420                                 
421                                 play_again();
422                         }
423                 }
424                 
425                 if ($guessed == $QUIT) {
426                         last;
427                 }
428                 elsif ($guessed) {
429                         if ($tries == 1) {
430                                 print "Excellent! You guessed it in only 1 try!\n";
431                         }
432                         elsif ($tries < ($DIGITS * 2)) {
433                                 print "Excellent! You guessed it in only $tries tries!\n";
434                         }
435                         else {
436                                 print "You guessed it in $tries tries!\n";
437                         }
438                         
439                         play_again();
440                 }
441         }
442 }
443
444 main();