Capstone Project 2: The Trivia Game Engine
"The point of a game engine isn't the game — it's the engine. Build the system that can run any trivia game, and you'll have built something worth talking about."
Project Overview
If the Personal Finance Tracker is about utility, the Trivia Game Engine is about delight. You're building a complete game — one with players, rounds, scoring, difficulty curves, and a leaderboard that persists between sessions. But underneath the fun, every design decision maps to a concept you learned in this course.
A game engine is fundamentally a state machine. It tracks whose turn it is, what question is active, how many points are at stake, and when the game ends. Managing that state cleanly requires the full OOP toolkit from Chapters 14-16. Loading questions from files requires the I/O skills from Chapter 10. Supporting multiple question formats requires polymorphism from Chapter 15. And making sure the whole thing doesn't crash when a player types "banana" instead of "2" requires the error handling discipline from Chapter 11.
This project is particularly well-suited for students who enjoyed the Crypts of Pythonia text adventure running example. You're building a different kind of game, but the same architectural instincts apply: separate the game logic from the display logic, model your domain with objects, and make the computer do the bookkeeping.
Why a Game?
Games are state-rich applications. At any moment, the Trivia Game Engine needs to know: Who are the players? Whose turn is it? What category did they choose? What question are they answering? How much time has passed? What's the score? Who's winning? That density of state — and the need to manage it without bugs — is exactly the kind of complexity that OOP was invented to handle.
Games also have a natural testing surface. You can verify that scoring works correctly, that difficulty filtering returns the right questions, that the leaderboard sorts properly, and that invalid input is handled gracefully. Every one of those tests reinforces the habits from Chapter 13.
And honestly? Games are fun to build. You'll spend your debugging sessions playing trivia instead of staring at spreadsheet data. That matters for motivation, especially during the final push.
Required Features
1. Question Bank
- Support a question bank loaded from a JSON file. Each question must have: question text, answer choices (for multiple choice), correct answer, category, and difficulty level (easy, medium, hard). (Chapters 9, 10)
- Include a starter question bank with at least 50 questions across at least 5 categories (e.g., Science, History, Geography, Pop Culture, Technology). (Chapter 10)
- Support two question types: multiple choice (4 options, one correct) and true/false. (Chapter 4, Chapter 15 — polymorphism)
- Allow users to create and add custom questions through the application interface. New questions are saved to the question bank file. (Chapters 10, 11)
- Validate question data on load — skip malformed questions and warn the user rather than crashing. (Chapter 11)
2. Game Setup and Configuration
- Support 1 to 4 players in a single session. Each player enters their name at the start. (Chapters 5, 8)
- Allow players to choose a category or play with mixed categories (random selection). (Chapters 4, 9)
- Allow players to select a difficulty level or play with mixed difficulty (progressive — starts easy, gets harder). (Chapter 4)
- Configure the number of rounds (5, 10, 15, or 20 questions). (Chapter 4)
3. Gameplay Loop
- Present questions one at a time with clear formatting: question number, category, difficulty, question text, and answer choices. (Chapter 7)
- For multiple choice questions, accept answers by letter (a/b/c/d) or number (1/2/3/4). (Chapter 4)
- After each answer, display whether the player was correct or incorrect, show the correct answer if wrong, and display a running score. (Chapters 4, 7)
- Rotate between players in turn-based order for multiplayer games. (Chapters 5, 8)
- Track each player's score with a point system: easy questions worth 1 point, medium worth 2, hard worth 3. (Chapters 3, 9)
- Prevent question repetition within a single game session. (Chapter 9 — sets)
4. End-of-Game Summary
- At the end of a game, display a formatted scoreboard showing all players ranked by score. (Chapters 7, 8, 19)
- Show each player's accuracy percentage (correct answers / total answers). (Chapter 3)
- Show a category performance breakdown: how each player scored in each category. (Chapter 9)
- Declare a winner (or announce a tie with all tied players named). (Chapter 4)
5. Persistent Leaderboard
- Maintain a leaderboard in a JSON file that persists between game sessions. (Chapter 10)
- Store: player name, score, total questions, accuracy percentage, date, and difficulty setting. (Chapter 9)
- Display the top 10 all-time scores from the main menu. (Chapters 8, 19)
- Handle the first-run case — if no leaderboard file exists, create one. (Chapter 11)
6. Question Editor
- Provide a "Create Question" mode accessible from the main menu. (Chapters 5, 6)
- Walk the user through creating a question step by step: enter the question text, enter each answer choice, indicate the correct answer, select a category, select a difficulty. (Chapters 4, 5)
- Validate every field — reject empty questions, reject questions with fewer than 2 choices, ensure the correct answer matches one of the choices. (Chapter 11)
- Save new questions to the question bank file immediately. (Chapter 10)
7. Error Handling
- Handle file not found errors for both the question bank and leaderboard gracefully. (Chapter 11)
- Handle malformed JSON with a specific error message (don't just crash with a JSONDecodeError traceback). (Chapter 11)
- Handle insufficient questions — if the user selects a category/difficulty combination with fewer questions than rounds requested, inform them and adjust. (Chapters 4, 11)
- The game must never crash from user input. Invalid menu selections, non-numeric input, and empty strings should all produce helpful messages. (Chapter 11)
8. Testing
- Write at least 15 unit tests using pytest covering:
- Question creation and validation (both types)
- Score calculation (correct scoring for each difficulty level)
- Leaderboard sorting and persistence
- Question filtering by category and difficulty
- Edge cases (one player, tied scores, empty category, no questions available)
- The question editor's validation logic
- Use fixtures for reusable test data (a sample question bank, sample players). (Chapter 13)
Technical Requirements
| Requirement | Relevant Chapters | Details |
|---|---|---|
| Variables and types | Ch 3 | Scores, names, timestamps, accuracy percentages |
| Conditionals | Ch 4 | Answer checking, menu routing, difficulty selection, tie detection |
| Loops | Ch 5 | Game loop, turn rotation, menu loop, question editor workflow |
| Functions | Ch 6 | At least 15 functions with docstrings |
| Strings | Ch 7 | Formatted output, question display, scoreboard alignment |
| Lists and tuples | Ch 8 | Player lists, question collections, sorted leaderboards |
| Dictionaries and sets | Ch 9 | Category mappings, score tracking, seen-question tracking |
| File I/O | Ch 10 | JSON question bank, JSON leaderboard, pathlib |
| Error handling | Ch 11 | Robust input validation, file error handling, EAFP |
| Modules | Ch 12 | Code split across at least 4 modules |
| Testing | Ch 13 | pytest suite with 15+ tests and fixtures |
| OOP | Ch 14-16 | Class hierarchy with inheritance and polymorphism |
Suggested Architecture
Module and Class Structure
trivia_engine/
__init__.py
models.py # Question, MultipleChoiceQuestion, TrueFalseQuestion, Player
game.py # Game class (core game loop and logic)
leaderboard.py # Leaderboard class (persistent high scores)
question_bank.py # QuestionBank class (load, filter, add questions)
cli.py # Display and input handling
main.py # Entry point
data/
questions.json # Starter question bank
leaderboard.json # Persistent leaderboard (created at runtime)
tests/
test_models.py
test_game.py
test_leaderboard.py
test_question_bank.py
conftest.py
Core Classes
Question (base class) (Chapter 15)
- Attributes: text (str), correct_answer (str), category (str), difficulty (str), points (int, computed from difficulty)
- Methods: check_answer(response), to_dict(), from_dict() (class method), __str__()
- The check_answer() method normalizes input (case-insensitive, stripped whitespace) before comparing
MultipleChoiceQuestion(Question) (Chapter 15)
- Additional attributes: choices (list of 4 strings)
- Overrides __str__() to display choices labeled A-D
- Overrides check_answer() to accept letter (a/b/c/d) or number (1/2/3/4) in addition to the full text
TrueFalseQuestion(Question) (Chapter 15)
- Choices are always ["True", "False"]
- Overrides check_answer() to accept "t"/"f", "true"/"false", "yes"/"no"
This is a textbook application of polymorphism from Chapter 15. The Game class calls question.check_answer(response) without caring which type of question it is. The subclass handles the details. That's the whole point of inheritance.
Player (Chapter 14)
- Attributes: name (str), score (int), correct_count (int), total_count (int), category_scores (dict mapping category to [correct, total])
- Methods: record_answer(category, is_correct, points), accuracy(), __str__(), __eq__(), __lt__() (for sorting by score)
- __lt__ enables sorted(players) to rank by score descending — this is the special method pattern from Chapter 14
QuestionBank (Chapters 10, 14)
- Attributes: questions (list of Question objects), filepath (Path)
- Methods: load(), save(), add_question(question), get_questions(category=None, difficulty=None, count=None, exclude=None), categories(), difficulties()
- get_questions() returns a filtered, shuffled subset. The exclude parameter accepts a set of already-asked question indices to prevent repetition.
Leaderboard (Chapters 10, 14, 19)
- Attributes: entries (list of dicts), filepath (Path)
- Methods: load(), save(), add_entry(player, game_settings), top_n(n=10), display()
- Sorting the leaderboard uses the sorting concepts from Chapter 19
Game (Chapters 14, 16)
- Attributes: players (list of Player), question_bank (QuestionBank), settings (dict: rounds, category, difficulty), current_round (int), current_player_index (int)
- Methods: setup(), play_round(), next_player(), is_over(), get_results(), run()
- The run() method is the main game loop — it's the heart of the application
- The Game class does not print anything directly. It returns data that the CLI layer formats and displays. This is the separation of concerns principle from Chapter 16.
Design Principles (Chapter 16)
- Polymorphism over conditionals: Don't write
if question_type == "mc": ... elif question_type == "tf": ...in the game loop. Let the Question subclasses handle their own behavior. - Separation of concerns: The
Gameclass manages state and logic. The CLI layer handles all user-facing I/O. TheQuestionBankhandles file operations. No class does two jobs. - Composition over inheritance (where appropriate): The
Gameclass has aQuestionBankand a list ofPlayerobjects. It doesn't inherit from any of them. - Data encapsulation: Use
@propertyfor computed values likePlayer.accuracyandQuestion.points.
Development Milestones
Phase 1: Questions and Answers (Days 1-3)
Goal: A single-player game that asks questions and checks answers.
- Create the
Questionbase class andMultipleChoiceQuestionsubclass - Hardcode 10 questions in a list (no file loading yet)
- Write a simple game loop: display question, get answer, check answer, update score
- Display the final score at the end
- Test: Write tests for
Questioncreation,check_answer()with various input formats
Checkpoint: You can play a 10-question trivia game against yourself. It's not pretty yet, but the core mechanic works.
Phase 2: Question Bank and File I/O (Days 4-6)
Goal: Load questions from a file and support categories/difficulty.
- Create the
QuestionBankclass with JSON loading - Build a starter question bank (50+ questions in JSON)
- Add
TrueFalseQuestionsubclass with its owncheck_answer()override - Implement question filtering by category and difficulty
- Add the
from_dict()class method that creates the right subclass based on the"type"field in JSON - Test: Write tests for question loading, filtering, malformed data handling
Checkpoint: Questions load from a file. You can play a Science-only game or a Hard-only game. Malformed questions in the JSON don't crash the program.
Phase 3: Multiplayer and Scoring (Days 7-10)
Goal: Support multiple players with turn-based play and proper scoring.
- Create the
Playerclass with score tracking and category breakdowns - Create the
Gameclass with turn rotation logic - Implement the point system (1/2/3 based on difficulty)
- Add the end-of-game summary with rankings, accuracy, and category performance
- Handle ties
- Test: Write tests for scoring, turn rotation, tie detection, player ranking
Checkpoint: You and your roommates can play a 4-player trivia game with category selection, and the scoreboard at the end is actually correct.
Phase 4: Leaderboard and Question Editor (Days 11-14)
Goal: Persistent high scores and user-created questions.
- Create the
Leaderboardclass with JSON persistence - Implement the question editor workflow with full validation
- Add the main menu with all options (New Game, Leaderboard, Create Question, Quit)
- Split code into modules
- Add error handling for all file operations and user input paths
- Test: Write tests for leaderboard persistence, question editor validation
Checkpoint: The leaderboard remembers your best scores across sessions. You can add your own questions and they show up in the next game. The application handles every wrong input gracefully.
Phase 5: Polish and Submission (Days 15-16)
- Expand the question bank to 50+ questions if you haven't already
- Polish display formatting: clear screen between questions, aligned columns, visual separators
- Run the full test suite and fix any failures
- Write a README with setup and usage instructions
- Review against the rubric (see
capstone-rubric.md) - Have someone else play your game — watch where they get confused
Starter Question Bank Format
Your questions.json file should follow this structure:
{
"questions": [
{
"type": "multiple_choice",
"text": "What is the time complexity of binary search?",
"choices": ["O(1)", "O(log n)", "O(n)", "O(n log n)"],
"correct_answer": "O(log n)",
"category": "Technology",
"difficulty": "medium"
},
{
"type": "true_false",
"text": "Python lists are immutable.",
"correct_answer": "False",
"category": "Technology",
"difficulty": "easy"
}
]
}
Building this JSON file is itself an exercise in structured data design (Chapter 9) and file formats (Chapter 10). Use a consistent schema, validate against it when loading, and handle deviations gracefully.
Stretch Goals
Timed Questions (New skill)
Add an optional timer mode where players have a limited number of seconds to answer each question. Use Python's time module to measure elapsed time. Award bonus points for fast answers. This requires careful design — you can't interrupt input() easily in Python, so consider accepting the answer and then checking whether the elapsed time exceeded the limit.
Hint System (Extends Chapter 15)
Add a hint mechanism: players can request a hint that eliminates two wrong choices (for multiple choice) or provides a clue (for true/false). Requesting a hint halves the point value of the question. This exercises method design and state tracking within the Question class.
Open Trivia Database API (Extends Chapter 21)
Integrate with the free Open Trivia Database API to dynamically fetch questions. Use urllib.request or the requests library (Chapter 23) to make API calls. Parse the JSON response and create Question objects from the API data. Handle network errors gracefully — fall back to the local question bank if the API is unreachable.
Category Statistics (Extends Chapter 17)
Track and display long-term statistics: which categories a player performs best in, accuracy trends over time, average score by difficulty level. Store this data in the leaderboard file and display it as a "Player Profile."
Tournament Mode (Extends Chapter 16)
Implement a tournament bracket for 4+ players. Players compete head-to-head in pairs, winners advance to the next round. This requires more sophisticated game state management and exercises the observer pattern or state machine concepts from Chapter 16.
Question Import from CSV (Extends Chapter 10)
Allow users to import questions from a CSV file (e.g., one exported from a spreadsheet). Parse the CSV, validate each row, convert to your JSON format, and merge with the existing question bank. Handle duplicates by checking question text similarity.
What This Project Demonstrates
The Trivia Game Engine proves you can build an interactive, stateful application with:
- Polymorphism in action (Chapter 15): Different question types, same interface
- State management (Chapters 14, 16): Tracking players, turns, scores, and game progression through clean object design
- Data persistence (Chapter 10): Both the question bank and the leaderboard survive between sessions
- User experience (Chapters 4, 7, 11): Clear formatting, helpful error messages, and an interface that doesn't require reading the source code
- Testing (Chapter 13): Verifiable game logic separated from I/O
- Modular design (Chapters 12, 16): Each component is independent, testable, and replaceable
This project also demonstrates something the rubric can't fully capture: you built something people want to use. That's the difference between an assignment and a product.
Getting Started
Start with a single question, a single player, and a loop that asks and checks. That's Phase 1, and you can have it working in an afternoon. Everything else is iteration.
If you're stuck on the class hierarchy, sketch it on paper first. Draw boxes for Question, MultipleChoiceQuestion, TrueFalseQuestion, Player, Game, QuestionBank, and Leaderboard. Draw arrows showing which class uses which. The arrows should point in one direction — if two classes point at each other, you have a coupling problem to solve (Chapter 16).
If your game logic works in tests but not in the actual game, your display code is probably entangled with your logic code. Extract the logic into a method that returns a value, test the returned value, and have the CLI call the method and format the result. This is the most common architecture mistake in this project — and it's the same lesson from Chapter 16's discussion of separation of concerns.
Remember the Crypts of Pythonia running example? You've already built a game that manages state, responds to user input, and gets more sophisticated over time. You know how to do this. The Trivia Game Engine is bigger, but it's not conceptually different.
You've got this.
Assessment: See capstone-rubric.md for detailed grading criteria.