Skip to content

PA3: Trivia Night

Trivia night is a fixture of coffee shops, libraries, and dorm lounges everywhere. Teams huddle over a sheet of paper, someone is sure the answer is Saturn, and someone else is right that it's Jupiter. Behind the scenes, the host has a stack of questions, asks them in an order nobody can predict, and never asks the same one twice.

A good host is also patient. A blank stare doesn't count as an answer, and saying "Mars" a second time doesn't earn you another guess.

In this PA, you write the host. Your code reads the questions and answers from a file, draws them at random without repeating one, gives the player a couple of tries at each, and writes the night's results to a file.

This is one of the two PA3 options; do this one or Gift Swap, not both.

What you'll practice

  • Writing a while loop that repeats until a random event happens, and making sure it can end.
  • Writing a while loop that asks for input again until the input is valid.
  • Naming the three parts of a loop: initialization, condition, and update.
  • Using a set to keep track of what you have already seen.
  • Reading a file one line at a time, and writing a new one (Week 10).

Get set up

Download both files and save them in the Week09 folder inside ~/CS149:

  • trivia.py, the module with your four functions
  • questions.txt, the questions and answers, which the program reads in Part C

Open trivia.py and look around. All four functions are already there, each with type hints and a docstring that says exactly what it takes and what it returns, and each body is just pass. Your job is to replace those pass lines with real code. Read each docstring all the way through, example included, before you start that function. The main block at the bottom is written for you; leave it alone.

Put your name and today's date in place of YOUR NAME and THE DATE at the top of the file. Gradescope checks.

Submit early to Gradescope

If you don't submit to Gradescope and get a nonzero score, you will receive a reflection grade of no higher than 15/30 points. It's one assignment for the whole PA. Submit Part A as soon as it works; Gradescope grades each part separately, so the parts you haven't started don't stop it.

Part A: pick a question with draw_question

draw_question(questions, asked) picks the next question. It draws with random.choice(questions), which picks one item of the list at random. If that question has been asked already (it is in the set asked), it draws again. How many draws will that take? Nobody knows ahead of time, and that is exactly the job a while loop is for.

>>> draw_question(["Q1", "Q2", "Q3"], {"Q1", "Q3"})
'Q2'
>>> draw_question(["Q1", "Q2", "Q3"], set())     # any of the three, at random
'Q3'

There is one trap. If every question has been asked, drawing again will never help, and a while loop that waits for a new question runs forever. So before it draws at all, draw_question checks with a for loop whether any question is left, and if none is, it returns "" without drawing.

Predict it: what can draw_question(["Q1", "Q2", "Q3"], {"Q2"}) return?

'Q1' or 'Q3', never 'Q2'. Run it a few times and you will see both.

Trace it: name the three parts of the drawing loop

The initialization is the first draw, before the loop. The condition is "this question is already asked". The update is drawing again, inside the loop. If you leave out the update, the condition never changes, and the loop runs forever on the first repeat.

Part B: take the answers with ask

ask(answer, tries) reads answers with input("Answer: ") until the player gets it right or runs out of tries. It handles four kinds of answer:

The player types It prints Uses a try?
nothing (a blank line) Type an answer first. no
an answer they tried before You already tried mars. no
the right answer Correct! yes, and the question is over
any other answer Not quite. yes

When the last try is used up, it prints Out of tries. The answer was jupiter. It returns how many tries the player used, counting the right one, or 0 if they never got it. To check "tried before", keep a set of the answers you have seen: a set answers in without you writing a loop, and it never holds the same answer twice.

>>> ask("jupiter", 3)       # the player types mars, a blank line, mars, jupiter
Not quite.
Type an answer first.
You already tried mars.
Correct!
2
Your turn: what does ask("pacific", 2) print and return if the player types atlantic, atlantic, indian?

It prints Not quite., You already tried atlantic., Not quite., and Out of tries. The answer was pacific., and returns 0. The repeated atlantic didn't use a try, so indian was the second try.

Predict it: with ask("6", 1), the player types 8, then a blank line, then 6. How many of those does ask read?

Only the 8. It is wrong and uses the one try, so the loop's condition is false, and ask prints Out of tries. The answer was 6. and returns 0 without asking again.

Which loop condition?

The loop keeps going while the player has tries left. A right answer ends the question early, so return the count from inside the loop the moment you see it. Anything that doesn't use a try must not change the count, or the player loses a turn for a typo.

Until Part C reads the questions file, running trivia.py stops right away with a TypeError in the main block. That is expected, not a sign you broke something. So check Parts A and B this week by adding a few calls at the top of the main block, right under the if __name__ == "__main__": line, like print(draw_question(["Q1", "Q2", "Q3"], {"Q2"})) and print(ask("jupiter", 2)). Run the file a few times and type some answers, including a blank one and a repeat. Take those lines out again before you submit.

Part C: read the questions with load_questions (Week 10)

questions.txt holds one question per line, a question and its answer separated by |:

# question | answer
What is the largest planet in our solar system? | jupiter
How many sides does a hexagon have? | 6

load_questions(filename) opens the file, reads it one line at a time, and returns a dictionary from each question to its answer. Each line needs a bit of cleaning: strip() takes the spaces and the newline off the ends, and split("|") breaks it into the question and the answer, which need a strip() of their own. It skips four kinds of line:

  • a blank line,
  • a line that starts with # (a comment, like the first line of the file),
  • a line whose answer is empty, like What is frozen water called? |,
  • a question it has already seen, which keeps the first answer.
Predict it: what does load_questions do with the line Two plus two? | 4?

It keeps it, as "Two plus two?" mapped to "4". The strips take the spaces off both ends of the line and both ends of each piece.

Your turn: the provided questions.txt has ten lines with a | in them. How many questions end up in the dictionary?

Seven. The first line is a comment, the second "largest planet" line is a repeat (so the answer stays jupiter, not saturn), and the frozen water question has no answer.

Part D: write the results with save_results (Week 10)

save_results(filename, questions, scores, answers) writes one line per question. scores[0] is what ask returned for questions[0], and so on. Each line is the question, --, and how it went:

What is the largest planet ... -- got it in 2 tries
What color do you get by mi... -- got it in 1 try
Which city is the capital o... -- missed (RICHMOND)

A results sheet has only so much room, so a question longer than 30 characters is cut to its first 27 with a slice, and ... is added. That keeps every question to 30 characters at most. A score of 1 is 1 try, any other score is tries, and a 0 shows the answer in capital letters.

Predict it: how does How many legs does a spider have? look on the sheet?

How many legs does a spider... The question is 33 characters, more than 30, so the sheet shows its first 27, then ....

Your turn: why does Which ocean is the largest? show up whole?

It is 27 characters, and only a question longer than 30 gets cut.

Test it before you submit

Test each part as you finish it, the way the sections above describe, and submit to Gradescope after each one. Gradescope's messages name the part and what it checked.

When all four work, run trivia.py and type the answers below. You should see exactly this, and a new file results.txt in your Week09 folder:

Welcome to Trivia Night! All answers are lowercase.

Question 1: What is the largest planet in our solar system?
Answer: mars
Not quite.
Answer:
Type an answer first.
Answer: mars
You already tried mars.
Answer: jupiter
Correct!

Question 2: What color do you get by mixing blue and yellow?
Answer: green
Correct!

Question 3: Which city is the capital of Virginia?
Answer: norfolk
Not quite.
Answer: roanoke
Not quite.
Out of tries. The answer was richmond.

Results saved to results.txt.
What is the largest planet ... -- got it in 2 tries
What color do you get by mi... -- got it in 1 try
Which city is the capital o... -- missed (RICHMOND)

The main block starts the random numbers from a fixed seed, so your questions come out in the same order as these, as long as draw_question draws exactly the way its docstring says.

Rules

Only what we have covered

Gradescope rejects a trivia.py that uses any of these:

  • a comprehension or a lambda (Week 11)
  • the dictionary methods get(), setdefault(), keys(), values(), and items() (Week 11)
  • any import other than random

while loops are fair game from now on, and so are the string methods from class, like strip(), split(), startswith(), and upper().

Submission

Submit trivia.py to the Gradescope assignment PA3b Trivia Night, then commit and push your Week09 folder (how). You don't need to submit questions.txt; Gradescope has its own copy. Gradescope also checks that your name and the date are in the docstring, and that ruff check is clean.

Do this option or Gift Swap, not both.

Where the idea comes from

Pub quizzes started in British pubs in the 1970s and spread everywhere from there. Drawing questions at random without repeats is the same problem as dealing cards from a deck, and asking again until the answer is valid is one of the most common loops in any program that talks to a person.