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
[0] Guess a number between 1 and 50 (q to quit): 25
You guessed "25"
Too high.
[1] Guess a number between 1 and 50 (q to quit): 12
You guessed "12"
Too low.
[2] Guess a number between 1 and 50 (q to quit): 20
You guessed "20"
Too high.
[3] Guess a number between 1 and 50 (q to quit): 17
You guessed "17"
Too low.
[4] 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')
    parser.add_argument('-m', '--min', help='Minimum value',
                        metavar='int', type=int, default=1)
    parser.add_argument('-x', '--max', help='Maximum value',
                        metavar='int', type=int, default=50)
    parser.add_argument('-g', '--guesses', help='Number of guesses',
                        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')
    12        parser.add_argument('-m', '--min', help='Minimum value',
    13                            metavar='int', type=int, default=1)
    14        parser.add_argument('-x', '--max', help='Maximum value',
    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)
    20        parser.add_argument('-w', '--wordlist', help='Word list',
    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:
    91            print('You already guessed that')
    92        elif new_guess in word:
    93            prev_guesses.add(new_guess)
    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()

results matching ""

    No results matching ""