Capstone Goals
3 minBy the end of this project you can:
- Compose a multi-file Python project — classes split across modules.
- Combine classes, inheritance, dunders, generators, recursion and JSON in one app.
- Build an extensible game where adding a new monster takes ~5 lines.
- Save and load game state between sessions.
The Spec
5 minBuild Dungeon Quest. Folder structure:
dungeon_quest/ creatures.py Hero, Monster, child monster classes items.py Item, Weapon, Potion, Treasure maze.py Recursive maze generator (room tree) game.py Game class — orchestrates rooms, combat, save/load main.py CLI entry point: menu loop
The hero explores a dungeon. Each room either has a monster, a treasure, or both. Combat is polymorphic — each monster type fights differently. Defeated monsters drop loot. The dungeon is generated as a tree of rooms (a recursive structure). The whole state saves to JSON between sessions.
If you can build this you can build any text-based game. The patterns scale to graphical games too — the data shapes don't change, just the I/O.
Task 1 · creatures.py
10 minBuild the creature hierarchy. Pattern from PY-L3-07 + L3-13:
# creatures.py — Hero + Monster family with polymorphic attacks import random class Creature: def __init__(self, name, hp, attack_power): self.name = name self.hp = hp self.max_hp = hp self.attack_power = attack_power def is_alive(self): return self.hp > 0 def take_damage(self, n): self.hp = max(0, self.hp - n) def heal(self, n): self.hp = min(self.max_hp, self.hp + n) def attack(self, victim): dmg = self.attack_power + random.randint(-2, 2) victim.take_damage(dmg) print(f" {self.name} hits {victim.name} for {dmg}.") class Hero(Creature): def __init__(self, name, hp=120, attack_power=15): super().__init__(name, hp, attack_power) self.inventory = [] self.xp = 0 self.level = 1 self.gold = 0 def pick_up(self, item): self.inventory.append(item) def gain_xp(self, n): self.xp += n while self.xp >= self.level * 100: self.xp -= self.level * 100 self.level += 1 self.max_hp += 20 self.attack_power += 2 self.hp = self.max_hp print(f" ✨ {self.name} reaches Lv {self.level}!") def __repr__(self): return (f"Hero(name={self.name!r}, hp={self.hp}/{self.max_hp}, " f"lvl={self.level}, xp={self.xp}, gold={self.gold})") class Monster(Creature): """Base monster — children override attack().""" def __init__(self, name, hp, attack_power, xp_reward=30, gold_reward=10): super().__init__(name, hp, attack_power) self.xp_reward = xp_reward self.gold_reward = gold_reward class Goblin(Monster): def __init__(self): super().__init__("Goblin", hp=30, attack_power=8, xp_reward=20, gold_reward=5) class Orc(Monster): def __init__(self): super().__init__("Orc", hp=60, attack_power=14, xp_reward=50, gold_reward=15) class IceWraith(Monster): def __init__(self): super().__init__("Ice Wraith", hp=50, attack_power=10, xp_reward=40, gold_reward=12) def attack(self, victim): super().attack(victim) if random.random() < 0.3: print(f" ❄ {victim.name} is frozen — takes another hit!") super().attack(victim) class Dragon(Monster): def __init__(self): super().__init__("Dragon", hp=200, attack_power=22, xp_reward=200, gold_reward=100) def attack(self, victim): if random.random() < 0.3: dmg = self.attack_power + 10 print(f" 🔥 Dragon breathes fire for {dmg}!") victim.take_damage(dmg) else: super().attack(victim)
Task 2 · items.py
10 min# items.py — items the hero can find or pick up class Item: """Base item.""" def __init__(self, name, value): self.name = name self.value = value # for sale price def __repr__(self): return f"{type(self).__name__}({self.name!r})" class Weapon(Item): def __init__(self, name, value, attack_bonus): super().__init__(name, value) self.attack_bonus = attack_bonus def use(self, hero): hero.attack_power += self.attack_bonus print(f" ⚔ {hero.name} equips {self.name}. ATK +{self.attack_bonus}") class Potion(Item): def __init__(self, name, value, heal_amount): super().__init__(name, value) self.heal_amount = heal_amount def use(self, hero): hero.heal(self.heal_amount) print(f" 💖 {hero.name} drinks {self.name}. +{self.heal_amount} HP") # Remove from inventory after use if self in hero.inventory: hero.inventory.remove(self) class Treasure(Item): def __init__(self, name, value): super().__init__(name, value) def use(self, hero): hero.gold += self.value print(f" 💰 {hero.name} pockets {self.name}. +{self.value} gold") if self in hero.inventory: hero.inventory.remove(self)
Three item types. Each has a polymorphic use(hero) method. The game code calls item.use(hero) and the right thing happens — exactly the polymorphism pattern from PY-L3-11.
Task 3 · maze.py
10 minThe dungeon is a tree of rooms. Each Room has at most 2 children rooms. Recursion from PY-L3-31 builds it.
# maze.py — recursive room tree import random from creatures import Goblin, Orc, IceWraith, Dragon from items import Weapon, Potion, Treasure MONSTER_CLASSES = [Goblin, Orc, IceWraith] class Room: def __init__(self, depth, monster=None, item=None, left=None, right=None): self.depth = depth self.monster = monster self.item = item self.left = left self.right = right self.visited = False def make_room(depth, max_depth): if depth >= max_depth: # The final room contains the boss return Room(depth, monster=Dragon(), item=Treasure("Dragon's hoard", value=500)) # Most rooms have a monster, some have an item too monster = random.choice(MONSTER_CLASSES)() if random.random() < 0.7 else None if random.random() < 0.4: item = random.choice([ Weapon("steel sword", 50, attack_bonus=4), Potion("healing potion", 25, heal_amount=30), Treasure("gold coins", 30), ]) else: item = None return Room( depth=depth, monster=monster, item=item, left=make_room(depth + 1, max_depth), right=make_room(depth + 1, max_depth), ) def build_dungeon(max_depth=5): return make_room(0, max_depth)
The dungeon is a binary tree built recursively. make_room is the recursive function — base case at max_depth (boss room), otherwise build self + recurse into left + right children.
Task 4 · game.py + main.py
10 min# game.py — orchestrates the game import random from creatures import Hero from maze import build_dungeon, Room def fight(hero, monster): print(f"\n⚔ {hero.name} vs {monster.name}!") while hero.is_alive() and monster.is_alive(): hero.attack(monster) if monster.is_alive(): monster.attack(hero) if hero.is_alive(): print(f" 🏆 {hero.name} wins!") hero.gain_xp(monster.xp_reward) hero.gold += monster.gold_reward return True else: print(f" 💀 {hero.name} fell.") return False def explore(room, hero): """Recursive room walker — depth-first.""" if not hero.is_alive(): return False if room is None or room.visited: return True print(f"\n=== Room at depth {room.depth} ===") room.visited = True if room.monster is not None and room.monster.is_alive(): if not fight(hero, room.monster): return False if room.item is not None: print(f" 💎 Found: {room.item.name}") room.item.use(hero) # Pick a direction (or both) choice = random.choice(["left", "right", "both"]) if choice in ("left", "both"): if not explore(room.left, hero): return False if choice in ("right", "both"): if not explore(room.right, hero): return False return True def play(hero_name="Aisyah"): hero = Hero(hero_name) dungeon = build_dungeon(max_depth=4) random.seed(42) survived = explore(dungeon, hero) print(f"\n=== Result ===") print(hero) if survived: print("🏆 Dungeon cleared!") else: print("💀 Better luck next time.")
And the entry point:
# main.py from game import play name = input("Hero name: ").strip() or "Aisyah" play(name)
Test & Polish
8 minRun python main.py. The hero explores; fights monsters; picks up loot; reaches the boss; either wins or dies.
Polish ideas to add:
- JSON save — at the end of each room, save the hero (name, hp, xp, gold, inventory item names). Load on start.
- Player choice — instead of random left/right, ask the user.
- Use potions — after each fight, if hero has a potion and HP < 50%, use it.
- Shop room — occasional rooms where the hero can sell items for gold.
- High scores — save final XP/gold to a JSON file across runs.
Recap · Level 3 in Review
5 minForty-seven lessons. Here's the picture.
OOP foundations PY-L3-01..06 Inheritance + polymorphism PY-L3-07..13 Dunders + Pet Shop project PY-L3-14..17 Decorators (@property, @classmethod, @staticmethod) PY-L3-18..19 Functional Python PY-L3-20..24 Iterators + generators PY-L3-25..29 Functional Olympics PY-L3-30 Recursion fundamentals + art PY-L3-31..36 Search + sort + Big-O PY-L3-37..42 Data structures PY-L3-43..46 Dungeon Quest capstone PY-L3-47 PCEP exam prep PY-L3-48 (tomorrow)
You can now design class hierarchies, write functional pipelines, build generators, reason recursively, choose the right algorithm and the right data structure. That's most of what a working Python developer does.
Tomorrow's lesson 48 — PCEP exam prep. We'll walk through the exam format, run a 10-question mock, identify your weak areas, and lay out a study plan for the exam itself. Then Level 3 is done. Level 4 (Real-World Software & Databases) is waiting.
Capstone Homework
4 minPick at least two of the polish ideas above and implement them. Submit your final code (all five files) plus a screenshot of one playthrough where you beat the Dragon.
Ideas list:
- JSON save/load.
- Player choice at each fork.
- Auto-use potion logic.
- Shop rooms.
- High-score tracking across runs.
- Custom monster type (add to
creatures.py+MONSTER_CLASSES).
Sample · JSON save + player choice
# Add to game.py: import json def save_state(hero, path="save.json"): data = { "name": hero.name, "hp": hero.hp, "max_hp": hero.max_hp, "level": hero.level, "xp": hero.xp, "gold": hero.gold, "inventory": [item.name for item in hero.inventory], } with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) def load_state(path="save.json"): try: with open(path, encoding="utf-8") as f: data = json.load(f) hero = Hero(data["name"], hp=data["max_hp"]) hero.hp = data["hp"] hero.level = data["level"] hero.xp = data["xp"] hero.gold = data["gold"] return hero except FileNotFoundError: return None # Modify explore to ask the player: def explore_with_choice(room, hero): if not hero.is_alive() or room is None or room.visited: return hero.is_alive() room.visited = True print(f"\n=== Room at depth {room.depth} ===") if room.monster and room.monster.is_alive(): if not fight(hero, room.monster): return False if room.item: room.item.use(hero) save_state(hero) if room.left is None and room.right is None: return True while True: c = input("(L)eft / (R)ight / (B)oth: ").lower() if c in ("l", "r", "b"): break if c in ("l", "b"): if not explore_with_choice(room.left, hero): return False if c in ("r", "b"): if not explore_with_choice(room.right, hero): return False return True
Non-negotiables: at least two polish features. JSON save persists hero state — quit, restart, continue. Player choice converts a generated dungeon into an actual game. Together they make the project feel shippable.