Skip to main content

Strategy Game Mechanics - Python Implementation

Complete Strategy Game Implementation in Python

This is the complete Python/Pygame implementation of a strategy game with all core mechanics including resource management, unit control, pathfinding, base building, and AI opponents.

Full Strategy Game Code

import pygame
import random
import math
import heapq
from enum import Enum
from typing import List, Tuple, Optional, Dict
from dataclasses import dataclass

class UnitType(Enum):
    WORKER = "worker"
    SOLDIER = "soldier"
    ARCHER = "archer"
    CAVALRY = "cavalry"
    SIEGE = "siege"

class BuildingType(Enum):
    BASE = "base"
    BARRACKS = "barracks"
    MINE = "mine"
    FARM = "farm"
    TOWER = "tower"
    WALL = "wall"

@dataclass
class Resources:
    gold: int = 0
    food: int = 0
    wood: int = 0
    
    def can_afford(self, cost: 'Resources') -> bool:
        return (self.gold >= cost.gold and 
                self.food >= cost.food and 
                self.wood >= cost.wood)
    
    def subtract(self, cost: 'Resources'):
        self.gold -= cost.gold
        self.food -= cost.food
        self.wood -= cost.wood
    
    def add(self, income: 'Resources'):
        self.gold += income.gold
        self.food += income.food
        self.wood += income.wood

class Unit:
    """Strategy game unit"""
    
    def __init__(self, unit_type: UnitType, x: int, y: int, faction: str):
        self.type = unit_type
        self.x = x
        self.y = y
        self.faction = faction
        self.selected = False
        
        # Stats based on type
        stats = {
            UnitType.WORKER: {
                'hp': 50, 'attack': 5, 'defense': 0,
                'speed': 1.5, 'range': 1, 'sight': 5
            },
            UnitType.SOLDIER: {
                'hp': 100, 'attack': 15, 'defense': 5,
                'speed': 1, 'range': 1, 'sight': 6
            },
            UnitType.ARCHER: {
                'hp': 60, 'attack': 12, 'defense': 2,
                'speed': 1.2, 'range': 5, 'sight': 8
            },
            UnitType.CAVALRY: {
                'hp': 120, 'attack': 20, 'defense': 3,
                'speed': 2, 'range': 1, 'sight': 7
            },
            UnitType.SIEGE: {
                'hp': 200, 'attack': 50, 'defense': 0,
                'speed': 0.5, 'range': 6, 'sight': 5
            }
        }
        
        unit_stats = stats[unit_type]
        self.max_hp = unit_stats['hp']
        self.hp = self.max_hp
        self.attack = unit_stats['attack']
        self.defense = unit_stats['defense']
        self.speed = unit_stats['speed']
        self.range = unit_stats['range']
        self.sight = unit_stats['sight']
        
        # Movement
        self.target_x = x
        self.target_y = y
        self.path = []
        self.moving = False
        
        # Combat
        self.target = None
        self.cooldown = 0
        
        # Worker specific
        self.carrying = None
        self.gathering_from = None

class Building:
    """Strategy game building"""
    
    def __init__(self, building_type: BuildingType, x: int, y: int, faction: str):
        self.type = building_type
        self.x = x
        self.y = y
        self.faction = faction
        self.selected = False
        
        # Stats based on type
        stats = {
            BuildingType.BASE: {
                'hp': 1000, 'size': 3, 'sight': 10,
                'produces': [UnitType.WORKER]
            },
            BuildingType.BARRACKS: {
                'hp': 600, 'size': 2, 'sight': 5,
                'produces': [UnitType.SOLDIER, UnitType.ARCHER, UnitType.CAVALRY]
            },
            BuildingType.MINE: {
                'hp': 500, 'size': 2, 'sight': 3,
                'generates': {'gold': 10}
            },
            BuildingType.FARM: {
                'hp': 400, 'size': 2, 'sight': 3,
                'generates': {'food': 8}
            },
            BuildingType.TOWER: {
                'hp': 800, 'size': 1, 'sight': 8,
                'attack': 30, 'range': 8
            },
            BuildingType.WALL: {
                'hp': 300, 'size': 1, 'sight': 1
            }
        }
        
        building_stats = stats[building_type]
        self.max_hp = building_stats['hp']
        self.hp = self.max_hp
        self.size = building_stats['size']
        self.sight = building_stats['sight']
        
        self.produces = building_stats.get('produces', [])
        self.generates = building_stats.get('generates', {})
        self.attack = building_stats.get('attack', 0)
        self.range = building_stats.get('range', 0)
        
        # Production
        self.production_queue = []
        self.production_progress = 0
        
        # Combat
        self.target = None
        self.cooldown = 0
        
        # Construction
        self.construction_progress = 100  # Starts complete
        self.is_complete = True

class Map:
    """Game map with terrain and fog of war"""
    
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height
        self.tiles = []
        self.fog = []
        
        self.generate()
    
    def generate(self):
        """Generate random terrain"""
        for y in range(self.height):
            row = []
            fog_row = []
            for x in range(self.width):
                terrain = 'grass'
                
                # Add terrain variety
                if random.random() < 0.15:
                    terrain = 'forest'
                elif random.random() < 0.05:
                    terrain = 'mountain'
                elif random.random() < 0.03:
                    terrain = 'water'
                
                # Resources
                resource = None
                if terrain == 'grass' and random.random() < 0.05:
                    resource = random.choice(['gold', 'food', 'wood'])
                
                row.append({
                    'terrain': terrain,
                    'resource': resource,
                    'occupied': False
                })
                
                fog_row.append(False)  # Not explored
            
            self.tiles.append(row)
            self.fog.append(fog_row)
    
    def is_passable(self, x: int, y: int) -> bool:
        """Check if tile is passable"""
        if not self.in_bounds(x, y):
            return False
        
        tile = self.tiles[y][x]
        return (tile['terrain'] != 'water' and 
                tile['terrain'] != 'mountain' and
                not tile['occupied'])
    
    def in_bounds(self, x: int, y: int) -> bool:
        """Check if coordinates are in map bounds"""
        return 0 <= x < self.width and 0 <= y < self.height
    
    def reveal(self, x: int, y: int, radius: int):
        """Reveal fog of war around position"""
        for dy in range(-radius, radius + 1):
            for dx in range(-radius, radius + 1):
                nx, ny = x + dx, y + dy
                if self.in_bounds(nx, ny):
                    dist = math.sqrt(dx*dx + dy*dy)
                    if dist <= radius:
                        self.fog[ny][nx] = True

class Pathfinding:
    """A* pathfinding for units"""
    
    @staticmethod
    def find_path(start: Tuple[int, int], goal: Tuple[int, int], 
                  game_map: Map) -> List[Tuple[int, int]]:
        """Find path using A* algorithm"""
        
        def heuristic(a, b):
            return abs(a[0] - b[0]) + abs(a[1] - b[1])
        
        open_set = [(0, start)]
        came_from = {}
        g_score = {start: 0}
        f_score = {start: heuristic(start, goal)}
        
        while open_set:
            current = heapq.heappop(open_set)[1]
            
            if current == goal:
                # Reconstruct path
                path = []
                while current in came_from:
                    path.append(current)
                    current = came_from[current]
                return list(reversed(path))
            
            for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0),
                          (1, 1), (-1, 1), (1, -1), (-1, -1)]:
                neighbor = (current[0] + dx, current[1] + dy)
                
                if not game_map.is_passable(neighbor[0], neighbor[1]):
                    continue
                
                tentative_g = g_score[current] + 1
                
                if neighbor not in g_score or tentative_g < g_score[neighbor]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g
                    f_score[neighbor] = tentative_g + heuristic(neighbor, goal)
                    heapq.heappush(open_set, (f_score[neighbor], neighbor))
        
        return []  # No path found

class AIController:
    """AI opponent controller"""
    
    def __init__(self, faction: str, difficulty: str = 'normal'):
        self.faction = faction
        self.difficulty = difficulty
        self.action_timer = 0
        self.strategy = 'balanced'  # balanced, aggressive, defensive, economic
        self.build_order = ['worker', 'barracks', 'soldier', 'mine', 'worker', 'farm']
        self.current_build_index = 0
    
    def update(self, game_state: 'StrategyGame', dt: float):
        """Update AI decisions"""
        self.action_timer += dt
        
        # Make decisions at different rates based on difficulty
        decision_rate = {'easy': 5, 'normal': 3, 'hard': 2}[self.difficulty]
        
        if self.action_timer > decision_rate:
            self.action_timer = 0
            self.make_strategic_decision(game_state)
    
    def make_strategic_decision(self, game_state: 'StrategyGame'):
        """Make high-level strategic decisions"""
        my_units = [u for u in game_state.units if u.faction == self.faction]
        my_buildings = [b for b in game_state.buildings if b.faction == self.faction]
        enemy_units = [u for u in game_state.units if u.faction != self.faction]
        enemy_buildings = [b for b in game_state.buildings if b.faction != self.faction]
        
        # Assess situation
        military_strength = sum(1 for u in my_units if u.type != UnitType.WORKER)
        worker_count = sum(1 for u in my_units if u.type == UnitType.WORKER)
        
        # Decide strategy
        if military_strength < 3:
            self.strategy = 'defensive'
        elif worker_count < 5:
            self.strategy = 'economic'
        elif military_strength > 10:
            self.strategy = 'aggressive'
        else:
            self.strategy = 'balanced'
        
        # Execute strategy
        if self.strategy == 'aggressive':
            self.execute_attack(my_units, enemy_buildings + enemy_units)
        elif self.strategy == 'defensive':
            self.build_defenses(game_state, my_buildings)
        elif self.strategy == 'economic':
            self.expand_economy(game_state, my_buildings, my_units)
        else:
            self.balanced_approach(game_state, my_units, my_buildings)
    
    def execute_attack(self, my_units: List[Unit], targets: List):
        """Command units to attack"""
        if not targets:
            return
        
        # Find closest target
        for unit in my_units:
            if unit.type != UnitType.WORKER and not unit.target:
                closest_target = min(targets, 
                                   key=lambda t: math.sqrt((t.x - unit.x)**2 + (t.y - unit.y)**2))
                unit.target = closest_target
    
    def build_defenses(self, game_state: 'StrategyGame', my_buildings: List[Building]):
        """Build defensive structures"""
        for building in my_buildings:
            if building.type == BuildingType.BASE:
                # Build towers around base
                positions = [(building.x - 2, building.y), 
                           (building.x + building.size + 1, building.y),
                           (building.x, building.y - 2),
                           (building.x, building.y + building.size + 1)]
                
                for pos in positions:
                    if game_state.build_structure(BuildingType.TOWER, pos[0], pos[1]):
                        break
    
    def expand_economy(self, game_state: 'StrategyGame', my_buildings: List[Building], 
                       my_units: List[Unit]):
        """Focus on economic growth"""
        # Train more workers
        for building in my_buildings:
            if building.type == BuildingType.BASE and not building.production_queue:
                game_state.train_unit(UnitType.WORKER, building)
        
        # Build resource buildings
        if len([b for b in my_buildings if b.type == BuildingType.MINE]) < 2:
            # Find good location for mine
            for building in my_buildings:
                if building.type == BuildingType.BASE:
                    x, y = building.x + building.size + 2, building.y
                    game_state.build_structure(BuildingType.MINE, x, y)
                    break
    
    def balanced_approach(self, game_state: 'StrategyGame', my_units: List[Unit], 
                         my_buildings: List[Building]):
        """Balanced strategy"""
        # Train units
        for building in my_buildings:
            if building.type == BuildingType.BARRACKS and not building.production_queue:
                unit_types = [UnitType.SOLDIER, UnitType.ARCHER, UnitType.CAVALRY]
                game_state.train_unit(random.choice(unit_types), building)
        
        # Build structures
        if len(my_buildings) < 5:
            building_types = [BuildingType.BARRACKS, BuildingType.MINE, BuildingType.FARM]
            for building in my_buildings:
                if building.type == BuildingType.BASE:
                    x = building.x + random.randint(-5, 5)
                    y = building.y + random.randint(-5, 5)
                    game_state.build_structure(random.choice(building_types), x, y)
                    break

class StrategyGame:
    """Main strategy game class"""
    
    def __init__(self, screen_width: int = 800, screen_height: int = 600):
        pygame.init()
        self.screen = pygame.display.set_mode((screen_width, screen_height))
        pygame.display.set_caption("Strategy Game")
        self.clock = pygame.time.Clock()
        
        # Game state
        self.resources = Resources(gold=500, food=200, wood=100)
        self.map = Map(50, 40)
        self.camera_x = 0
        self.camera_y = 0
        
        # Entities
        self.units = []
        self.buildings = []
        self.selected = []
        
        # AI
        self.ai_controller = AIController('enemy', 'normal')
        
        # Costs
        self.unit_costs = {
            UnitType.WORKER: Resources(gold=50),
            UnitType.SOLDIER: Resources(gold=100, food=20),
            UnitType.ARCHER: Resources(gold=120, food=20),
            UnitType.CAVALRY: Resources(gold=200, food=50),
            UnitType.SIEGE: Resources(gold=300, wood=100)
        }
        
        self.building_costs = {
            BuildingType.BASE: Resources(gold=500, wood=200),
            BuildingType.BARRACKS: Resources(gold=300, wood=100),
            BuildingType.MINE: Resources(gold=200),
            BuildingType.FARM: Resources(gold=150, wood=50),
            BuildingType.TOWER: Resources(gold=250, wood=50),
            BuildingType.WALL: Resources(gold=50, wood=20)
        }
        
        # Tech tree
        self.researched_techs = set()
        self.tech_tree = {
            'improved_mining': {'cost': 200, 'effect': 'gold_bonus'},
            'advanced_farming': {'cost': 200, 'effect': 'food_bonus'},
            'military_training': {'cost': 300, 'effect': 'unit_attack'},
            'fortification': {'cost': 300, 'effect': 'building_hp'}
        }
        
        # Initialize game
        self.initialize_game()
    
    def initialize_game(self):
        """Set up initial game state"""
        # Player starting position
        start_x, start_y = 5, 5
        
        # Create player base
        base = Building(BuildingType.BASE, start_x, start_y, 'player')
        self.buildings.append(base)
        self.map.reveal(start_x, start_y, base.sight)
        
        # Create player starting units
        for i in range(3):
            worker = Unit(UnitType.WORKER, start_x + 3 + i, start_y + 3, 'player')
            self.units.append(worker)
            self.map.reveal(worker.x, worker.y, worker.sight)
        
        # Create enemy base
        enemy_x, enemy_y = 40, 30
        enemy_base = Building(BuildingType.BASE, enemy_x, enemy_y, 'enemy')
        self.buildings.append(enemy_base)
        
        # Create enemy starting units
        for i in range(3):
            worker = Unit(UnitType.WORKER, enemy_x + 3 + i, enemy_y + 3, 'enemy')
            self.units.append(worker)
    
    def handle_click(self, x: int, y: int, button: int):
        """Handle mouse click"""
        # Convert screen to world coordinates
        world_x = (x + self.camera_x) // 20
        world_y = (y + self.camera_y) // 20
        
        if button == 1:  # Left click - selection
            self.select_at(world_x, world_y)
        elif button == 3:  # Right click - action
            self.issue_command(world_x, world_y)
    
    def select_at(self, x: int, y: int):
        """Select units/buildings at position"""
        # Clear previous selection
        for unit in self.units:
            unit.selected = False
        for building in self.buildings:
            building.selected = False
        self.selected = []
        
        # Select unit at position
        for unit in self.units:
            if unit.x == x and unit.y == y and unit.faction == 'player':
                unit.selected = True
                self.selected.append(unit)
                return
        
        # Select building at position
        for building in self.buildings:
            if (building.x <= x < building.x + building.size and
                building.y <= y < building.y + building.size and
                building.faction == 'player'):
                building.selected = True
                self.selected.append(building)
                return
    
    def issue_command(self, x: int, y: int):
        """Issue command to selected units"""
        for entity in self.selected:
            if isinstance(entity, Unit):
                # Check for enemy at target
                enemy = None
                for unit in self.units:
                    if unit.faction != entity.faction and unit.x == x and unit.y == y:
                        enemy = unit
                        break
                
                if not enemy:
                    for building in self.buildings:
                        if (building.faction != entity.faction and
                            building.x <= x < building.x + building.size and
                            building.y <= y < building.y + building.size):
                            enemy = building
                            break
                
                if enemy:
                    # Attack command
                    entity.target = enemy
                else:
                    # Move command
                    entity.target_x = x
                    entity.target_y = y
                    entity.path = Pathfinding.find_path(
                        (entity.x, entity.y),
                        (x, y),
                        self.map
                    )
                    entity.moving = bool(entity.path)
    
    def train_unit(self, unit_type: UnitType, building: Building):
        """Train a unit from building"""
        if unit_type not in building.produces:
            return False
        
        cost = self.unit_costs[unit_type]
        if not self.resources.can_afford(cost):
            return False
        
        self.resources.subtract(cost)
        building.production_queue.append({
            'type': unit_type,
            'time': 180  # 3 seconds at 60 FPS
        })
        
        return True
    
    def build_structure(self, building_type: BuildingType, x: int, y: int):
        """Build a structure"""
        cost = self.building_costs[building_type]
        if not self.resources.can_afford(cost):
            return False
        
        # Check if area is clear
        size = Building(building_type, 0, 0, 'player').size
        for dy in range(size):
            for dx in range(size):
                if not self.map.is_passable(x + dx, y + dy):
                    return False
        
        self.resources.subtract(cost)
        
        # Create building
        building = Building(building_type, x, y, 'player')
        building.construction_progress = 0
        building.is_complete = False
        self.buildings.append(building)
        
        # Mark tiles as occupied
        for dy in range(size):
            for dx in range(size):
                self.map.tiles[y + dy][x + dx]['occupied'] = True
        
        return True
    
    def research_tech(self, tech_name: str):
        """Research a technology"""
        if tech_name in self.researched_techs:
            return False
        
        tech = self.tech_tree.get(tech_name)
        if not tech:
            return False
        
        if self.resources.gold < tech['cost']:
            return False
        
        self.resources.gold -= tech['cost']
        self.researched_techs.add(tech_name)
        
        # Apply tech effects
        if tech['effect'] == 'gold_bonus':
            for building in self.buildings:
                if building.type == BuildingType.MINE:
                    building.generates['gold'] = int(building.generates['gold'] * 1.25)
        elif tech['effect'] == 'food_bonus':
            for building in self.buildings:
                if building.type == BuildingType.FARM:
                    building.generates['food'] = int(building.generates['food'] * 1.25)
        elif tech['effect'] == 'unit_attack':
            for unit in self.units:
                unit.attack = int(unit.attack * 1.2)
        elif tech['effect'] == 'building_hp':
            for building in self.buildings:
                building.max_hp = int(building.max_hp * 1.25)
                building.hp = building.max_hp
        
        return True
    
    def update(self, dt: float):
        """Update game state"""
        # Update units
        for unit in self.units[:]:
            # Movement
            if unit.path:
                next_pos = unit.path[0]
                unit.x, unit.y = next_pos
                unit.path.pop(0)
                
                if unit.faction == 'player':
                    self.map.reveal(unit.x, unit.y, unit.sight)
                
                if not unit.path:
                    unit.moving = False
            
            # Combat
            if unit.target:
                dx = abs(unit.x - unit.target.x)
                dy = abs(unit.y - unit.target.y)
                dist = max(dx, dy)
                
                if dist <= unit.range:
                    if unit.cooldown <= 0:
                        # Attack
                        damage = max(1, unit.attack - unit.target.defense)
                        unit.target.hp -= damage
                        unit.cooldown = 60  # 1 second
                        
                        if unit.target.hp <= 0:
                            unit.target = None
                else:
                    # Move toward target
                    unit.target_x = unit.target.x
                    unit.target_y = unit.target.y
                    unit.path = Pathfinding.find_path(
                        (unit.x, unit.y),
                        (unit.target_x, unit.target_y),
                        self.map
                    )
            
            # Cooldown
            if unit.cooldown > 0:
                unit.cooldown -= 1
            
            # Death
            if unit.hp <= 0:
                self.units.remove(unit)
        
        # Update buildings
        for building in self.buildings[:]:
            # Construction
            if not building.is_complete:
                building.construction_progress += 1
                if building.construction_progress >= 100:
                    building.is_complete = True
                    if building.faction == 'player':
                        self.map.reveal(building.x, building.y, building.sight)
            
            # Production
            if building.production_queue and building.is_complete:
                production = building.production_queue[0]
                building.production_progress += 1
                
                if building.production_progress >= production['time']:
                    # Spawn unit
                    unit = Unit(
                        production['type'],
                        building.x + building.size,
                        building.y + building.size,
                        building.faction
                    )
                    self.units.append(unit)
                    
                    building.production_queue.pop(0)
                    building.production_progress = 0
            
            # Resource generation
            if building.generates and building.is_complete:
                for resource, amount in building.generates.items():
                    if resource == 'gold':
                        self.resources.gold += amount / 60  # Per second
                    elif resource == 'food':
                        self.resources.food += amount / 60
                    elif resource == 'wood':
                        self.resources.wood += amount / 60
            
            # Tower attacks
            if building.type == BuildingType.TOWER and building.is_complete:
                if building.cooldown <= 0:
                    # Find nearest enemy
                    nearest_enemy = None
                    nearest_dist = float('inf')
                    
                    for unit in self.units:
                        if unit.faction != building.faction:
                            dist = max(abs(unit.x - building.x), 
                                     abs(unit.y - building.y))
                            if dist <= building.range and dist < nearest_dist:
                                nearest_enemy = unit
                                nearest_dist = dist
                    
                    if nearest_enemy:
                        damage = building.attack
                        nearest_enemy.hp -= damage
                        building.cooldown = 60
                
                if building.cooldown > 0:
                    building.cooldown -= 1
            
            # Death
            if building.hp <= 0:
                # Clear occupied tiles
                for dy in range(building.size):
                    for dx in range(building.size):
                        self.map.tiles[building.y + dy][building.x + dx]['occupied'] = False
                
                self.buildings.remove(building)
        
        # Update AI
        self.ai_controller.update(self, dt)
    
    def render(self):
        """Render game"""
        self.screen.fill((34, 139, 34))
        
        # Calculate visible tiles
        tile_size = 20
        start_x = self.camera_x // tile_size
        start_y = self.camera_y // tile_size
        end_x = min(self.map.width, start_x + self.screen.get_width() // tile_size + 2)
        end_y = min(self.map.height, start_y + self.screen.get_height() // tile_size + 2)
        
        # Draw terrain
        terrain_colors = {
            'grass': (34, 139, 34),
            'forest': (0, 100, 0),
            'mountain': (139, 137, 137),
            'water': (64, 164, 223)
        }
        
        for y in range(start_y, end_y):
            for x in range(start_x, end_x):
                if not self.map.fog[y][x]:
                    # Fog of war
                    color = (20, 20, 20)
                else:
                    tile = self.map.tiles[y][x]
                    color = terrain_colors[tile['terrain']]
                
                screen_x = x * tile_size - self.camera_x
                screen_y = y * tile_size - self.camera_y
                
                pygame.draw.rect(self.screen, color,
                               (screen_x, screen_y, tile_size, tile_size))
                
                # Draw resources
                if self.map.fog[y][x] and tile['resource']:
                    resource_colors = {
                        'gold': (255, 215, 0),
                        'food': (139, 69, 19),
                        'wood': (101, 67, 33)
                    }
                    pygame.draw.circle(self.screen,
                                     resource_colors[tile['resource']],
                                     (screen_x + tile_size//2, screen_y + tile_size//2),
                                     5)
        
        # Draw buildings
        for building in self.buildings:
            screen_x = building.x * tile_size - self.camera_x
            screen_y = building.y * tile_size - self.camera_y
            size = building.size * tile_size
            
            color = (100, 100, 200) if building.faction == 'player' else (200, 100, 100)
            
            if building.is_complete:
                pygame.draw.rect(self.screen, color,
                               (screen_x, screen_y, size, size))
            else:
                # Under construction
                pygame.draw.rect(self.screen, color,
                               (screen_x, screen_y, size, size), 2)
                # Progress bar
                progress_width = size * (building.construction_progress / 100)
                pygame.draw.rect(self.screen, (0, 255, 0),
                               (screen_x, screen_y - 5, progress_width, 3))
            
            # Selection indicator
            if building.selected:
                pygame.draw.rect(self.screen, (255, 255, 0),
                               (screen_x - 2, screen_y - 2, size + 4, size + 4), 2)
            
            # Health bar
            if building.hp < building.max_hp:
                health_width = size * (building.hp / building.max_hp)
                pygame.draw.rect(self.screen, (255, 0, 0),
                               (screen_x, screen_y - 10, size, 3))
                pygame.draw.rect(self.screen, (0, 255, 0),
                               (screen_x, screen_y - 10, health_width, 3))
        
        # Draw units
        for unit in self.units:
            screen_x = unit.x * tile_size - self.camera_x + tile_size//2
            screen_y = unit.y * tile_size - self.camera_y + tile_size//2
            
            color = (50, 50, 200) if unit.faction == 'player' else (200, 50, 50)
            
            pygame.draw.circle(self.screen, color, (screen_x, screen_y), 8)
            
            # Selection indicator
            if unit.selected:
                pygame.draw.circle(self.screen, (255, 255, 0),
                                 (screen_x, screen_y), 12, 2)
            
            # Health bar
            if unit.hp < unit.max_hp:
                bar_width = 20
                health_width = bar_width * (unit.hp / unit.max_hp)
                pygame.draw.rect(self.screen, (255, 0, 0),
                               (screen_x - bar_width//2, screen_y - 15, bar_width, 3))
                pygame.draw.rect(self.screen, (0, 255, 0),
                               (screen_x - bar_width//2, screen_y - 15, health_width, 3))
        
        # Draw UI
        self.render_ui()
        
        pygame.display.flip()
    
    def render_ui(self):
        """Render user interface"""
        # Resource bar
        ui_height = 40
        pygame.draw.rect(self.screen, (50, 50, 50),
                       (0, 0, self.screen.get_width(), ui_height))
        
        font = pygame.font.Font(None, 24)
        
        # Resources
        resources_text = f"Gold: {int(self.resources.gold)}  " \
                        f"Food: {int(self.resources.food)}  " \
                        f"Wood: {int(self.resources.wood)}"
        
        text_surface = font.render(resources_text, True, (255, 255, 255))
        self.screen.blit(text_surface, (10, 10))
        
        # Selection info
        if self.selected:
            entity = self.selected[0]
            if isinstance(entity, Unit):
                info = f"Unit: {entity.type.value}  HP: {entity.hp}/{entity.max_hp}"
            else:
                info = f"Building: {entity.type.value}  HP: {entity.hp}/{entity.max_hp}"
            
            info_surface = font.render(info, True, (255, 255, 255))
            self.screen.blit(info_surface, (10, self.screen.get_height() - 30))
    
    def run(self):
        """Main game loop"""
        running = True
        
        while running:
            dt = self.clock.tick(60) / 1000.0
            
            # Handle events
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    self.handle_click(event.pos[0], event.pos[1], event.button)
                
                elif event.type == pygame.KEYDOWN:
                    # Camera movement
                    speed = 10
                    if event.key == pygame.K_LEFT:
                        self.camera_x = max(0, self.camera_x - speed)
                    elif event.key == pygame.K_RIGHT:
                        self.camera_x = min(self.map.width * 20 - self.screen.get_width(),
                                          self.camera_x + speed)
                    elif event.key == pygame.K_UP:
                        self.camera_y = max(0, self.camera_y - speed)
                    elif event.key == pygame.K_DOWN:
                        self.camera_y = min(self.map.height * 20 - self.screen.get_height(),
                                          self.camera_y + speed)
            
            # Update
            self.update(dt)
            
            # Render
            self.render()
        
        pygame.quit()

if __name__ == "__main__":
    game = StrategyGame()
    game.run()

Key Features Implemented

✅ Complete Systems

Usage Instructions

🎮 How to Play

  1. Install Pygame: pip install pygame
  2. Run the game: python strategy_game.py
  3. Controls:
    • Left-click to select units/buildings
    • Right-click to move units or attack
    • Arrow keys to move camera
    • Build structures by selecting a builder unit
    • Train units from barracks

Customization Options

🔧 Easy Modifications

🏋️‍♂️ Practice Exercise

🏋️‍♂️ Exercise 1: Three Pillars of Python RTS Architecture in One Pygame Window

Objective: Build a runnable pygame program (~85 lines) that distills the lesson's UnitType/BuildingType‐Enum + per‐type stats dict + Building.is_complete gate + research_tech retroactive‐mutation architecture into one cohesive pygame demo where three orthogonal Python‐RTS‐architecture disciplines are visible per frame on a 1088×540 window split into three vertical panels (LEFT = Building‐lifecycle gate at is_complete + global income tally / MIDDLE = Enum‐keyed UNIT_STATS dict + spawn cohort + runtime mutation / RIGHT = retroactive research_tech walk over extant soldier cohort). (a) Building‐lifecycle gate at is_complete boolean checked at every consumer site at BUILDING‐LIFECYCLE scope: each Building ramps progress per frame until >= 100 then flips is_complete = True; downstream consumers ALL guard via if b.is_complete: checks before adding income / running production / firing tower attacks — so during construction (progress < 100) the building exists on the map but contributes ZERO income to the global tally. Python‐idiomatic delta from chat‐81's onComplete() method: chat‐81 (JS) centralized the side‐effects into one onComplete() method called exactly once at the moment progress crossed 100; chat‐82 (Python) distributes the side‐effect‐gating across N consumer‐site checks. Both are valid; the JS‐vs‐Python implementations differ at the call‐site‐distribution‐vs‐callback‐centralization level even though the architectural intent is identical (gate side effects on lifecycle completion). The two‐layer separation (data: income_per_sec in the building record; gate: is_complete flag check) is the same default‐to‐deny‐at‐the‐boundary shape as platformer_tilemap's is_solid‐True‐OOB invariant + architecture_state_machines' on_exit→reassign→on_enter strict ordering + architecture_event_systems' queued‐emit‐vs‐immediate‐emit timing modes + chat‐81 genres_strategy's onComplete‐as‐gate, applied here at BUILDING‐LIFECYCLE scope with the consumer‐check Python‐idiomatic delta. (b) Enum‐keyed stats dict at UNIT‐TYPE‐DEFINITION scope: a module‐level UNIT_STATS: dict[UnitType, dict] holds per‐type stat records keyed by UnitType Enum MEMBERS (not string literals); Unit.__init__ reads UNIT_STATS[t]['hp'] and UNIT_STATS[t]['attack'] exactly once per spawn. Python‐idiomatic delta from chat‐81's JS object‐literal UNIT_STATS = { worker: {...} }: chat‐82 uses Enum members as keys, which means a typo at the spawn site (Unit(UnitType.WROKER)) raises AttributeError at IMPORT time before the constructor even runs; the equivalent JS typo (new Unit('wroker')) silently produces a unit with all‐undefined stats that crashes much later or runs as a glitched zero‐stat unit. Adding a new unit type is one new UnitType enum member + one new UNIT_STATS entry rather than scattered if/elif edits across __init__ / update() / render(). SEVENTH design‐time‐validation‐vs‐runtime‐discovery lesson reinforcing the pattern across Phase 8, FIRST applied at STATS‐DICT‐KEY‐IDENTITY scope where Enum‐member‐as‐key replaces string‐as‐key. The H key mutates UNIT_STATS[UnitType.SOLDIER]['hp'] += 20 at runtime so subsequent spawned soldiers show the new HP while extant soldiers retain their old HP — a property of constructor‐snapshot semantics and module‐level dict identity rather than a property of the Enum‐key shape itself. (c) Retroactive research bonus applied to all extant units once at research moment, NOT snapshotted at creation time at RESEARCH‐EFFECT‐TIMING scope: pressing R triggers for u in soldiers: u.attack = int(u.attack * 1.2) mutating every extant soldier's attack stat IN PLACE at the moment research completes, with NO corresponding hook in Unit.__init__ to consult a researched‐techs set for future spawns. The HUD shows Pre‐R: [15, 15, 15] static and Post‐R: [18, 18, 18] live so the retroactive walk is visible as a single‐frame transition across the existing cohort. OPPOSITE design choice from chat‐81's snapshot‐at‐creation: chat‐81 (JS) had this.attack *= 1.25 inside Unit's constructor guarded by gameState.researched.has('military') so future spawns get the bonus and extant units retain baseline; chat‐82 (Python) does live‐retroactive at the research call site so extant units get the bonus and future spawns get baseline. Both are legitimate design choices producing different gameplay rhythms — snapshot encourages 'tech‐up THEN rebuild' army‐replacement pacing (AoE2‐style late‐game cycle); retroactive encourages 'tech‐up empowers your existing army immediately' instant‐effect pacing (Civ‐style instant‐tech). The chat‐82 lesson picking the OPPOSITE side from chat‐81 is a deliberate pedagogical contrast that lets the JS‐then‐pygame paired companion lessons demonstrate the SAME architectural axis (RESEARCH‐EFFECT‐TIMING) from BOTH sides of the design‐spectrum, making the context‐determines‐correct‐choice rhetoric visible as the pair. SECOND lesson at RESEARCH‐EFFECT‐TIMING scope (after chat‐81) and FIRST to demonstrate the retroactive side at this scope. HUD shows time, gold income tally, current UNIT_STATS[UnitType.SOLDIER]['hp'] value, research‐on/off flag, and the legend — three orthogonal Python‐RTS‐architecture disciplines visible per frame as concrete numbers and panel deltas. ADVANCES the genres module 6/9 → 7/9 partial at chat‐82 M1; module‐completeness stays 12/13 since genres doesn't close in one chat (2 genres lessons remain after chat‐82: tower_defense / tower_defense_python).

Instructions:

  1. Define class UnitType(Enum) with members WORKER, SOLDIER, ARCHER mirroring the lesson's pattern (string values are fine but the Enum members themselves are what matter for keying).
  2. Define a module‐level UNIT_STATS dict whose keys are the UnitType Enum members and whose values are {'hp': int, 'attack': int} records.
  3. Define class Unit whose __init__(self, t) reads UNIT_STATS[t] once and assigns self.hp and self.attack from the record.
  4. Define a @dataclass Building with fields name: str, rate: float, income_per_sec: float, progress: float = 0.0, is_complete: bool = False; create three instances (Mine / Farm / Barracks) at staggered rates so they finish at visibly different times.
  5. In the per‐frame loop, ramp each building's progress += rate * dt and flip is_complete = True at progress >= 100; gate the income tally with if b.is_complete: gold += b.income_per_sec * dt so only complete buildings contribute to gold.
  6. Bind keys 1/2/3 to spawn worker/soldier/archer via Unit(UnitType.WORKER) / etc; bind H to UNIT_STATS[UnitType.SOLDIER]['hp'] += 20 at runtime so the next soldier spawned shows the new HP while extant soldiers retain old HP.
  7. Pre‐spawn 3 soldiers and snapshot their attack values as pre_attack_snapshot; bind R to walk the cohort with for u in soldiers: u.attack = int(u.attack * 1.2) exactly once (gate on a research_done flag so subsequent R presses do nothing) so the HUD shows the visible single‐frame transition across the existing cohort.
  8. Render three vertical panels with thin separator lines at x=362 and x=724; per‐panel HUD lines + per‐building progress bars + per‐spawn unit list + per‐soldier circles colored green after research; verify the demo runs at 60 FPS with all three axes responding correctly to keyboard input before submitting.
💡 Hint

The is_complete consumer‐check pattern means EVERY consumer of the building (income, production, tower attacks in the full lesson code) does if b.is_complete: at its own call site — do NOT try to centralize the side‐effects into a single on_complete() method (that would be the chat‐81 JS pattern; the Python‐idiomatic shape is distributed checks). The UNIT_STATS dict MUST be module‐level (not local to Unit.__init__) for the H‐key mutation to propagate to subsequent spawns; the lesson's actual code keeps stats local to the constructor which means runtime mutation isn't visible — hoist it to module level for the demo. The retroactive research walk is a SINGLE pass over the extant soldiers list at the moment R is pressed; do NOT add any check inside Unit.__init__ for a researched‐techs flag (that would be chat‐81's snapshot pattern; the Python‐idiomatic shape here is the OPPOSITE choice). Use int(u.attack * 1.2) not u.attack * 1.2 to match the lesson's integer‐floored arithmetic so the HUD shows clean integer values.

✅ Example Solution
import pygame
from enum import Enum
from dataclasses import dataclass

pygame.init()
W, H = 1088, 540
screen = pygame.display.set_mode((W, H))
font = pygame.font.SysFont(None, 18)
clock = pygame.time.Clock()

class UnitType(Enum):
    WORKER = 'worker'
    SOLDIER = 'soldier'
    ARCHER = 'archer'

# Axis B: module-level Enum-keyed stats dict
UNIT_STATS = {
    UnitType.WORKER:  {'hp': 50,  'attack': 5},
    UnitType.SOLDIER: {'hp': 100, 'attack': 15},
    UnitType.ARCHER:  {'hp': 60,  'attack': 12},
}

class Unit:
    def __init__(self, t):
        self.type = t
        self.hp = UNIT_STATS[t]['hp']      # KeyError if t isn't a UnitType member
        self.attack = UNIT_STATS[t]['attack']

@dataclass
class Building:
    name: str
    rate: float            # progress per second
    income_per_sec: float
    progress: float = 0.0
    is_complete: bool = False

# Axis A: 3 buildings under construction, gate via is_complete
buildings = [
    Building('Mine',     rate=33.0, income_per_sec=2.0),  # ~3s to complete
    Building('Farm',     rate=20.0, income_per_sec=1.5),  # ~5s to complete
    Building('Barracks', rate=14.0, income_per_sec=0.0),  # ~7s, produces units only
]
gold = 0.0

# Axis C: 3 pre-spawned soldiers, retroactive research bonus
soldiers = [Unit(UnitType.SOLDIER) for _ in range(3)]
research_done = False
pre_attack_snapshot = [u.attack for u in soldiers]   # [15, 15, 15]

spawned = []   # middle-panel spawn list
WHITE=(240,240,240); GREY=(90,90,100); GREEN=(80,180,90)
BLUE=(80,130,220); YELLOW=(220,200,80)

running = True
while running:
    dt = clock.tick(60) / 1000.0
    for e in pygame.event.get():
        if e.type == pygame.QUIT: running = False
        elif e.type == pygame.KEYDOWN:
            if e.key == pygame.K_1: spawned.append(Unit(UnitType.WORKER))
            elif e.key == pygame.K_2: spawned.append(Unit(UnitType.SOLDIER))
            elif e.key == pygame.K_3: spawned.append(Unit(UnitType.ARCHER))
            elif e.key == pygame.K_h:
                UNIT_STATS[UnitType.SOLDIER]['hp'] += 20  # propagates only to subsequent spawns
            elif e.key == pygame.K_r and not research_done:
                for u in soldiers: u.attack = int(u.attack * 1.2)  # retroactive walk
                research_done = True

    # Axis A: building update + income consumer-check
    for b in buildings:
        if not b.is_complete:
            b.progress += b.rate * dt
            if b.progress >= 100:
                b.progress = 100; b.is_complete = True
        if b.is_complete:                  # consumer-check pattern
            gold += b.income_per_sec * dt

    # Render
    screen.fill((22, 26, 32))
    pygame.draw.line(screen, GREY, (362, 0), (362, H))
    pygame.draw.line(screen, GREY, (724, 0), (724, H))

    # LEFT: Axis A
    screen.blit(font.render('A: is_complete gate', True, YELLOW), (12, 8))
    screen.blit(font.render(f'gold = {gold:.1f}', True, WHITE), (12, 28))
    for i, b in enumerate(buildings):
        y = 70 + i * 90
        screen.blit(font.render(f'{b.name}  income/s={b.income_per_sec}', True, WHITE), (12, y))
        bar_w = int(330 * (b.progress / 100))
        col = GREEN if b.is_complete else BLUE
        pygame.draw.rect(screen, GREY, (12, y + 22, 330, 18), 1)
        pygame.draw.rect(screen, col, (12, y + 22, bar_w, 18))
        screen.blit(font.render(f'progress={b.progress:.0f}/100  is_complete={b.is_complete}', True, WHITE), (12, y + 46))

    # MIDDLE: Axis B
    screen.blit(font.render('B: Enum-keyed UNIT_STATS', True, YELLOW), (374, 8))
    cur_hp = UNIT_STATS[UnitType.SOLDIER]['hp']
    screen.blit(font.render(f"UNIT_STATS[UnitType.SOLDIER]['hp'] = {cur_hp}", True, WHITE), (374, 28))
    screen.blit(font.render('keys: 1=W  2=S  3=A    H=hp+=20', True, WHITE), (374, 50))
    for i, u in enumerate(spawned[-12:]):
        y = 90 + i * 28
        screen.blit(font.render(f'{u.type.name:8s}  hp={u.hp}  atk={u.attack}', True, WHITE), (374, y))

    # RIGHT: Axis C
    screen.blit(font.render('C: Retroactive research', True, YELLOW), (734, 8))
    screen.blit(font.render('R = research_tech (extant only)', True, WHITE), (734, 28))
    screen.blit(font.render(f'Pre-R:  {pre_attack_snapshot}', True, WHITE), (734, 60))
    cur_attack = [u.attack for u in soldiers]
    label = 'Post-R' if research_done else 'Now   '
    screen.blit(font.render(f'{label}: {cur_attack}', True, GREEN if research_done else WHITE), (734, 84))
    for i, u in enumerate(soldiers):
        col = GREEN if research_done else BLUE
        pygame.draw.circle(screen, col, (754 + i * 70, 150), 14)
        screen.blit(font.render(f'atk={u.attack}', True, WHITE), (734 + i * 70, 175))

    pygame.display.flip()

pygame.quit()

🎯 Quick Quiz

Question 1: Why does the demo gate the global income tally with if b.is_complete: gold += b.income_per_sec * dt rather than letting every building contribute income from the moment it's placed on the map?

Question 2: The lesson uses UNIT_STATS = {UnitType.WORKER: {...}, UnitType.SOLDIER: {...}, ...} with UnitType Enum members as dict keys, then reads UNIT_STATS[unit_type] in the Unit constructor. What architectural property does the Enum-member-as-key shape provide that string-as-key (UNIT_STATS = {"worker": {...}, ...}) does not?

Question 3: chat-81's genres_strategy.html (JS canvas) implements research bonuses as a SNAPSHOT taken in Unit's constructor (future spawns get the bonus, extant units retain baseline). chat-82's genres_strategy_python.html (pygame) implements research bonuses as a RETROACTIVE walk over the extant cohort at the moment research_tech() is called (extant units get the bonus, future spawns retain baseline). Which framing best captures the relationship between the two design choices?