7
\$\begingroup\$

I am teaching a python class to some high school seniors. I was thinking of doing an "oracle" game where the computer would come up with a number, and the player would guess it. These are brand new coders, and I'll teach them what they need to know to get there, but is this a good first project? I have my code here as my implementation of this game. I want to make sure that I don't teach them anything bad or give them bad habits.

My software requirements are:

  • Must generate a random (or psuedo-random) number
  • Must import a library
  • Must use a loop
  • Must take user input
  • Must tell the user to guess higher or lower

# imports
import random
import time
import sys
import os

# set global variables
MIN_NUMBER = 1
MAX_NUMBER = 10

# important functions
def get_random(min, max): 
    print "Coming up with a number."
    time.sleep(2)
    random_number = random.randint(min, max)
    print "Got it!"
    return random_number

def to_int(x):
    try:
        x = int(x)
        return x
    except ValueError:
        print "That is not a whole number."
        return False

def how_close(guess, right_answer):
    return right_answer - guess

def hi_low_done(delta):
    if delta < 0:
        print "Guess lower."
        return False
    elif delta > 0:
        print "Guess higher."
        return False
    else:
        print "You got it!"
        return True


# main Program

#clear screen
os.system('cls')  # on windows
print "Welcome. I am the Oracle."
print "I will think of a number between %s and %s. Guess it and you will win!" % (MIN_NUMBER, MAX_NUMBER)

while True:
    user_answer = raw_input("Ready? <y/n> ")
    if user_answer.lower() == 'y' or user_answer.lower() == 'yes':
        print "Great! Let's go."
        break;
    elif user_answer.lower() == 'n' or user_answer.lower() == 'no':
        print "Okay. Consult the Oracle when you are ready."
        sys.exit()

oracles_number = get_random(MIN_NUMBER, MAX_NUMBER)

number_of_guesses = 0
while True: 
    number_of_guesses = number_of_guesses + 1
    user_answer = raw_input("\nGuess> ")
    user_answer = to_int(user_answer)
    # if user answer is outside min max... else...
    if user_answer:
        delta = how_close(user_answer, oracles_number)
        done = hi_low_done(delta)
        if done: 
            break;

print "It took you %s guesses to guess my number." % (number_of_guesses)

I guess my concerns with my code are:

  1. Is this simple enough? It isn't the first thing they will code, but the first project they will do. (maybe group project?)
  2. Did I make any blatant errors or "In python, you should do this instead" errors? Python isn't my first language so I'm not sure.

I have created a gist file with my most up-to-date version of this.

\$\endgroup\$
  • 2
    \$\begingroup\$ Why aren't you teaching Python 3? \$\endgroup\$ – 200_success Jul 20 '15 at 14:38
  • \$\begingroup\$ Thanks for the title update! It is better. @200_success, I don't get to make changes to the computer lab and 2.7 is installed. I was also under the impression that python3 wasn't really accepted by the majority of the community. Is that false? \$\endgroup\$ – Jeff Jul 20 '15 at 14:40
  • \$\begingroup\$ I would say that the community considers stragglers to be part of the problem, and you would be perpetuating the problem by grooming a new generation of students on obsolescent technology. \$\endgroup\$ – 200_success Jul 20 '15 at 14:44
  • \$\begingroup\$ @Jeff Anyhow the versions are similar passing from one to the other requires very little effort. \$\endgroup\$ – Caridorc Jul 20 '15 at 14:45
  • 3
    \$\begingroup\$ I'll see if I can get 3 in the lab then! \$\endgroup\$ – Jeff Jul 20 '15 at 14:45
6
\$\begingroup\$

In my opinion I think this is a very very simple problem and much less for high school seniors. Also the amount of code in your project is too much. This will thwart away students and is not pythonic. My aim for the first time students should be to show them how beautiful coding can be and the sheer amount of power you can get with few lines of code. Below is how I will implement it.

1st Solution: Without loop. Uses recursion, use of global etc.

import random

def check():
    global count  # good example of use of global
    guess = int(raw_input("Take a guess\n"))
    if guess == no:
        print "Good job, %s! You guessed my number in %d guesses!" %(name,count)
    if guess < no:
        print "Your guess is too low."
        count +=1        
        check()          
    if guess > no:
        print "Your guess is too high"
        count +=1
        check()

name = raw_input("Hello! What is your name?\n")
print "Well, " + name + ", I am thinking of a number between 1 and 20"
no = random.randint(1,20)
global count
count  =1
check()

2nd Solution: With loops. Less code. Showcases importance of while loops. Satisfies all five of your software requirements.

import random

name = raw_input("Hello! What is your name?\n")
print "Well, " + name + ", I am thinking of a number between 1 and 20"
no = random.randint(1,20)
guess = int(raw_input("Take a guess\n"))
count =1

while guess != no:    
    if guess < no:
        print "Your guess is too low."                 
    if guess > no:
        print "Your guess is too high"
    count +=1
    guess = int(raw_input("Take a guess\n"))

print "Good job, %s! You guessed my number in %d guesses!" % (name ,count)

Best would be you should showcase both the approach so that they can understand the difference, appreciate while loops, get a liking for python. Avoid using too much constructs (checking for their input, errors) etc. Testing, using OOPs, error handling etc. should be kept for further stages.

\$\endgroup\$
  • \$\begingroup\$ These students just learned how to open a command prompt, so as simple as it may seem, it's not really. The short code you submitted rocks though. Thanks. \$\endgroup\$ – Jeff Jul 20 '15 at 19:06
  • \$\begingroup\$ @Jeff thx, If they don't know coding kudos you chose python, but avoid using too much boiler plate code initially. \$\endgroup\$ – garg10may Jul 20 '15 at 19:11
  • \$\begingroup\$ @garg10may What do you mean exactly by boilerplate code? I would not consider function definition boilerplate. \$\endgroup\$ – Caridorc Jul 20 '15 at 19:16
  • 1
    \$\begingroup\$ @Caridorc hmm, not literally boiler plate, I meant testing for user inputs, printing them beautiful messages. \$\endgroup\$ – garg10may Jul 20 '15 at 19:20
  • \$\begingroup\$ @garg10may I understand. You want to start with the very basic features and maybe build on them later. A very reasonable approach. \$\endgroup\$ – Caridorc Jul 20 '15 at 19:21
8
\$\begingroup\$

There is a serious problem at:

def get_random(min, max): 
    print "Coming up with a number."
    time.sleep(2)
    random_number = random.randint(min, max)
    print "Got it!"
    return random_number

This function mixes user interaction (printing and sleeping) and logic (returning a random number). This is basic separation of intents, and should be taught to beginners from the start.

The same can be seen at:

def hi_low_done(delta):
    if delta < 0:
        print "Guess lower."
        return False
    elif delta > 0:
        print "Guess higher."
        return False
    else:
        print "You got it!"
        return True

This function should only return a message, not return a message and print.

About to_int

def to_int(x):
    try:
        x = int(x)
        return x
    except ValueError:
        print "That is not a whole number."
        return False

While it is possible to return different types from a function in Python, it is a symptom of bad design. I reccomend writing a is_int method to return True or False if the argument is an int and then converting to int with int.

Further refactoring

Asking a user for yes or no is a common task and should be its own function, also using startswith is more flexible and allows 'yeah' and 'yup' for example.

Also you should write a function to generate the message. You then print it and break if guess and real number are equal.

Use Python 3

Python 3 is continually improved while Python 2 is deprecated, it makes more sense to teach 3 than 2 (The two are similar just use parens around print).

Avoid needless comments

Comments should tell why not what, if you write

# imports

You just add noise as it is obvious that the following lines are imports.

Define a main function and call it

It is a good habit to get into and allows the importing of scripts.

Like:

def number_guessing_game(minimum, maximum):
     # All your currently top level code

if __name__ == "__main__":
    number_guessing_game()
\$\endgroup\$
  • \$\begingroup\$ Okay. I added the sleep in there to make the computer look like it was "thinking" but I see what you mean. \$\endgroup\$ – Jeff Jul 20 '15 at 13:42
  • 1
    \$\begingroup\$ I created a gist to fix what you said. \$\endgroup\$ – Jeff Jul 20 '15 at 13:46
  • \$\begingroup\$ @Jeff Updated answer \$\endgroup\$ – Caridorc Jul 20 '15 at 13:51
  • \$\begingroup\$ Gotcha. In Java and some other languages, the function has to return whatever you declared it as, public int get_random_number or something, SO I should "enforce" the same rule in python. Updated gist. \$\endgroup\$ – Jeff Jul 20 '15 at 13:56
  • \$\begingroup\$ @Jeff Typing is possible in Python but not reccomended. Also in general you should return only one type, I think that enforcing (i.e. giving no other choice than) that will not benefit you. \$\endgroup\$ – Caridorc Jul 20 '15 at 13:59
4
\$\begingroup\$

Code organization

Please set a good example by teaching your students to write docstrings. Docstrings also make your code easier for beginners to follow. Docstrings are also more informative than worthless comments like # important functions and # main program.

Along those lines, it is good discipline to discourage free-floating code, and put all code inside functions. Not only is it good software engineering practice, it also helps beginners by forcing them to answer "What does this chunk of code do?" for every line of their program.

how_close() and hi_low_done() are more of a nuisance as separate functions. I'd rather see a guessing_game() function. hi_low_done(), in particular, is annoying because you still need an if done: break.

Avoid sys.exit() by structuring the code properly.

Good habits

to_int() is weird in that it sometimes returns an int, and sometimes returns a bool. Rather than False, you should return None to indicate failure. That way, you can say that to_int() returns either an integer or no integer.

Speaking of type consistency, I think it is poor practice to reassign a value in a way that changes its type or meaning. For example:

user_answer = raw_input("\nGuess> ")
user_answer = to_int(user_answer)

Either use two different variable names, or set it correctly the first time:

user_answer = to_int(raw_input("\nGuess> "))

Fluency

Instead of writing user_answer.lower() four times, normalize the input to lowercase as soon as you receive it. To check for whether a string is one of two values, use the in operator.

Loops in Python that increment a counter are awkward. There is usually a more elegant way, often involving range() or enumerate(). In this case, you want itertools.count().

You have a couple of unnecessary semicolons after break.

Suggested solution

from itertools import count
import random
import os
import time

def random_int(min, max): 
    """
    Generate a random integer between min and max inclusive, verbosely, with
    simulated thinking time.
    """
    print("Coming up with a number.")
    time.sleep(2)
    random_number = random.randint(min, max)
    print("Got it!")
    return random_number

def to_int(x):
    """
    Interprets x as an int. If x does not look like an int, return None.
    """
    try:
        return int(x)
    except ValueError:
        return None

def guessing_game(min, max):
    """
    Play a number-guessing game with the specified integer bounds.  Return the
    number of guesses needed (1 if guessed correctly on the first try).
    Nonsense guesses count against the number of guesses.
    """
    oracles_number = random_int(min, max)
    for i in count(1):
        guess = to_int(raw_input("\nGuess> "))
        if guess is None:
            print("That is not a whole number.")
        else:
            delta = oracles_number - guess
            if delta < 0:
                print("Guess lower.")
            elif delta > 0:
                print("Guess higher.")
            else:
                print("You got it!")
                return i

def yes_no(prompt):
    """
    Prompt the user for a yes/no response.  Return True for yes, False for no.
    """
    while True:
        answer = raw_input(prompt).lower()
        if answer in ('y', 'yes'):
            return True
        elif answer in ('n', 'no'):
            return False

def main():
    MIN_NUMBER = 1
    MAX_NUMBER = 10

    #clear screen on Windows
    os.system('cls')
    print("Welcome. I am the Oracle.")
    print("I will think of a number between %s and %s. Guess it and you will win!" % (MIN_NUMBER, MAX_NUMBER))
    if yes_no("Ready? <y/n> "):
        print("Great! Let's go.")
        guesses = guessing_game(MIN_NUMBER, MAX_NUMBER)
        print("It took you %s %s to guess my number." % (guesses, ['guess', 'guesses'][guesses > 1]))

    else:
        print("Okay. Consult the Oracle when you are ready.")

main()

Miscellaneous remarks:

The singular/plural switch may be a bit tricky for beginners to understand. You might want to just live with the pluralization bug.

Standard practice is to write the boilerplate if __name__ == '__main__': main(). If you're not ready to have that conversation, I think just calling main() will do.

\$\endgroup\$
1
\$\begingroup\$

Unnecessary variables:

done = hi_low_done(delta)
if done: 
    break;

You don't need to save that in a variable because you only use it once.

sys.exit()

If you define a main function as Caridorc suggests, you can just return from the function.

Input

if user_answer.lower() == 'y' or user_answer.lower() == 'yes':

Instead of manually checking each allowed input here, I would create an array of allowed inputs:

accept_input_set = ['y', 'yes']

You can check if your input is in this set like this:

if user_answer.lower() in accept_input_set:

Formatted strings

print "It took you %s guesses to guess my number." % (number_of_guesses)

Python usually uses this style formatting now:

print ("It took you {0} guesses to guess my number.".format(number_of_guesses))
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.