Game Character System (Capstone)
You've learned the foundations of object-oriented programming across the first four lessons: classes and objects, constructors and attributes, and encapsulation with multiple method types. Now it's time to bring everything together by building a real system that demonstrates professional OOP design.
This capstone lesson guides you through building a turn-based game character system—a project that integrates every concept from Chapter 24 and demonstrates how professional developers think about object-oriented design. You won't just apply concepts; you'll synthesize them into a cohesive, extensible system.
Project Overview: What We're Building
Imagine a text-based turn-based combat game where:
- A Player character fights Enemy characters
- Characters have Health (protected with a property), Name, and Level
- Players have an Inventory and Experience
- Enemies have Loot they drop when defeated
- A Combat System manages turn-based battles
This is more than an exercise—it's a real architecture pattern used in game development, AI agents, and any system with multiple interacting entities.
Why This Capstone Matters
By the end of this lesson, you'll understand:
- How to design classes that work together
- When to use inheritance (brief preview of Chapter 25)
- How encapsulation protects data in multi-class systems
- How to organize code for extension and maintenance
- How to collaborate with AI on system architecture
Design Phase: Planning Your Classes
Before writing code, let's plan the architecture. Professional developers do this step—they describe their intent to teammates (or AI!) before diving into implementation.
💬 AI Colearning Prompt
Ask your AI Co-Teacher:
"I'm building a turn-based combat system. Ask me clarifying questions about what I'm trying to build, then suggest a class structure. Here's my vision: Players fight enemies in turn-based combat, gain experience and level up, collect items, and manage health with a property for validation."
Expected outcome: You'll practice architectural thinking with AI as your design partner. AI will help you identify classes, responsibilities, and relationships before coding.
Key Design Decisions
Here's the architecture we'll implement:
| Class | Purpose | Key Attributes | Key Methods |
|---|---|---|---|
| Character | Base class for all characters | name, health (protected), level | attack() [abstract], take_damage() |
| Player | Player character | inventory, experience | attack(), add_item(), use_item(), gain_experience() |
| Enemy | Enemy character | loot, difficulty | attack() [specialized], drop_loot() |
| Item | Inventory item | name, heal_amount | (simple value object) |
| Combat | Static methods for battle logic | none (utilities) | battle() [static], calculate_critical_hit() [static] |
🎓 Instructor Commentary
In AI-native development, system design is about clarity. When you describe your architecture to AI, you're not memorizing syntax—you're thinking about responsibilities, boundaries, and how components interact. This skill transfers directly to designing AI agents where each agent is an object with clear responsibilities.
Part 1: Building the Character Base Class
The Character class is the foundation. All characters (players, enemies) share common attributes like name, health, and level. They all need to attack and take damage. This is perfect for an abstract base class.
from abc import ABC, abstractmethod
class Character(ABC):
"""Base class for all game characters (Player, Enemy, etc.)"""
def __init__(self, name: str, health: int, level: int = 1):
self.name = name
self._health = health # Protected - use property
self._max_health = health # Track maximum for healing
self.level = level
@property
def health(self) -> int:
"""Current health with bounds validation"""
return self._health
@health.setter
def health(self, value: int) -> None:
"""Set health, keeping it within [0, max_health]"""
self._health = max(0, min(value, self._max_health))
@property
def is_alive(self) -> bool:
"""Computed property - derived from health"""
return self._health > 0
@abstractmethod
def attack(self, target: 'Character') -> int:
"""All characters MUST implement attack behavior"""
pass
def take_damage(self, amount: int) -> None:
"""Receive damage and apply it through property setter"""
self.health -= amount
status = "Alive" if self.is_alive else "Defeated"
print(f"{self.name} took {amount} damage. "
f"Health: {self.health}/{self._max_health} HP [{status}]")
def __str__(self) -> str:
"""String representation for display during combat"""
status = "Alive" if self.is_alive else "Defeated"
return f"{self.name} (Lv.{self.level}) - {self.health}/{self._max_health} HP [{status}]"
Design decisions explained:
_healthis protected (not public) because it shouldn't be modified directly—it needs validation- Property
healthacts as a getter/setter with built-in bounds checking is_aliveis a computed property—it's derived from health, not stored separatelyattack()is abstract—each character type attacks differently (we'll see this in Player and Enemy)
🎓 Instructor Commentary
Notice how encapsulation isn't about hiding code—it's about creating a contract. When you use
self.health -= amount, you're relying on the property setter to enforce bounds. Any code using the Character class knows: "Health is always between 0 and max_health." That's a powerful guarantee.
🚀 CoLearning Challenge
Ask your AI:
"In the Character class, why do we use a property for health instead of letting external code directly modify
_health? Give me 3 scenarios where the property protection matters."
Expected outcome: You'll understand encapsulation as a contract mechanism, not just data hiding.
Part 2: Building the Player Class
The Player class extends Character and adds player-specific features: inventory management and experience tracking.
class Item:
"""Simple item in inventory"""
def __init__(self, name: str, heal_amount: int):
self.name = name
self.heal_amount = heal_amount
class Player(Character):
"""Player character with inventory and progression"""
def __init__(self, name: str, health: int = 100):
super().__init__(name, health, level=1)
self.inventory: list[Item] = []
self.experience = 0
self._experience_to_level = 100
def attack(self, target: Character) -> int:
"""Player attack scales with level"""
damage = 10 * self.level
print(f"{self.name} attacks {target.name} for {damage} damage!")
target.take_damage(damage)
return damage
def add_item(self, item: Item) -> None:
"""Add item to inventory"""
self.inventory.append(item)
print(f"{self.name} obtained {item.name}!")
def use_item(self, item_name: str) -> bool:
"""Use item from inventory by name"""
for item in self.inventory:
if item.name == item_name:
self.health += item.heal_amount # Uses property setter
self.inventory.remove(item)
print(f"{self.name} used {item.name} and healed {item.heal_amount} HP!")
return True
print(f"{item_name} not found in inventory.")
return False
def gain_experience(self, amount: int) -> None:
"""Gain XP and check for level up"""
self.experience += amount
print(f"{self.name} gained {amount} XP!")
# Check if we've accumulated enough XP to level
while self.experience >= self._experience_to_level:
self.level_up()
def level_up(self) -> None:
"""Increase level and boost stats"""
self.level += 1
self._max_health += 20
self.health = self._max_health # Full heal on level up
self.experience -= self._experience_to_level
self._experience_to_level = int(self._experience_to_level * 1.5)
print(f"🎉 {self.name} reached level {self.level}! Max HP: {self._max_health}")
Key techniques demonstrated:
- Inheritance:
Player(Character)reuses Character's constructor and health system super().__init__(): Calls parent constructor to initialize character data- Instance methods:
attack(),add_item(),use_item()operate on instance data - List attributes:
inventorystores Item objects, typed aslist[Item] - Property usage:
use_item()sets health through the property setter, ensuring validation
🚀 CoLearning Challenge
Ask your AI:
"Add a mana system to Player. Create
_mana(protected), amanaproperty with getter/setter, and implementmagic_attack()that costs mana. Include mana regeneration after each turn."
Expected outcome: You'll practice encapsulation and properties on a new feature.
Part 3: Building the Enemy Class
The Enemy class also extends Character but with different behavior: enemies scale difficulty and drop loot.
class Enemy(Character):
"""Enemy character with difficulty scaling and loot"""
def __init__(self, name: str, health: int, level: int, difficulty: str):
super().__init__(name, health, level)
self.difficulty = difficulty # "easy", "normal", "hard"
self.loot: dict[str, int] = {} # item_name -> quantity
def attack(self, target: Character) -> int:
"""Enemy attack scales with difficulty multiplier"""
base_damage = 8 * self.level
multiplier = {
"easy": 0.7,
"normal": 1.0,
"hard": 1.5
}.get(self.difficulty, 1.0)
damage = int(base_damage * multiplier)
print(f"{self.name} attacks {target.name} for {damage} damage!")
target.take_damage(damage)
return damage
def drop_loot(self) -> dict[str, int]:
"""Return loot when defeated"""
if not self.is_alive:
print(f"{self.name} dropped: {self.loot}")
return self.loot
return {}
Design patterns:
- Inheritance with specialization: Player and Enemy both attack, but differently
- Difficulty multiplier: Uses a dictionary for lookup—scalable design
- Loot dictionary:
dict[str, int]maps item names to quantities - Conditional logic: Check
is_aliveproperty before dropping loot
✨ Teaching Tip
Use Claude Code to explore polymorphism concepts: "In the Character system, both Player and Enemy implement
attack()differently. What's the benefit of this design compared to having oneattack()method that checks the character type?"
Part 4: Building the Combat System
The Combat class uses static methods—it's a utility for managing battles. Notice it doesn't have instance data; it's just a collection of related functions.
class Combat:
"""Static methods for battle management"""
@staticmethod
def battle(player: Player, enemy: Enemy) -> bool:
"""Execute turn-based combat until one is defeated"""
print(f"\n⚔️ Battle: {player.name} vs {enemy.name}\n")
turn = 1
while player.is_alive and enemy.is_alive:
print(f"--- Turn {turn} ---")
# Player attacks first
player.attack(enemy)
if not enemy.is_alive:
print(f"\n🏆 {player.name} defeated {enemy.name}!")
xp_reward = enemy.level * 50
player.gain_experience(xp_reward)
loot = enemy.drop_loot()
return True
# Enemy counter-attacks
enemy.attack(player)
if not player.is_alive:
print(f"\n💀 {player.name} was defeated by {enemy.name}.")
return False
# Show status
print(f"{player}")
print(f"{enemy}\n")
turn += 1
return player.is_alive
@staticmethod
def calculate_critical_hit(base_damage: int, crit_chance: float = 0.1) -> int:
"""Calculate damage with critical hit chance"""
import random
if random.random() < crit_chance:
print("💥 CRITICAL HIT!")
return int(base_damage * 2)
return base_damage
Static method insights:
- No
selfparameter—doesn't operate on instance data - Used for pure functions and utilities related to a class concept
- Perfect for Combat: it's "related to characters" conceptually, but not attached to any specific character
💬 AI Colearning Prompt
"In this Combat class,
battle()is a static method. Could we make it an instance method instead? What would be the pros and cons of each approach? When is static vs instance the right choice?"
Part 5: Putting It All Together
Now let's see the complete system in action:
# Create player
hero = Player("Arin", health=120)
# Create enemies with loot
goblin = Enemy("Goblin", health=50, level=1, difficulty="easy")
goblin.loot = {"Gold Coin": 5, "Health Potion": 1}
orc = Enemy("Orc Warrior", health=100, level=2, difficulty="normal")
orc.loot = {"Gold Coin": 15, "Iron Sword": 1}
# Equip player
hero.add_item(Item("Health Potion", 30))
# Execute battles
print("=== Game Start ===\n")
if Combat.battle(hero, goblin):
print("\nVictory! Next challenge...\n")
if Combat.battle(hero, orc):
print("\n🎊 All enemies defeated! You win!")
else:
print("\nGame Over - You were defeated")
else:
print("\nGame Over - You were defeated")
Expected output (abbreviated):
=== Game Start ===
⚔️ Battle: Arin vs Goblin
--- Turn 1 ---
Arin attacks Goblin for 10 damage!
Goblin took 10 damage. Health: 40/50 HP [Alive]
Goblin attacks Arin for 5 damage!
Arin took 5 damage. Health: 115/120 HP [Alive]
[... turns continue ...]
🏆 Arin defeated Goblin!
Arin gained 50 XP!
Goblin dropped: {'Gold Coin': 5, 'Health Potion': 1}
Victory! Next challenge...
⚔️ Battle: Arin vs Orc Warrior
[... battle ...]
🚀 CoLearning Challenge
Ask your AI Co-Teacher:
"Design a Boss class that has special abilities. Create a spell_attack() method that does more damage. Then implement a shop system: a Shop class that manages items the player can buy/sell. Design how Player and Shop interact."
Expected outcome: You'll practice extending the system with new features and managing interactions between classes.
Part 6: Common Design Pitfalls and How to Avoid Them
When building multi-class systems, watch for these patterns:
Pitfall 1: Violating Encapsulation
❌ Bad: Direct modification of health bypasses validation
player.health = 999 # Bypasses bounds checking!
player._max_health = 1 # Breaks level-up logic
✅ Good: Use properties or methods
player.health = 999 # Actually sets to min(999, max_health)
# Use gain_experience() to level up, which adjusts _max_health correctly
Pitfall 2: Inheritance vs Composition Confusion
This system uses inheritance (Player and Enemy extend Character). In Chapter 25, you'll learn when composition (having objects contain other objects) is better.
For now: if classes share common behavior and ARE variations of a concept, use inheritance. Player and Enemy ARE both Characters, so inheritance fits.
Pitfall 3: Not Using Type Hints
❌ Bad: Hard to understand what methods expect
def use_item(self, item): # What type is item? String name or Item object?
✅ Good: Type hints make intent clear
def use_item(self, item_name: str) -> bool: # Clearly a string name
def add_item(self, item: Item) -> None: # Clearly an Item object
Pitfall 4: Static Methods That Should Be Instance Methods
❌ Bad: Why is this a static method?
@staticmethod
def take_damage(self, amount): # Has self? This is confused
✅ Good: Instance methods for character behavior
def take_damage(self, amount: int) -> None: # Real instance method
Static is for: Utilities (calculate_critical_hit), factories (from_file())
Instance is for: Operating on character data (attack, take_damage)
🎓 Instructor Commentary
The Game Character System demonstrates what professional developers do: encapsulate data (health with property), organize behavior (instance/class/static methods), establish contracts (ABC abstract methods), and manage interactions (character relationships in Combat). This pattern appears in every professional system—from web frameworks to AI agents. You've just learned the architecture of scalable software.
Testing Your System
Before extending with new features, test each component independently:
# Test 1: Character health bounds
player = Player("Test", 50)
player.health = 100 # Should max at 100
assert player.health == 50, "Health capped at max_health"
# Test 2: Inventory management
item = Item("Potion", 20)
player.add_item(item)
assert len(player.inventory) == 1
assert player.use_item("Potion") == True
# Test 3: Enemy loot drops
enemy = Enemy("Goblin", 30, 1, "easy")
enemy.loot = {"Gold": 5}
enemy.health = 0
loot = enemy.drop_loot()
assert loot == {"Gold": 5}
This is unit testing—a fundamental practice in professional development. AI can help generate tests, but you should understand what each test validates.
✨ Teaching Tip
Use Claude Code to generate tests: "Write unit tests for the Player class. Test health capping, XP gain, item use, and level up. Then run them and fix any failures."
Try With AI
You've now built a complete object-oriented system integrating all Chapter 24 concepts. Use your AI companion to validate your understanding and extend the system.
Prompt 1: Recall — Design Review
Review the Game Character System code. Identify and list all OOP concepts from Chapter 24:
- Encapsulation patterns (public/protected/private)
- Property decorators
- Instance/class/static methods
- Abstract base classes
- Inheritance
For each, point to the specific code line where it appears.
Expected outcome: You'll recognize all Chapter 24 concepts integrated into working code. This validates your synthesis of the entire chapter.
Prompt 2: Understand — Design Reasoning
Why did we design the system this way?
1. Why is health protected (_health) with a property instead of public?
2. Why does Player extend Character instead of being independent?
3. Why is Combat.battle() a static method instead of an instance method?
4. Why is attack() abstract in Character instead of having a default implementation?
Explain the reasoning for each design choice.
Expected outcome: You'll understand design tradeoffs and principles behind professional OOP, not just syntax.
Prompt 3: Apply — Extending the System
Design a new Merchant class. Merchants are NPCs that don't fight but sell items.
Questions:
- Should Merchant extend Character? Why or why not?
- What attributes should Merchant have?
- What methods (instance, class, static) should it have?
- How does it interact with Player?
Implement your design and test it works with the existing system.
Expected outcome: You'll practice class design and integration decisions, extending beyond the capstone.
Prompt 4: Synthesize — From Game to AI Agents
The Game Character System is actually a pattern used in AI-native development.
Map the concepts:
- Character → Agent (has state and behavior)
- Player → ChatAgent (user-facing agent)
- Enemy → OpponentAgent (system agent)
- Combat.battle() → Negotiation logic
- Inventory → Agent memory/context
How would you redesign this system as a multi-agent AI system instead of a game?
What would change? What would stay the same?
Design the architecture and implement a simple ChatAgent class using this pattern.
Expected outcome: You'll connect OOP Part I concepts to professional AI development, preparing for advanced topics in later chapters.
Checklist: Did You Achieve the Learning Objectives?
By the end of this capstone, you should be able to check off:
- I can describe the architecture of a multi-class system (Character → Player/Enemy)
- I can explain why each class design choice was made (inheritance, encapsulation, method types)
- I can identify all Chapter 24 concepts in real, working code
- I can extend the system with new features (Merchant, Boss, Shop)
- I can design a system from requirements, not just implement given code
- I understand how OOP patterns apply to AI agents, not just games
- I can collaborate with AI to plan and refine system architecture