73cba9a9e89e195932f33df2ceda081ce61cdef6
[digits.git] / digits.pl
1 #!/usr/bin/perl
2 #
3 # Digits 1.0.1
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" . '*** DIGITS 1.0.1 ***' . "\n";
225 print "Homepage: http://jxself.org/digits.shtml\n\n";
226 print "Copyright (C) 2013 Jason Self <j\@jxself.org>\n\n";
227 print "Licensed under the GNU AGPL v3 or any later version.\n\n";
228
229 print "I will think of a number. The object of the game is to guess the number\n";
230 print "in as few tries as possible. Each time you guess I will provide a hint:\n\n";
231
232 print "For each digit in the correct position I will print an X\n";
233 print "For a correct digit with an incorrect position I will print an O\n";
234 print "For a totally incorrect digit I will print an -\n\n";
235
236 print "Let's begin. Here are some questions.\nYou can press return to accept the default setting.\n";
237
238 }
239
240 # Sets the following game state variables by reading command line arguments or 
241 # by prompting the player where required:
242 # 1) Length of combination, stored in $DIGITS
243 # 2) Whether of not the digits in the combination are distinct, stored in $DISTINCT
244 # 3) Number of guesses, stored in $MAX_GUESSES
245 # If the player accepts the defaults then the initial assignment of the above
246 # game state variable are used.
247 sub get_options {
248         my $digits;
249         my $distinct;
250         my $guesses;
251
252         GetOptions (
253                 'digits=i' => \$digits,
254                 'distinct=s' => \$distinct,
255                 'guesses=i' => \$guesses
256         );
257         
258         if (!defined($digits)) {
259                 get_digits();
260         }
261         elsif (is_number($digits)) {
262                 $DIGITS = $digits;
263         }
264         else {
265                 get_digits();
266         }
267         
268         if (!defined($distinct)) {
269                 get_distinct();
270         }
271         elsif (affirmative($distinct)) {
272                 $DISTINCT = $TRUE;
273         }
274         elsif (negative($distinct)) {
275                 $DISTINCT = $FALSE;
276         }
277         else {
278                 get_distinct();
279         }
280         
281         if (!defined($guesses)) {
282                 get_max_guesses();
283         }
284         elsif (is_number($guesses)) {
285                 $MAX_GUESSES = $guesses;
286         }
287         else {
288                 get_max_guesses();
289         }
290         
291         set_game_state();
292 }
293
294 # Checks if the input is a number.
295 sub is_number {
296         my $input = shift;
297         
298         return ($input =~ m/^\d+$/);
299 }
300
301 # Prompts the player to get number of digits or accept default of 3.
302 sub get_digits {
303         print "\nHow long should the combination be? [Default 3, Maximum 8]\n";
304         my $input = read_input();
305         
306         if ($input) {
307                 if (is_number($input)) {
308                         if ($input > $MAX_DIGITS) {
309                                 print "Please enter a number less than 8.\n";
310                                 get_digits();
311                                 return;
312                         }
313                         else {
314                                 $DIGITS = $input;
315                         }
316                 }
317                 else {
318                         print "Please enter a number.\n";
319                         get_digits();
320                         return;
321                 }
322         }
323 }
324
325 # Prompts the player about using distinct digits or not. Default is to use 
326 # distinct digits.
327 sub get_distinct {
328         print "\nShould each digit in the combination be different from the others? [Default Yes]\n";
329         my $input = read_input();
330         
331         if ($input) {
332                 if (affirmative($input)) {
333                         $DISTINCT = $TRUE;
334                 }
335                 elsif (negative($input)) {
336                         $DISTINCT = $FALSE;
337                 }
338                 else {
339                         print "Please enter a Y for yes or a N for No.\n";
340                         get_distinct();
341                         return;
342                 }
343         }
344 }
345
346 # Prompts the player to get number of guesses or accept default of unlimited.
347 sub get_max_guesses {
348         print "\nMaximum number of guesses? [Default unlimited]\n";
349         my $input = read_input();
350         
351         if ($input) {
352                 if (is_number($input)) {
353                         $MAX_GUESSES = $input;
354                 }
355                 else {
356                         print "Please enter a number.\n";
357                         get_max_guesses();
358                         return;
359                 }
360         }
361 }
362
363 # Sets game state for a new game.  Setting new game state includes
364 # 1) Setting whether or not the number has been guessed to $FALSE
365 # 2) Setting number of guesses/tries to 0
366 # 3) Setting the number to be guessed
367 sub set_game_state {
368         my $digit;
369         $guessed = $FALSE;
370         $tries = 0;
371         @digits = ();
372         
373         my $i = 0;
374         while ($i < $DIGITS) {
375                 $digit = int(rand(9));
376                 
377                 if ($DISTINCT) {
378                         my $no_match = $TRUE;
379                         
380                         for (my $j = 0; $j < $#digits + 1; $j++) {
381                                 if ($digits[$j] == $digit) {
382                                         $no_match = $FALSE;
383                                         last;
384                                 }
385                         }
386                                 
387                         if ($no_match) {
388                                 push(@digits, $digit);
389                                 $i++;
390                         }
391                 }
392                 else {
393                         push(@digits, $digit);
394                         $i++;
395                 }
396                 
397         }
398         print "\nOkay, I've got a number...\n";
399 }
400
401 # The main/starting program for the game.
402 sub main {
403         my $guess;
404         
405         display_text();
406         get_options();
407         
408         while ($TRUE) {
409                 print "\nWhat is your guess? Type 'q' or 'quit' to exit.\n";
410                 $guess = read_input();
411                 $guessed = check_digits($guess);
412                 
413                 if ($MAX_GUESSES) {
414                         if ($tries == $MAX_GUESSES) {
415                                 if ($tries == 1) {
416                                         print "\nYou have not guessed my number in 1 guess.\n";
417                                 }
418                                 else {
419                                         print "\nYou have not guessed my number in $MAX_GUESSES guesses.\n";
420                                 }
421                                 
422                                 play_again();
423                         }
424                 }
425                 
426                 if ($guessed == $QUIT) {
427                         print "\nThe number I had was @digits. Bye!\n";
428                         last;
429                 }
430                 elsif ($guessed) {
431                         if ($tries == 1) {
432                                 print "Excellent! You guessed it in only 1 try!\n";
433                         }
434                         elsif ($tries < ($DIGITS * 2)) {
435                                 print "Excellent! You guessed it in only $tries tries!\n";
436                         }
437                         else {
438                                 print "You guessed it in $tries tries!\n";
439                         }
440                         
441                         play_again();
442                 }
443         }
444 }
445
446 main();