26  Week 11 Project: Text Adventure Game

ImportantBefore You Start

Make sure you’ve completed: - All previous projects - Chapter 12: Interactive Systems - Understanding of GUI event handling and state management

You should be ready to: - Design complex interactive systems - Manage application state across time - Create engaging user experiences - Handle dynamic content generation

26.1 Project Overview

This project pushes interactive systems to their limits. You’ll create a text-based adventure game with a graphical interface, featuring dynamic storytelling, inventory management, character progression, and branching narratives.

This is where programming becomes storytelling - your code creates worlds!

26.2 The Problem to Solve

People love interactive stories with meaningful choices! Your text adventure should: - Present an engaging narrative with multiple paths - Respond dynamically to player choices - Manage complex game state (inventory, character stats, story progress) - Provide an immersive interface with visuals and audio cues - Save and load game progress - Create replayable experiences with different outcomes

26.3 Architect Your Solution First

Before writing any code or consulting AI, design your adventure game:

1. Story and Game Design

Plan your adventure: - Setting: Medieval fantasy? Space exploration? Modern mystery? - Main Quest: What’s the player trying to achieve? - Key Characters: Who will the player meet? - Major Locations: What places will they explore? - Choice Consequences: How do decisions affect the story?

2. Interface Design

Sketch your game window:

┌───────────────────────────────────────────────────────────────┐
│  🗡️ ADVENTURE GAME - The Crystal Caves                       │
├───────────────────────────────────────────────────────────────┤
│                    STORY DISPLAY                              │
│ ┌─────────────────────────────────────────────────────────┐   │
│ │ You stand at the entrance to the mysterious Crystal    │   │
│ │ Caves. Ancient runes glow faintly on the stone        │   │
│ │ archway. A cold wind whispers from within...          │   │
│ │                                                        │   │
│ │ To your left, you notice a rusted sword partially     │   │
│ │ buried in the ground. To your right, a narrow path    │   │
│ │ leads around the cave entrance.                        │   │
│ └─────────────────────────────────────────────────────────┘   │
├───────────────────────────────────────────────────────────────┤
│                     CHOICES                                   │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Enter the caves │ │ Examine sword   │ │ Take side path  │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
├───────────────────────────────────────────────────────────────┤
│ CHARACTER | INVENTORY     | GAME INFO                         │
│ Health: ❤️❤️❤️❤️❤️ | 🗡️ Rusty Sword  | Location: Cave Entrance  │
│ Magic:  ⭐⭐⭐      | 🧪 Health Potion| Choices Made: 3           │
│ Level: 1        | 💰 15 gold coins| Story Branch: A           │
└───────────────────────────────────────────────────────────────┘

3. Game State Architecture

Plan your data structures:

# Game state structure
game_state = {
    'player': {
        'name': 'Hero',
        'health': 100,
        'magic': 50,
        'level': 1,
        'experience': 0,
        'location': 'cave_entrance'
    },
    'inventory': [
        {'item': 'rusty_sword', 'type': 'weapon', 'damage': 5},
        {'item': 'health_potion', 'type': 'consumable', 'healing': 25}
    ],
    'story': {
        'current_scene': 'cave_entrance_01',
        'choices_made': ['examined_runes', 'talked_to_wizard'],
        'flags': {'has_sword': True, 'wizard_friendly': True},
        'branch': 'heroic_path'
    },
    'game_progress': {
        'scenes_visited': 15,
        'items_found': 3,
        'battles_won': 2,
        'save_time': '2024-03-15 16:30:00'
    }
}

26.4 Implementation Strategy

Phase 1: Core Game Engine

  1. Scene management system
  2. Choice handling and consequences
  3. Basic state tracking
  4. Simple navigation

Phase 2: Player Systems

  1. Character stats (health, magic, level)
  2. Inventory management
  3. Experience and leveling
  4. Combat system (if applicable)

Phase 3: Rich Interface

  1. Formatted story display
  2. Dynamic choice buttons
  3. Character/inventory panels
  4. Progress tracking

Phase 4: Advanced Features

  1. Save/load game functionality
  2. Multiple story branches
  3. Random events
  4. Achievement system

26.5 AI Partnership Guidelines

Effective Prompts for This Project

Good Learning Prompts:

"I'm building a text adventure game. I need a Scene class that stores 
story text, available choices, and consequences. Show me a simple 
structure with methods for displaying and handling choices."
"My adventure game needs to track player inventory. Show me how to 
add/remove items and display them in a tkinter Listbox with item 
descriptions on selection."
"I want to save game state to JSON and reload it later. Show me how 
to serialize my game state dictionary and restore it safely."

Avoid These Prompts: - “Create a full RPG with graphics and multiplayer” - “Build an AI that generates infinite storylines” - “Add 3D graphics and voice acting”

AI Learning Progression

  1. Architecture Phase: Game structure

    "I need to manage game scenes with story text and player choices. 
    What's a good design pattern for this? Show me a simple example."
  2. State Management: Complex data tracking

    "My adventure game tracks player stats, inventory, and story progress. 
    How do I organize this data and update it efficiently?"
  3. Interface Integration: GUI and game logic

    "How do I update tkinter widgets when game state changes? 
    Show me a pattern for keeping GUI in sync with game data."

26.6 Requirements Specification

Functional Requirements

Your text adventure must:

  1. Story System
    • Present narrative text engagingly
    • Offer meaningful player choices
    • Handle branching storylines
    • Support multiple endings
  2. Character Management
    • Track player stats (health, magic, level)
    • Manage inventory system
    • Handle character progression
    • Support item usage
  3. Game Flow
    • Navigate between scenes smoothly
    • Remember player choices and consequences
    • Provide save/load functionality
    • Show game progress and statistics
  4. User Interface
    • Display story text clearly
    • Present choices as clickable options
    • Show character status and inventory
    • Provide game controls (save, load, quit)

Learning Requirements

Your implementation should: - [ ] Use classes to organize game components - [ ] Manage complex application state - [ ] Create dynamic GUI updates - [ ] Handle user input and choices - [ ] Demonstrate file I/O for save games

26.7 Sample Interaction

Here’s how your text adventure might work:

🗡️ THE CRYSTAL CAVES ADVENTURE
═══════════════════════════════════════════════════════════════

[STORY PANEL]
═══════════════════════════════════════════════════════════════
You stand before the legendary Crystal Caves, where ancient magic 
is said to still flow through the crystalline walls. The entrance 
is carved with mystical runes that pulse with a faint blue light.

A weathered sign reads: "Those who enter with pure hearts may find 
what they seek. Those who enter with greed will find only danger."

As you approach, you notice three paths:
═══════════════════════════════════════════════════════════════

[CHOICES]
┌─────────────────────────────────────┐
│ 🚪 Enter through the main entrance  │  [BOLD APPROACH]
├─────────────────────────────────────┤
│ 🌿 Follow the narrow side path      │  [CAUTIOUS APPROACH]  
├─────────────────────────────────────┤
│ 📚 Study the runes more carefully   │  [SCHOLARLY APPROACH]
├─────────────────────────────────────┤
│ 🎒 Check your equipment first       │  [PREPARED APPROACH]
└─────────────────────────────────────┘

═══════════════════════════════════════════════════════════════
PLAYER STATUS:                   INVENTORY:
❤️  Health: 100/100             🗡️  Iron Sword (Damage: 10)
⭐ Magic:  30/50                🧪  Health Potion x2
🏆 Level: 2 (XP: 250/500)       🔑  Mysterious Key
🧭 Location: Cave Entrance      💰  45 Gold Pieces
                                📜  Ancient Map Fragment

GAME PROGRESS:                   ACHIEVEMENTS:
⏱️  Time Played: 45 minutes      ✅ First Steps (Enter the cave)
📍 Scenes Visited: 8            ✅ Collector (Find 5 items)
🎯 Main Quest: Find the Crystal  ⬜ Warrior (Win 3 battles)
📊 Completion: 15%               ⬜ Scholar (Solve 3 puzzles)
═══════════════════════════════════════════════════════════════

[GAME CONTROLS]
💾 Save Game    📁 Load Game    ⚙️ Settings    ❌ Quit

26.8 Development Approach

Step 1: Scene Management System

Create the core game structure:

class Scene:
    def __init__(self, scene_id, title, description, choices=None):
        self.scene_id = scene_id
        self.title = title
        self.description = description
        self.choices = choices or []
        self.visited = False
        self.items = []
        self.characters = []
    
    def add_choice(self, text, consequence, condition=None):
        """Add a choice with optional condition"""
        choice = {
            'text': text,
            'consequence': consequence,
            'condition': condition,
            'available': True
        }
        self.choices.append(choice)
    
    def get_available_choices(self, game_state):
        """Get choices available based on current game state"""
        available = []
        for choice in self.choices:
            if choice['condition'] is None or choice['condition'](game_state):
                available.append(choice)
        return available

class StoryEngine:
    def __init__(self):
        self.scenes = {}
        self.current_scene = None
        self.create_story()
    
    def create_story(self):
        """Create all game scenes and connections"""
        # Cave entrance
        entrance = Scene(
            'cave_entrance',
            'The Crystal Caves Entrance',
            """You stand before the legendary Crystal Caves. Ancient runes 
            glow with mystical energy on the stone archway. A sign warns 
            of dangers within, but also speaks of great treasures for the 
            worthy."""
        )
        
        entrance.add_choice(
            "Enter the caves boldly",
            {'next_scene': 'main_tunnel', 'player_change': {'courage': +1}}
        )
        
        entrance.add_choice(
            "Study the runes first",
            {'next_scene': 'rune_study', 'player_change': {'wisdom': +1}}
        )
        
        entrance.add_choice(
            "Look for another entrance",
            {'next_scene': 'side_path', 'player_change': {'caution': +1}}
        )
        
        self.scenes['cave_entrance'] = entrance
        
        # Add more scenes...
        self.create_main_tunnel()
        self.create_rune_study()
        self.create_side_path()
    
    def get_scene(self, scene_id):
        """Get a scene by ID"""
        return self.scenes.get(scene_id)
    
    def process_choice(self, choice, game_state):
        """Process a player's choice and update game state"""
        consequence = choice['consequence']
        
        # Change scene
        if 'next_scene' in consequence:
            self.current_scene = consequence['next_scene']
        
        # Update player stats
        if 'player_change' in consequence:
            for stat, change in consequence['player_change'].items():
                if stat in game_state['player']:
                    game_state['player'][stat] = game_state['player'].get(stat, 0) + change
        
        # Add items
        if 'add_item' in consequence:
            game_state['inventory'].append(consequence['add_item'])
        
        # Set story flags
        if 'set_flag' in consequence:
            for flag, value in consequence['set_flag'].items():
                game_state['story']['flags'][flag] = value
        
        return game_state

Step 2: Player Management

Handle character stats and inventory:

class Player:
    def __init__(self, name="Hero"):
        self.name = name
        self.health = 100
        self.max_health = 100
        self.magic = 50
        self.max_magic = 50
        self.level = 1
        self.experience = 0
        self.stats = {
            'courage': 0,
            'wisdom': 0,
            'caution': 0
        }
    
    def take_damage(self, amount):
        """Reduce health by amount"""
        self.health = max(0, self.health - amount)
        return self.health <= 0  # Return True if player died
    
    def heal(self, amount):
        """Restore health"""
        self.health = min(self.max_health, self.health + amount)
    
    def use_magic(self, amount):
        """Use magic if available"""
        if self.magic >= amount:
            self.magic -= amount
            return True
        return False
    
    def gain_experience(self, amount):
        """Add experience and check for level up"""
        self.experience += amount
        if self.experience >= self.level * 100:
            self.level_up()
    
    def level_up(self):
        """Level up the player"""
        self.level += 1
        self.experience = 0
        self.max_health += 20
        self.max_magic += 10
        self.health = self.max_health  # Full heal on level up
        self.magic = self.max_magic
        return True

class Inventory:
    def __init__(self):
        self.items = []
        self.max_capacity = 20
    
    def add_item(self, item):
        """Add item to inventory if space available"""
        if len(self.items) < self.max_capacity:
            self.items.append(item)
            return True
        return False
    
    def remove_item(self, item_name):
        """Remove item from inventory"""
        for i, item in enumerate(self.items):
            if item.get('name') == item_name:
                return self.items.pop(i)
        return None
    
    def has_item(self, item_name):
        """Check if inventory contains item"""
        return any(item.get('name') == item_name for item in self.items)
    
    def get_items_by_type(self, item_type):
        """Get all items of a specific type"""
        return [item for item in self.items if item.get('type') == item_type]
    
    def use_item(self, item_name, player):
        """Use an item and apply its effects"""
        item = self.remove_item(item_name)
        if item and item.get('type') == 'consumable':
            if 'healing' in item:
                player.heal(item['healing'])
                return f"Used {item['name']} and restored {item['healing']} health!"
            elif 'magic_restore' in item:
                player.magic = min(player.max_magic, player.magic + item['magic_restore'])
                return f"Used {item['name']} and restored {item['magic_restore']} magic!"
        return "Item cannot be used."

Step 3: GUI Integration

Connect the game engine to the interface:

import tkinter as tk
from tkinter import scrolledtext, messagebox
import json

class AdventureGameGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("🗡️ The Crystal Caves Adventure")
        self.root.geometry("900x700")
        
        # Initialize game components
        self.story_engine = StoryEngine()
        self.player = Player()
        self.inventory = Inventory()
        self.game_state = self.create_initial_state()
        
        self.create_interface()
        self.start_game()
    
    def create_interface(self):
        # Main title
        title_frame = tk.Frame(self.root, bg='darkblue', height=50)
        title_frame.pack(fill='x')
        title_frame.pack_propagate(False)
        
        title_label = tk.Label(title_frame, text="🗡️ THE CRYSTAL CAVES ADVENTURE", 
                              font=('Arial', 16, 'bold'), fg='white', bg='darkblue')
        title_label.pack(expand=True)
        
        # Story display area
        story_frame = tk.Frame(self.root)
        story_frame.pack(fill='both', expand=True, padx=10, pady=5)
        
        tk.Label(story_frame, text="STORY", font=('Arial', 12, 'bold')).pack(anchor='w')
        
        self.story_text = scrolledtext.ScrolledText(
            story_frame, height=15, wrap=tk.WORD, 
            font=('Arial', 11), bg='lightyellow'
        )
        self.story_text.pack(fill='both', expand=True)
        
        # Choices frame
        choices_frame = tk.Frame(self.root)
        choices_frame.pack(fill='x', padx=10, pady=5)
        
        tk.Label(choices_frame, text="CHOICES", font=('Arial', 12, 'bold')).pack(anchor='w')
        
        self.choices_frame = tk.Frame(choices_frame)
        self.choices_frame.pack(fill='x')
        
        # Status panel
        status_frame = tk.Frame(self.root, bg='lightgray', height=100)
        status_frame.pack(fill='x', padx=10, pady=5)
        status_frame.pack_propagate(False)
        
        # Split status into three columns
        player_frame = tk.Frame(status_frame, bg='lightgray')
        player_frame.pack(side='left', fill='both', expand=True)
        
        inventory_frame = tk.Frame(status_frame, bg='lightgray')
        inventory_frame.pack(side='left', fill='both', expand=True)
        
        progress_frame = tk.Frame(status_frame, bg='lightgray')
        progress_frame.pack(side='left', fill='both', expand=True)
        
        # Player status
        tk.Label(player_frame, text="PLAYER STATUS", font=('Arial', 10, 'bold'), 
                bg='lightgray').pack()
        self.player_status = tk.Label(player_frame, text="", justify='left', 
                                     bg='lightgray', font=('Arial', 9))
        self.player_status.pack()
        
        # Inventory
        tk.Label(inventory_frame, text="INVENTORY", font=('Arial', 10, 'bold'), 
                bg='lightgray').pack()
        self.inventory_status = tk.Label(inventory_frame, text="", justify='left', 
                                        bg='lightgray', font=('Arial', 9))
        self.inventory_status.pack()
        
        # Progress
        tk.Label(progress_frame, text="PROGRESS", font=('Arial', 10, 'bold'), 
                bg='lightgray').pack()
        self.progress_status = tk.Label(progress_frame, text="", justify='left', 
                                       bg='lightgray', font=('Arial', 9))
        self.progress_status.pack()
        
        # Control buttons
        control_frame = tk.Frame(self.root)
        control_frame.pack(fill='x', padx=10, pady=5)
        
        tk.Button(control_frame, text="💾 Save Game", 
                 command=self.save_game).pack(side='left', padx=5)
        tk.Button(control_frame, text="📁 Load Game", 
                 command=self.load_game).pack(side='left', padx=5)
        tk.Button(control_frame, text="🎒 Use Item", 
                 command=self.show_inventory_dialog).pack(side='left', padx=5)
        tk.Button(control_frame, text="❌ Quit", 
                 command=self.quit_game).pack(side='right', padx=5)
    
    def start_game(self):
        """Start the adventure"""
        self.story_engine.current_scene = 'cave_entrance'
        self.display_current_scene()
    
    def display_current_scene(self):
        """Display the current scene and update interface"""
        scene = self.story_engine.get_scene(self.story_engine.current_scene)
        if not scene:
            return
        
        # Mark scene as visited
        scene.visited = True
        
        # Clear and update story text
        self.story_text.delete(1.0, tk.END)
        self.story_text.insert(tk.END, f"{scene.title}\n\n")
        self.story_text.insert(tk.END, scene.description)
        
        # Clear previous choices
        for widget in self.choices_frame.winfo_children():
            widget.destroy()
        
        # Display available choices
        available_choices = scene.get_available_choices(self.game_state)
        for i, choice in enumerate(available_choices):
            btn = tk.Button(
                self.choices_frame, 
                text=f"{i+1}. {choice['text']}", 
                command=lambda c=choice: self.make_choice(c),
                width=40, height=2, wraplength=300
            )
            btn.pack(pady=2, fill='x')
        
        # Update status displays
        self.update_status_displays()
    
    def make_choice(self, choice):
        """Process a player choice"""
        # Update game state based on choice
        self.game_state = self.story_engine.process_choice(choice, self.game_state)
        
        # Add choice to history
        self.game_state['story']['choices_made'].append(choice['text'])
        
        # Display the scene
        self.display_current_scene()
        
        # Check for special events
        self.check_random_events()
    
    def update_status_displays(self):
        """Update all status displays"""
        # Player status
        player_text = f"""❤️ Health: {self.player.health}/{self.player.max_health}
⭐ Magic: {self.player.magic}/{self.player.max_magic}
🏆 Level: {self.player.level} (XP: {self.player.experience})
🧭 Location: {self.story_engine.current_scene.replace('_', ' ').title()}"""
        self.player_status.config(text=player_text)
        
        # Inventory
        if self.inventory.items:
            inventory_text = "\n".join([f"• {item.get('name', 'Unknown')}" 
                                      for item in self.inventory.items[:5]])
            if len(self.inventory.items) > 5:
                inventory_text += f"\n... and {len(self.inventory.items) - 5} more"
        else:
            inventory_text = "Empty"
        self.inventory_status.config(text=inventory_text)
        
        # Progress
        progress_text = f"""⏱️ Scenes Visited: {len([s for s in self.story_engine.scenes.values() if s.visited])}
🎯 Choices Made: {len(self.game_state['story']['choices_made'])}
📊 Items Found: {len(self.inventory.items)}"""
        self.progress_status.config(text=progress_text)

    def save_game(self):
        """Save current game state"""
        save_data = {
            'player': {
                'name': self.player.name,
                'health': self.player.health,
                'max_health': self.player.max_health,
                'magic': self.player.magic,
                'max_magic': self.player.max_magic,
                'level': self.player.level,
                'experience': self.player.experience,
                'stats': self.player.stats
            },
            'inventory': self.inventory.items,
            'current_scene': self.story_engine.current_scene,
            'game_state': self.game_state
        }
        
        try:
            with open('adventure_save.json', 'w') as f:
                json.dump(save_data, f, indent=2)
            messagebox.showinfo("Save Game", "Game saved successfully!")
        except Exception as e:
            messagebox.showerror("Save Error", f"Could not save game: {e}")
    
    def load_game(self):
        """Load saved game state"""
        try:
            with open('adventure_save.json', 'r') as f:
                save_data = json.load(f)
            
            # Restore player
            player_data = save_data['player']
            self.player.name = player_data['name']
            self.player.health = player_data['health']
            self.player.max_health = player_data['max_health']
            self.player.magic = player_data['magic']
            self.player.max_magic = player_data['max_magic']
            self.player.level = player_data['level']
            self.player.experience = player_data['experience']
            self.player.stats = player_data['stats']
            
            # Restore inventory
            self.inventory.items = save_data['inventory']
            
            # Restore scene
            self.story_engine.current_scene = save_data['current_scene']
            
            # Restore game state
            self.game_state = save_data['game_state']
            
            self.display_current_scene()
            messagebox.showinfo("Load Game", "Game loaded successfully!")
            
        except FileNotFoundError:
            messagebox.showerror("Load Error", "No saved game found!")
        except Exception as e:
            messagebox.showerror("Load Error", f"Could not load game: {e}")

# Run the game
if __name__ == "__main__":
    root = tk.Tk()
    game = AdventureGameGUI(root)
    root.mainloop()

26.9 Advanced Features

Random Events System

import random

class RandomEvents:
    def __init__(self):
        self.events = [
            {
                'name': 'treasure_find',
                'chance': 0.1,
                'description': 'You discover a hidden treasure!',
                'consequence': {'add_item': {'name': 'Gold Coins', 'value': 50}}
            },
            {
                'name': 'magic_surge',
                'chance': 0.05,
                'description': 'A wave of magic energy flows through you!',
                'consequence': {'player_change': {'magic': +20}}
            }
        ]
    
    def check_for_event(self, game_state):
        """Check if a random event occurs"""
        for event in self.events:
            if random.random() < event['chance']:
                return event
        return None

Achievement System

class AchievementManager:
    def __init__(self):
        self.achievements = {
            'first_choice': {'name': 'Decision Maker', 'description': 'Made your first choice'},
            'item_collector': {'name': 'Collector', 'description': 'Found 5 items'},
            'explorer': {'name': 'Explorer', 'description': 'Visited 10 scenes'},
            'level_up': {'name': 'Growing Strong', 'description': 'Reached level 2'}
        }
        self.unlocked = set()
    
    def check_achievements(self, game_state):
        """Check for newly unlocked achievements"""
        newly_unlocked = []
        
        # Check various conditions
        if len(game_state['story']['choices_made']) >= 1 and 'first_choice' not in self.unlocked:
            self.unlocked.add('first_choice')
            newly_unlocked.append('first_choice')
        
        # Add more achievement checks...
        
        return newly_unlocked

26.10 Common Pitfalls and Solutions

Pitfall 1: Overly Complex Story Branching

Problem: Too many story paths become unmanageable Solution: Use flags and conditions to merge paths intelligently

Pitfall 2: No Save/Load Validation

Problem: Corrupted save files crash the game Solution: Validate save data and provide fallbacks

Pitfall 3: Static Choices

Problem: Same choices available regardless of player state Solution: Use conditions to make choices dynamic

Pitfall 4: Poor State Management

Problem: Game state becomes inconsistent Solution: Centralize state updates through clear methods

26.11 Testing Your Adventure

Test Scenarios

  1. Complete Playthroughs: Multiple paths to different endings
  2. Save/Load: Save at various points and reload
  3. Edge Cases: Player at 0 health, full inventory
  4. Choice Validation: Conditional choices appear/disappear correctly
  5. State Persistence: All progress carries between sessions

26.12 Reflection Questions

After completing the project:

  1. Interactive Design: What made choices feel meaningful vs arbitrary?
  2. State Complexity: How did you manage all the interconnected data?
  3. Player Engagement: What kept players invested in the story?
  4. Technical Challenges: Which systems were hardest to implement?

26.13 Next Week Preview

Outstanding work! Next week, you’ll create the capstone project - a Todo GUI application that demonstrates everything you’ve learned about software architecture. You’ll design a complete application from scratch using all your skills!

Your text adventure proves you can create engaging, interactive experiences with complex state management - the foundation of game development and interactive applications! 🗡️