# Guessing Game

I firmly believe that writing your own simple games is a terrific way to learn how to program. Let's write a simple program where the user has to guess a random number.

``````\$ ./guess.py
 Guess a number between 1 and 50 (q to quit): 25
You guessed "25"
Too high.
 Guess a number between 1 and 50 (q to quit): 12
You guessed "12"
Too low.
 Guess a number between 1 and 50 (q to quit): 20
You guessed "20"
Too high.
 Guess a number between 1 and 50 (q to quit): 17
You guessed "17"
Too low.
 Guess a number between 1 and 50 (q to quit): 18
You guessed "18"
Too many guesses! The number was "19."
``````

To start, we'll use the "new_py.py" script to stub out the boilerplate. I'll use the `-a` flag to indicate that I want the program to use the `argparse` module so we can accept some named arguments to our script:

``````\$ new_py.py -a guess
Done, see new script "guess.py."
\$ cat -n guess.py
1    #!/usr/bin/env python3
2    """docstring"""
3
4    import argparse
5    import sys
6
7    # --------------------------------------------------
8    def get_args():
9        """get args"""
10        parser = argparse.ArgumentParser(description='Argparse Python script')
11        parser.add_argument('positional', metavar='str', help='A positional argument')
12        parser.add_argument('-a', '--arg', help='A named string argument',
13                            metavar='str', type=str, default='')
14        parser.add_argument('-i', '--int', help='A named integer argument',
15                            metavar='int', type=int, default=0)
16        parser.add_argument('-f', '--flag', help='A boolean flag',
17                            action='store_true')
18        return parser.parse_args()
19
20    # --------------------------------------------------
21    def main():
22        """main"""
23        args = get_args()
24        str_arg = args.arg
25        int_arg = args.int
26        flag_arg = args.flag
27        pos_arg = args.positional
28
29        print('str_arg = "{}"'.format(str_arg))
30        print('int_arg = "{}"'.format(int_arg))
31        print('flag_arg = "{}"'.format(flag_arg))
32        print('positional = "{}"'.format(pos_arg))
33
34    # --------------------------------------------------
35    if __name__ == '__main__':
36        main()
``````

The template shows how to create named arguments for strings, integers, Booleans, as well as positional (unnamed) values. For this program, we want to know the min/max numbers to guess and the number of guesses allowed. We will provide reasonable defaults for all of them so that they will be completely optional. The thing I like best about this step is that it makes me think carefully about what I expect from the user. Change your program to this:

``````def get_args():
"""get args"""
parser = argparse.ArgumentParser(description='Number guessing game')
metavar='int', type=int, default=1)
metavar='int', type=int, default=50)
metavar='int', type=int, default=5)
return parser.parse_args()
``````

Now in the `main` we will need to unpack the input:

``````def main():
"""main"""
args = get_args()
low = args.min
high = args.max
guesses_allowed = args.guesses
``````

Alway assume you get garbage from the user, so let's check the input:

``````    if low < 1:
print('--min cannot be lower than 1')
sys.exit(1)

if guesses_allowed < 1:
print('--guesses cannot be lower than 1')
sys.exit(1)

if low > high:
print('--min "{}" is higher than --max "{}"'.format(low, high))
sys.exit(1)
``````

The next thing we need is a random number between `--min` and `--max` for the user to guess. We can `import random` to do:

``````secret = random.randint(low, high)
``````

The meat of the program will be an infinite loop where we keep asking the user:

``````prompt = 'Guess a number between {} and {} (q to quit): '.format(low, high)
``````

Before we enter that loop, we'll need a variable to keep track of the number of guesses the user has made:

``````num_guesses = 0
``````

The beginning of the play loop looks like this:

``````    while True:
guess = input('[{}] {}'.format(num_guesses, prompt))
num_guesses += 1
``````

Here I want the user to know how many guesses they've made so far. We want to give them a way out, so they can enter "q" to quit:

``````        if guess == 'q':
print('Now you will never know the answer.')
sys.exit(0)
``````

The input from the user will be a string, and we are going to need to convert it to an integer to see if it is the secret number. Before we do that, we must check that it is a digit:

``````        if not guess.isdigit():
print('"{}" is not a number'.format(guess))
continue
``````

If it's not a digit, we `continue` to go to the next iteration of the loop. If we move ahead, then it's OK to convert the guess:

``````        print('You guessed "{}"'.format(guess))
num = int(guess)
``````

Now we need to determine if the user has guessed too many times, if the number if too high or low, or if they've won the game:

``````
if num_guesses >= guesses_allowed:
print('Too many guesses! The number was "{}."'.format(secret))
sys.exit()
elif num < low or num > high:
print('Number is not in the allowed range')
elif num == secret:
print('You win!')
break
elif num < secret:
print('Too low.')
else:
print('Too high.')
``````

The final version looks like this:

``````\$ cat -n guess.py
1    #!/usr/bin/env python3
2    """guess the number game"""
3
4    import argparse
5    import random
6    import sys
7
8    # --------------------------------------------------
9    def get_args():
10        """get args"""
11        parser = argparse.ArgumentParser(description='Number guessing game')
13                            metavar='int', type=int, default=1)
15                            metavar='int', type=int, default=50)
16        parser.add_argument('-g', '--guesses', help='Number of guesses',
17                            metavar='int', type=int, default=5)
18        return parser.parse_args()
19
20    # --------------------------------------------------
21    def main():
22        """main"""
23        args = get_args()
24        low = args.min
25        high = args.max
26        guesses_allowed = args.guesses
27
28        if low < 1:
29            print('--min cannot be lower than 1')
30            sys.exit(1)
31
32        if guesses_allowed < 1:
33            print('--guesses cannot be lower than 1')
34            sys.exit(1)
35
36        if low > high:
37            print('--min "{}" is higher than --max "{}"'.format(low, high))
38            sys.exit(1)
39
40        secret = random.randint(low, high)
41        num_guesses = 0
42        prompt = 'Guess a number between {} and {} (q to quit): '.format(low, high)
43
44        while True:
45            guess = input('[{}] {}'.format(num_guesses, prompt))
46            num_guesses += 1
47
48            if guess == 'q':
49                print('Now you will never know the answer.')
50                sys.exit(0)
51
52            if not guess.isdigit():
53                print('"{}" is not a number'.format(guess))
54                continue
55
56            print('You guessed "{}"'.format(guess))
57            num = int(guess)
58
59            if num_guesses >= guesses_allowed:
60                print('Too many guesses! The number was "{}."'.format(secret))
61                sys.exit()
62            elif num < low or num > high:
63                print('Number is not in the allowed range')
64            elif num == secret:
65                print('You win!')
66                break
67            elif num < secret:
68                print('Too low.')
69            else:
70                print('Too high.')
71
72    # --------------------------------------------------
73    if __name__ == '__main__':
74        main()
``````

# Hangman

Here is an implementation of the game "Hangman" that uses dictionaries to maintain the "state" of the program -- that is, all the information needed for each round of play such as the word being guessed, how many misses the user has made, which letters have been guessed, etc. The program uses the `argparse` module to gather options from the user while providing default values so that nothing needs to be provided. The `main` function is used just to gather the parameters and then run the `play` function which recursively calls itself, each time passing in the new "state" of the program. Inside `play`, we use the `get` method of `dict` to safely ask for keys that may not exist and use defaults. When the user finishes or quits, `play` will simply call `sys.exit` to stop. Here is the code:

``````\$ cat -n hangman.py
1    #!/usr/bin/env python3
2    """Hangman game"""
3
4    import argparse
5    import os
6    import random
7    import re
8    import sys
9
10    # --------------------------------------------------
11    def get_args():
12        """parse arguments"""
13        parser = argparse.ArgumentParser(description='Hangman')
14        parser.add_argument('-l', '--maxlen', help='Max word length',
15                            type=int, default=10)
16        parser.add_argument('-n', '--minlen', help='Min word length',
17                            type=int, default=5)
18        parser.add_argument('-m', '--misses', help='Max number of misses',
19                            type=int, default=10)
21                            type=str, default='/usr/share/dict/words')
22        return parser.parse_args()
23
24    # --------------------------------------------------
25    def main():
26        """main"""
27        args = get_args()
28        max_len = args.maxlen
29        min_len = args.minlen
30        max_misses = args.misses
31        wordlist = args.wordlist
32
33        if not os.path.isfile(wordlist):
34            print('--wordlist "{}" is not a file.'.format(wordlist))
35            sys.exit(1)
36
37        if min_len < 1:
38            print('--minlen must be positive')
39            sys.exit(1)
40
41        if not 3 <= max_len <= 20:
42            print('--maxlen should be between 3 and 20')
43            sys.exit(1)
44
45        if min_len > max_len:
46            print('--minlen ({}) is greater than --maxlen ({})'.format(min_len, max_len))
47            sys.exit(1)
48
49        regex = re.compile('^[a-z]{' + str(min_len) + ',' + str(max_len) + '}\$')
50        words = [w for w in open(wordlist).read().split() if regex.match(w)]
51        word = random.choice(words)
52        play({'word': word, 'max_misses': max_misses})
53
54    # --------------------------------------------------
55    def play(state):
56        """Loop to play the game"""
57        word = state.get('word') or ''
58
59        if not word:
60            print('No word!')
61            sys.exit(1)
62
63        guessed = state.get('guessed') or list('_' * len(word))
64        prev_guesses = state.get('prev_guesses') or set()
65        num_misses = state.get('num_misses') or 0
66        max_misses = state.get('max_misses') or 0
67
68        if ''.join(guessed) == word:
69            msg = 'You win. You guessed "{}" with "{}" miss{}!'
70            print(msg.format(word, num_misses, '' if num_misses == 1 else 'es'))
71            sys.exit(0)
72
73        if num_misses >= max_misses:
74            print('You lose, loser!  The word was "{}."'.format(word))
75            sys.exit(0)
76
77        print('{} (Misses: {})'.format(' '.join(guessed), num_misses))
78        new_guess = input('Your guess? ("?" for hint, "!" to quit) ').lower()
79
80        if new_guess == '!':
81            print('Better luck next time, loser.')
82            sys.exit(0)
83        elif new_guess == '?':
84            new_guess = random.choice([x for x in word if x not in guessed])
85            num_misses += 1
86
87        if not re.match('^[a-zA-Z]\$', new_guess):
88            print('"{}" is not a letter'.format(new_guess))
89            num_misses += 1
90        elif new_guess in prev_guesses:
92        elif new_guess in word:
94            last_pos = 0
95            while True:
96                pos = word.find(new_guess, last_pos)
97                if pos < 0:
98                    break
99                elif pos >= 0:
100                    guessed[pos] = new_guess
101                    last_pos = pos + 1
102        else:
103            num_misses += 1
104
105        play({'word': word, 'guessed': guessed, 'num_misses': num_misses,
106              'prev_guesses': prev_guesses, 'max_misses': max_misses})
107
108    # --------------------------------------------------
109    if __name__ == '__main__':
110        main()
``````