Number Guess

You've written functions. Many functions. Now let's put your skill as a function-writer to work.

How so? We'll write a program that consists of many functions.  Moreover, this program will be self-contained; it will ask for input from the user, process that input, ask again if necessary, etc. Thus all we need to do is run it and then it takes over.

Before we jump in, I must acquaint you with some bits of Python that we haven't yet seen.

The input Function

First we need to know how to solicit user input and store that input away in a variable.

Try this two-liner in your code editor.

user_input = input("Tell me about yourself. ")

print("You said:", user_input)

Here's my run:

Tell me about yourself. I'm not shy, just a little introverted.

You said: I'm not shy, just a little introverted.

>>> 

We see here Python's input function. The argument is the string that will be printed to the screen when input is solicited. When executed, the user will see that string (for me in the console, for you perhaps in a pop-up window) and will be given the opportunity to type.

The output of input is the string typed by the user. In the code snippet above, this input string was stored in the variable user_input.

Note this carefully. The value returned by in the input function is always a string.  Did the user type in "True"? That's a string, not a Boolean. Did she type in "2"? That's a string, not an int. This means that if we wish to input an integer, we'll have to convert from string type to int type.  Let's discuss how to do that.

Convert to int type

So, let's say the user typed in "2" when asked to input a value. That's a string, as the console shows us:

>>> answer = input("Pick a number: ")

Pick a number: 2

>>> type(answer)

<class 'str'>

That's a problem of course. We can't do with strings what we can do with numbers. (I continue on with the console session begun above.)

>>> 3 ** answer

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

TypeError: unsupported operand type(s) for ** or pow(): 'int' and 'str'

How do we turn the string "2" into the int 2? The type conversion function int.

>>> an_int = int(answer)

>>> an_int

2

>>> type(an_int)

<class 'int'>

So the lesson is clear. If you need the user to input an int, you'll have to take the output of the input function and run it through the int function.

try, except

Alright, let's say we want the user to input an integer but the user types "armadillo". What happens if we try to convert that to int type? Well, let's try.

>>> answer = input("Pick a number: ")

Pick a number: armadillo

>>> int(answer)

Traceback (most recent call last):

  File "<pyshell#0>", line 1, in <module>

    int("armadillo")

ValueError: invalid literal for int() with base 10: 'armadillo'

Python threw up its hands. It can't convert the string "armadillo" to an int. This of course would be fatal if it were a line of code in a program. The program would crash, and that my dear students is the programmers fault. You might be temped to blame the silly user, but the hard truth is that you must always account for silly user input.

We'd like to catch inputs that can't become ints, inform the user of their error and ask for another input. How? try and except

user_input = input("Please input an integer: ")

try:

    input_int = int(user_input)

except:

    print("I beg your pardon, but that is not an integer.")

You see here two new keywords: try and except. We use them to handle the error that would be generated by an attempt to convert "armadillo" to an int.  We handle and don't crash. How so? Here's the logic:

This is good - we won't crash if the user isn't cooperative. But we'd like to make the user keep at it until they get it right. Hmm ... we want to keep at it until a certain condition obtains. We want to ... what's that word again? I've got it! Loop! We want to loop until we get an actual int! That means while of course. Look at this pretty little snippet.

while True:

    user_input = input("Please input an integer: ")

    try:

        input_int = int(user_input)

    except:

        print("I beg your pardon, but that is not an integer.")

        print("Please do try again.")

    else:

        print("Bravo! You've done it!")

        break  # end the while loop

We've seen all of this except for the else at the end. The logic of this else is that if all the code in the try block is executed error free, then we skip the except block and continue on to the else block. Why have an else block? Why not just place all the else block code in the try block?  My answer is a question: What if the code in the else block contained an unexpected error, like perhaps a simple syntax error? What if we had misspelled print as prnt there? Well, the try block catches any exception and sends us to the except block. So that syntax error that we should really want to raise an exception would not; and that makes it more difficult than necessary to debug. The lesson here is this: in the try block, place only those lines of code which you think might crash.

os, random, time

In the program I want you to write (description below), you'll need to clear the console, pause execution and generate random integers. We need the os module for the first, time for the second and the random  for the third. (Modules were introduced in the discussion of the euclid module.) 

The script below pauses execution and then clears the console.

import time, os

print("Hello")

time.sleep(2)  # Pause for two seconds

os.system('clear') # This is for linux. Use os.system('cls') for Windows.

print("world!")

To time a process, we grab the time at the start and the time at the end, and then we subtract. How to get the current time? time.time() after we import time. (Time began at midnight on 1/1/1970. I know, I know. The world looks older. It's had a rough few decades.)

import time

t_init = time.time()

time.sleep(3)  # pause for 3 seconds

t_final = time.time()

print(t_final - t_init)

To generate random integers, we need the random module's randint function. random.randint(a, b) generates a random integer between a and b inclusive.

>>> import random 

>>> random.randint(1, 6)

5

>>> random.randint(1, 6)

4

>>> random.randint(1, 6)

2

>>> random.randint(1, 6)

1

Project Instructions

You're now ready to begin the project. Write a number guess game. Details:

Let me emphasize that when I test your code, I WILL try silly inputs. Like "dog" when I'm asked for a number; and an upper bound that isn't greater than the lower bound. Your code should handle these gracefully. That means no crashes!

What Functions?

Remember who were are! We're function writers! All of your code (except for a single line I'll describe below) should be inside some function or other.

Remember too that we keep our functions short, and to do that we make them all single-task. Why do we do this! Because we don't like pain! Long, multi-task functions are hard to get right.

Once a function is written, test it. With all possible types of input. Does your function ask for an integer and then return it? Well, try it with the input "armadillo" (or something else as silly). If you function crashes or otherwise misbehaves, fix it! Your functions should be crash-proof. 

Some of your  functions will of course call others. This is utterly typical.  In programs of any complexity, user-created functions call other user-created functions (which often call other user-created functions, which often call other user-created functions, etc.).

What functions should your write?  You'll have to decide for yourself, but here are some suggestions:

One last function is mandatory. Its name is  game_loop. It will call one_game and then, when the game is over, asks the user whether she wants to play again. 

The last line of code in your project will be game_loop(). This means that, when the run button is hit, the game will begin.

Below are suggestions about how you might structure one_game and game_loop

def one_game():

    # set up a game: get lower and upper bounds from the user, generate the secret number,

    # initialize a time taken variable and a number of guesses variable 

        while True:

            # get guess from player

            # if player guesses correctly, break out of while loop

            # if she doesn't guess correctly, increment the number of guesses made

        # congratulate the user

        # display the total time taken and the number of guesses made


def game_loop():

    while True:

        one_game()

        # get player answer to "Play again?"

        # if answer isn't yes, break out of while loop


game_loop() # this will be your last line of code

Let Me Reiterate

First, I expect that after I hit the run button, the game should begin. I  should not have to call a function in  the console.

Second, you code must not crash; and I will try to crash it.

My Output

I  think it'll be helpful to see a game actually played. This is a copy-paste from my console.

Input lower bound: armadillo


That can't be converted to int type. Try again.


Input lower bound: 10

Input upper bound: 1


Lower bound must be less than upper bound. Try again.


Input lower bound: 1

Input upper bound: 10


Lower bound: 1

Upper bound: 10

Number of guesses so far: 0


What's your guess: 5

That's too high.


Number of guesses so far: 1

What's your guess: 1

That's too low.


Number of guesses so far: 2

What's your guess: 3


Got it! Congrats!

Elapsed time: 13.85 seconds

Number of guesses: 3


Play again? Y or y for yes, anything else for no: Y


Input lower bound: 1

Input upper bound: 10


Lower bound: 1

Upper bound: 10

Number of guesses so far: 0


What's your guess: 5

That's too low.


Number of guesses so far: 1

What's your guess: 10

That's too high.


Number of guesses so far: 2

What's your guess: 7

That's too high.


Number of guesses so far: 3

What's your guess: 6


Got it! Congrats!

Elapsed time: 11.50 seconds

Number of guesses: 4


Play again? Y or y for yes, anything else for no: n