Trivia Game

Last Week!

  • This is our last week for Coding Club!
  • You should keep playing at home. See Installing Python at Home
  • This project will not be completed today!
  • Learning to code requires you to try and keep trying until you understand
  • There are lots of other ways to learn to code. See Useful Links for Learning

Our Trivia Game works like so:

  • We load some trivia questions
  • We ask the user each question in turn
  • Each level has an increasing point reward
  • If the user gets a question wrong, their points are halved
  • If the user gets 3 questions wrong, they lose everything
  • If the user decides to quit (enter no answer), they exit with their current score

What it Looks Like

This is what running the game should look like:

Level 1 for 1000pts    Score: 0pts Errors: 0/3
What is RAM?
    1) Memory
    2) Female Sheep
    3) Baby Sheep
    4) Forgotten Sheep
Your answer (Enter to Cancel)? 2
WRONG! 1 Errors

Level 1 for 1000pts    Score: 0pts Errors: 1/3
What is RAM?
    1) Memory
    2) Female Sheep
    3) Baby Sheep
    4) Forgotten Sheep
Your answer (Enter to Cancel)? 3
WRONG! 2 Errors

Level 1 for 1000pts    Score: 0pts Errors: 2/3
What is RAM?
    1) Memory
    2) Female Sheep
    3) Baby Sheep
    4) Forgotten Sheep
Your answer (Enter to Cancel)? 4
WRONG! Sorry, you've lost everything

Please try again!
Press <enter> to leave >

Reading the Game

You can download the game and the questions here:

Background

  • we need to be able to Read a File to load our questions
    • we will use Loops to load the questions from the file
  • we will store those questions and answers (in Lists)
  • we want to pull out particular elements in the lists (via List Indexing)
    • the first field in each line is a question
    • the second field is the answer
    • the rest of the line is wrong answers
  • we want to loop over the questions (with for Loops)
  • we need to be able to define Functions to make the code easier to understand

Overall Game Loop

Our overall game looks like this:

def run_game():
    """Run the Trivia Game until the user exits, wins or loses"""
    errors = 0
    score = 0
    list_of_questions = load_questions()
    for level_number,level in enumerate(list_of_questions):
        errors,score = run_level( level_number, level, errors, score )
        # errors == -1 -> user quit 
        # errors == 3 -> user failed
        if errors >= 3 or errors < 0:
            break
    if score:
        print("Your score was {:,}".format(score, ))
    else:
        print("Please try again!")
    raw_input("Press <enter> to leave > ")

#mainline_start

We are going to track:

  • score (points)
  • errors (to see if the user has failed)
  • we are using the function form of print() here, just to change things up
    • if you look at the top of the module you will see a weird import statement where we imported the print function
  • the current level (and level number)
    • we’ll do this by iterating with a for loop (see loop)
    • we use the enumerate() function to track our current level number

But to iterate over the questions, we have to have them loaded first.

Loading the Questions

This is what the questions.csv file looks like:

What is RAM?|Memory|Female Sheep|Baby Sheep|Forgotten Sheep

CSV files are a common way to represent a simple spreadsheet-like grid of columns and rows. There’s a module that handles all of the details of reading this format for us:

def load_questions(filename=DEFAULT_QUESTIONS):
    """Load our questions from the filename
    
    Format of the file:
    
        question|answer|bad_answer|bad_answer...
    
    returns [
        [ 'question', 'answer', 'bad_answer',... ],
        [ 'question', 'answer', 'bad_answer',... ],
        ...
    ]
    """
    import csv
    questions = []
    for record in csv.reader(open(filename), delimiter='|'):
        if len(record) > 2: # skip empty lines
            questions.append(record)
    return questions

The only bit of extra logic is to reject short lines (the if statement that checks len( record )).

Running a Level

def run_level( level_number, level, errors, score ):
    """Run a single level until user gets it correct, fails, or quits
    
    returns errors,score 
    returns errors == -1 if the user quits 
    """
    question,correct,answers = level[0],level[1],level[1:]
    random.shuffle(answers)
    
    correct_answer = False
    while not correct_answer:
        display_status(level_number, score, errors)
        display_questions( question,answers )
        response = get_response( )
        if response is None:
            # User has chosen to leave with current score
            print("Sorry to see you go!")
            return -1,score
        try:
            chosen = answers[response]
        except IndexError:
            print("Need to choose one of our options, please")
        else:
            if chosen == correct:
                print("Correct!\n")
                score += reward(level_number)
                correct_answer = True
            else:
                errors += 1
                score = score//2
                if errors >= 3:
                    print("WRONG! Sorry, you've lost everything\n")
                    score = 0
                    break
                else:
                    print("WRONG! %s Errors\n"%(errors,))
    return errors,score

Things to investigate:

  • List Indexing (level[0], level[1], level[1:], answers[response])
  • random.shuffle() so that the answer isn’t always 1
  • score // 2 (divide without producing a fractional number)

Randomizing the Order

  • We both need to keep track of what answer is correct and randomize the list.
  • We do that by tracking the correct answer with:
correct = level[1]
  • When the user chooses a number (index) we check what answers[chosen] is and compare that to our saved chosen variable
  • We catch any errors where the user has chosen a number that’s not in the answer set (such as -112) and ask them to behave

Displaying Status/Questions

We are going to use the str class’ .format method to format our game status and questions:

def display_status( level_number,  score, errors ):
    """Print out status report for a given level and current winnnings
    
    Uses string formatting to produce a nicely-formatted display
    """
    print("Level {0: 2d} for {1: 6d}pts    Score: {2: 6d}pts   Errors: {3}/3".format(
        level_number+1, # note: normal people think in 1-index
        reward(level_number), # calculate it
        score, # current value we are tracking
        errors,
    ))

That’s one big call, the {0: 2d} format inside the string reads as:

  • take the first (0-th index) value
  • format it with 2 spaces
  • as a decimal (base-10 number)

While the {3} format reads as:

  • take the fourth (3-rd index) value
  • format it “naturally”

We do the same thing for the questions, but we indent those lines (put spaces in front of them) so it’s easier for the user to read them. We’re going to use the enumerate() function again to keep track of which number we are currently showing.

def display_questions( question,answers ):
    """Display the question and the answers
    
    * displays the answers with 1-indexed labels
    
    return None
    """
    print(question)
    for i,answer in enumerate(answers):
        print('    {0}) {1}'.format(i+1, answers[i]))

Note

Most people tend to think in 1-index numbers, so we add 1 to the value we’re getting from enumerate().

Getting The User’s Choice

  • We’ll use raw_input() to get the user’s selection
  • We want to allow the user to quit, so if they enter nothing (just hit enter) we return a None
  • We don’t want the user hitting a wrong key to make them exit, so we catch any errors trying to make the number an integer and allow them to retry
def get_response():
    """Ask the user to make a choice
    
    returns 0-indexed result or None if the user enters nothing
    """
    while True:
        try:
            content = raw_input("Your answer (Enter to Cancel)? ")
            if not content:
                return None
            return int(content) - 1
        except ValueError:
            print("Didn't recognize a number in that: {0!r}".format(content))
            pass 

Calculating the Reward

  • We use a basic point amount of 1000 and then double it for each level
def reward( level_number ):
    """Reward should double for each level starting at 1000"""
    return 2**level_number * 1000

Run the Game when the Script is Run

if __name__ == "__main__":
    run_game()