Python可以做哪些小游戏——基于Python 3.13最新特性的游戏开发全指南(15万字超长文章,强烈建议收藏阅读)

目录

[第1章 Python游戏开发生态概述](#第1章 Python游戏开发生态概述)

[1.1 Python在游戏开发中的定位](#1.1 Python在游戏开发中的定位)

[1.2 Python游戏开发技术栈分析](#1.2 Python游戏开发技术栈分析)

[第2章 Python 3.13新特性对游戏开发的影响](#第2章 Python 3.13新特性对游戏开发的影响)

[2.1 Python 3.13性能优化对游戏帧率的提升](#2.1 Python 3.13性能优化对游戏帧率的提升)

[2.2 类型系统增强对游戏代码质量的提升](#2.2 类型系统增强对游戏代码质量的提升)

[2.3 异步编程特性在游戏网络功能中的应用](#2.3 异步编程特性在游戏网络功能中的应用)

[第3章 基础类小游戏的实现方案](#第3章 基础类小游戏的实现方案)

[3.1 文字类游戏的开发模式](#3.1 文字类游戏的开发模式)

[3.2 益智类游戏的算法实现](#3.2 益智类游戏的算法实现)

[3.3 休闲类游戏的交互设计](#3.3 休闲类游戏的交互设计)

[第4章 动作类游戏开发技术](#第4章 动作类游戏开发技术)

[4.1 实时碰撞检测算法](#4.1 实时碰撞检测算法)

[4.2 粒子系统与特效实现](#4.2 粒子系统与特效实现)

[4.3 AI行为树与敌人设计](#4.3 AI行为树与敌人设计)

[第5章 角色扮演与冒险游戏](#第5章 角色扮演与冒险游戏)

[5.1 RPG游戏核心系统设计](#5.1 RPG游戏核心系统设计)

[5.2 剧情系统与对话管理](#5.2 剧情系统与对话管理)

[第6章 策略与模拟游戏](#第6章 策略与模拟游戏)

[6.1 回合制策略游戏引擎](#6.1 回合制策略游戏引擎)

[第7章 网络游戏开发技术](#第7章 网络游戏开发技术)

[7.1 基于Socket的网络通信](#7.1 基于Socket的网络通信)

[第8章 Python游戏开发工具链](#第8章 Python游戏开发工具链)

[8.1 开发环境配置与优化](#8.1 开发环境配置与优化)

[8.2 调试与测试工具](#8.2 调试与测试工具)

[第9章 总结与展望](#第9章 总结与展望)

[9.1 Python游戏开发的优势与局限](#9.1 Python游戏开发的优势与局限)

[9.2 未来发展趋势](#9.2 未来发展趋势)

[9.3 最佳实践建议](#9.3 最佳实践建议)


第1章 Python游戏开发生态概述

1.1 Python在游戏开发中的定位

Python作为一种高级编程语言,在游戏开发领域占据着独特的地位。与C++、C#等传统游戏开发语言相比,Python具有开发效率高、学习曲线平缓、生态丰富的特点。特别是在独立游戏开发、教育游戏开发和快速原型制作方面,Python展现出了强大的竞争力。

Python游戏开发的优势主要体现在以下几个方面:首先是开发效率,Python简洁的语法使得开发者能够快速实现游戏逻辑;其次是丰富的第三方库支持,从简单的2D游戏到复杂的3D游戏都有相应的解决方案;最后是跨平台特性,Python代码可以在Windows、Linux、macOS等多个平台上运行。

然而,Python游戏开发也面临着一些挑战。性能问题是主要瓶颈,特别是在处理大量游戏对象和复杂物理计算时。不过,随着Python解释器的不断优化以及PyPy等高性能实现的出现,这一差距正在逐渐缩小。

1.2 Python游戏开发技术栈分析

Python游戏开发的技术栈可以分为几个层次:底层是Python解释器本身,中间层是各种游戏引擎和框架,上层是具体的游戏应用。在不同层次上,开发者有多种选择。

游戏引擎和框架对比

框架名称 类型 适用场景 学习难度 性能表现 社区活跃度
Pygame 2D游戏引擎 中小型2D游戏、教育项目 中等 中等 很高
Arcade 现代2D游戏引擎 教育游戏、轻量级商业项目 较低 较好
Panda3D 3D游戏引擎 3D游戏、VR应用 较高 很好 中等
Godot-Python 游戏引擎绑定 跨平台游戏开发 中等 很好 增长中
Ursina 3D游戏框架 快速3D原型开发 较低 中等 中等

在2D游戏开发领域,Pygame仍然是使用最广泛的框架。它提供了完整的游戏开发功能集,包括图形渲染、音频处理、输入管理等。Arcade作为较新的框架,采用了更现代的设计理念,对Python开发者更加友好,特别适合教学和快速开发。

对于3D游戏开发,Panda3D是较为成熟的选择,它最初由迪士尼开发,后来开源。Ursina则是一个相对较新的3D框架,建立在Panda3D之上,提供了更加简洁的API。

第2章 Python 3.13新特性对游戏开发的影响

2.1 Python 3.13性能优化对游戏帧率的提升

Python 3.13在性能方面带来了显著的改进,这对游戏开发具有重要意义。新的解释器采用了更高效的字节码执行机制,特别是在循环和函数调用方面有明显的性能提升。

性能对比测试

复制代码
import timeit
import random

def game_loop_simulation():
    """模拟游戏主循环的性能测试"""
    player_pos = [0, 0]
    enemies = [[random.randint(0, 100), random.randint(0, 100)] for _ in range(100)]
    
    for frame in range(1000):
        # 玩家移动
        player_pos[0] += random.choice([-1, 0, 1])
        player_pos[1] += random.choice([-1, 0, 1])
        
        # 敌人AI更新
        for enemy in enemies:
            dx = player_pos[0] - enemy[0]
            dy = player_pos[1] - enemy[1]
            if abs(dx) > abs(dy):
                enemy[0] += 1 if dx > 0 else -1
            else:
                enemy[1] += 1 if dy > 0 else -1

# 性能测试
time_taken = timeit.timeit(game_loop_simulation, number=100)
print(f"Python 3.13执行时间: {time_taken:.2f}秒")

在Python 3.13中,这类游戏循环的执行速度比Python 3.11提升了约15-20%。对于实时游戏来说,这意味着更高的帧率和更流畅的体验。

2.2 类型系统增强对游戏代码质量的提升

Python 3.13进一步增强了类型系统,这对于大型游戏项目的代码维护特别重要。新的类型特性使得开发者能够更精确地描述游戏对象之间的关系。

游戏对象类型定义示例

复制代码
from typing import Protocol, runtime_checkable, TypeVar, Generic, Any
from dataclasses import dataclass
from enum import Enum, auto


class GameObjectType(Enum):
    """游戏对象类型枚举"""
    PLAYER = auto()
    ENEMY = auto()
    PROJECTILE = auto()
    PICKUP = auto()


@runtime_checkable
class Renderable(Protocol):
    """可渲染对象协议"""

    def render(self, surface: Any) -> None: ...

    def get_position(self) -> tuple[int, int]: ...


@dataclass
class GameObject:
    """通用游戏对象类"""
    obj_type: GameObjectType
    position: tuple[int, int]
    velocity: tuple[float, float]
    health: int = 100

    def update(self, delta_time: float) -> None:
        """更新游戏对象状态"""
        x, y = self.position
        vx, vy = self.velocity
        self.position = (int(x + vx * delta_time), int(y + vy * delta_time))

这种增强的类型系统使得代码编辑器能够提供更好的智能提示,减少了运行时错误,提高了开发效率。

2.3 异步编程特性在游戏网络功能中的应用

Python 3.13进一步完善了异步编程特性,这对于开发多人在线游戏特别重要。新的异步I/O机制使得网络通信更加高效。

复制代码
import asyncio
from dataclasses import dataclass
from typing import Dict, Set

@dataclass
class PlayerState:
    """玩家状态"""
    player_id: str
    position: tuple[int, int]
    score: int = 0

class GameServer:
    """异步游戏服务器"""
    
    def __init__(self):
        self.players: Dict[str, PlayerState] = {}
        self.connected_clients: Set[asyncio.StreamWriter] = set()
    
    async def handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
        """处理客户端连接"""
        self.connected_clients.add(writer)
        player_id = f"player_{len(self.players)}"
        self.players[player_id] = PlayerState(player_id, (0, 0))
        
        try:
            while True:
                data = await reader.read(1024)
                if not data:
                    break
                
                # 处理游戏数据并广播
                await self.broadcast_game_state()
                
        finally:
            self.connected_clients.remove(writer)
            del self.players[player_id]
    
    async def broadcast_game_state(self):
        """广播游戏状态给所有客户端"""
        state_data = str(self.players).encode()
        for client in self.connected_clients:
            try:
                client.write(state_data)
                await client.drain()
            except:
                pass

这种异步架构能够高效处理大量并发连接,适合实时多人游戏。

第3章 基础类小游戏的实现方案

3.1 文字类游戏的开发模式

文字类游戏是Python游戏开发的入门选择,它们不依赖图形界面,专注于游戏逻辑和玩家交互。这类游戏包括文字冒险、猜数字游戏、文字RPG等。

文字冒险游戏引擎实现

复制代码
import pygame
import json
import tempfile
import os
from typing import Dict, List, Optional, Tuple, Callable
from dataclasses import dataclass
from enum import Enum


class GameState(Enum):
    """游戏状态枚举"""
    EXPLORING = "exploring"
    COMBAT = "combat"
    DIALOGUE = "dialogue"
    INVENTORY = "inventory"


@dataclass
class Location:
    """游戏地点"""
    id: str
    name: str
    description: str
    connections: Dict[str, str]
    items: List[str]
    npcs: List[str]


@dataclass
class NPC:
    """非玩家角色"""
    id: str
    name: str
    dialogue_tree: Dict[str, List[str]]
    quests: List[str]


class Button:
    """游戏按钮类 - Step 2: UI交互组件"""
    def __init__(self, x: int, y: int, width: int, height: int, text: str,
                 callback: Callable = None, color: Tuple[int, int, int] = (70, 90, 120),
                 text_color: Tuple[int, int, int] = (255, 255, 255)):
        self.rect = pygame.Rect(x, y, width, height)
        self.text = text
        self.callback = callback
        self.color = color
        self.text_color = text_color
        self.hover = False

    def draw(self, surface: pygame.Surface, font: pygame.font.Font):
        """绘制按钮"""
        color = tuple(min(c + 50, 255) for c in self.color) if self.hover else self.color
        pygame.draw.rect(surface, color, self.rect, border_radius=8)
        pygame.draw.rect(surface, (200, 200, 200), self.rect, 2, border_radius=8)

        text_surface = font.render(self.text, True, self.text_color)
        text_rect = text_surface.get_rect(center=self.rect.center)
        surface.blit(text_surface, text_rect)

    def is_clicked(self, pos: Tuple[int, int]) -> bool:
        """检查是否被点击"""
        return self.rect.collidepoint(pos)

    def update_hover(self, pos: Tuple[int, int]):
        """更新悬停状态"""
        self.hover = self.rect.collidepoint(pos)

    def handle_click(self):
        """处理点击"""
        if self.callback:
            self.callback()


class TextAdventureEngine:
    """文字冒险游戏引擎 - Step 1: 游戏逻辑保留"""

    def __init__(self):
        self.current_location: Optional[Location] = None
        self.player_inventory: List[str] = []
        self.visited_locations: set = set()
        self.game_state = GameState.EXPLORING
        self.locations: Dict[str, Location] = {}
        self.npcs: Dict[str, NPC] = {}

    def load_game_data(self, data_file: str) -> None:
        """加载游戏数据"""
        with open(data_file, 'r', encoding='utf-8') as f:
            game_data = json.load(f)

        for loc_data in game_data['locations']:
            location = Location(
                id=loc_data['id'],
                name=loc_data['name'],
                description=loc_data['description'],
                connections=loc_data['connections'],
                items=loc_data.get('items', []),
                npcs=loc_data.get('npcs', [])
            )
            self.locations[location.id] = location

        for npc_data in game_data['npcs']:
            npc = NPC(
                id=npc_data['id'],
                name=npc_data['name'],
                dialogue_tree=npc_data['dialogue_tree'],
                quests=npc_data.get('quests', [])
            )
            self.npcs[npc.id] = npc

        start_location_id = game_data.get('start_location', 'start')
        self.current_location = self.locations.get(start_location_id)
        self.visited_locations.add(start_location_id)

    def move(self, direction: str) -> Tuple[bool, str]:
        """处理移动"""
        if not self.current_location:
            return False, "你不在任何地方!"

        if direction not in self.current_location.connections:
            return False, f"不能往{direction}走"

        next_location_id = self.current_location.connections[direction]
        if next_location_id in self.locations:
            self.current_location = self.locations[next_location_id]
            self.visited_locations.add(next_location_id)
            return True, f"移动到: {self.current_location.name}"
        else:
            return False, "那个地方不存在!"

    def take_item(self, item_name: str) -> Tuple[bool, str]:
        """拿取物品"""
        if not self.current_location:
            return False, "你不在任何地方!"

        if item_name in self.current_location.items:
            self.current_location.items.remove(item_name)
            self.player_inventory.append(item_name)
            return True, f"✓ 获得: {item_name}"
        else:
            return False, f"这里没有{item_name}"

    def talk_to_npc(self, npc_name: str) -> str:
        """与NPC对话"""
        if not self.current_location:
            return "你不在任何地方!"

        for npc_id in self.current_location.npcs:
            npc = self.npcs[npc_id]
            if npc_name.lower() in npc.name.lower():
                return f"{npc.name}: 你好,旅行者!"

        return f"这里没有{npc_name}"

    def get_location_name(self) -> str:
        """获取当前地点名称"""
        return self.current_location.name if self.current_location else "未知地点"

    def get_location_description(self) -> str:
        """获取当前地点描述"""
        return self.current_location.description if self.current_location else "你不在任何地方"

    def get_available_exits(self) -> List[str]:
        """获取可用出口"""
        return list(self.current_location.connections.keys()) if self.current_location else []


class GameUI:
    """游戏UI管理器 - Step 3-6: UI绘制、布局和事件处理"""

    def __init__(self, width: int = 1400, height: int = 900):
        pygame.init()
        self.width = width
        self.height = height
        self.surface = pygame.display.set_mode((width, height))
        pygame.display.set_caption("文字冒险游戏 | TextAdventure")
        self.clock = pygame.time.Clock()

        # 字体加载 - Step 2: 中文字体处理
        self.font_title = self._load_font(32)
        self.font_normal = self._load_font(20)
        self.font_small = self._load_font(16)

        # 颜色定义 - Step 7: 界面美化
        self.COLOR_BG = (25, 28, 35)
        self.COLOR_PANEL = (45, 50, 65)
        self.COLOR_PANEL_BORDER = (80, 120, 180)
        self.COLOR_TEXT = (220, 220, 230)
        self.COLOR_TITLE = (100, 200, 255)
        self.COLOR_BUTTON = (70, 100, 140)
        self.COLOR_BUTTON_MOVE = (100, 140, 100)
        self.COLOR_BUTTON_ITEM = (140, 110, 80)
        self.COLOR_BUTTON_NPC = (140, 100, 120)

        # 布局定义 - Step 3: UI布局
        self.main_panel_rect = pygame.Rect(15, 15, 960, 870)
        self.side_panel_rect = pygame.Rect(990, 15, 395, 870)

        self.buttons: List[Button] = []
        self.game_engine = TextAdventureEngine()
        self.message = ""
        self.message_time = 0

    def _load_font(self, size: int) -> pygame.font.Font:
        """加载中文字体 - 尝试多个系统字体"""
        font_names = [
            "Microsoft YaHei",
            "SimHei",
            "STHeiti",
            "WenQuanYi Micro Hei",
            "Ubuntu",
        ]

        for font_name in font_names:
            try:
                return pygame.font.SysFont(font_name, size)
            except:
                continue

        return pygame.font.Font(None, size)

    def load_game(self, data_file: str):
        """加载游戏数据"""
        self.game_engine.load_game_data(data_file)
        self.update_buttons()

    def update_buttons(self):
        """更新按钮 - Step 4: 交互按钮系统"""
        self.buttons = []

        if not self.game_engine.current_location:
            return

        base_y = 500

        # 移动按钮
        exits = self.game_engine.get_available_exits()
        for i, direction in enumerate(exits):
            self.buttons.append(Button(
                30, base_y + i * 65, 200, 55,
                f"▶ 前往 {direction}",
                lambda d=direction: self._handle_move(d),
                self.COLOR_BUTTON_MOVE
            ))

        # 物品按钮
        button_x = 250
        items = self.game_engine.current_location.items
        for i, item in enumerate(items):
            row = i // 2
            col = i % 2
            self.buttons.append(Button(
                button_x + col * 220, base_y + row * 65, 200, 55,
                f"◆ 拿取: {item[:6]}",
                lambda it=item: self._handle_take_item(it),
                self.COLOR_BUTTON_ITEM
            ))

        # NPC按钮
        npcs = self.game_engine.current_location.npcs
        for i, npc_id in enumerate(npcs):
            npc_name = self.game_engine.npcs[npc_id].name
            self.buttons.append(Button(
                30, 350 + i * 65, 200, 55,
                f"◉ 交谈: {npc_name}",
                lambda n=npc_name: self._handle_talk(n),
                self.COLOR_BUTTON_NPC
            ))

    def _handle_move(self, direction: str):
        """处理移动"""
        success, msg = self.game_engine.move(direction)
        self.message = msg
        self.message_time = 150
        self.update_buttons()

    def _handle_take_item(self, item: str):
        """处理拿取物品"""
        success, msg = self.game_engine.take_item(item)
        self.message = msg
        self.message_time = 150
        self.update_buttons()

    def _handle_talk(self, npc_name: str):
        """处理对话"""
        msg = self.game_engine.talk_to_npc(npc_name)
        self.message = msg
        self.message_time = 150

    def handle_events(self) -> bool:
        """处理事件 - Step 5: 事件处理"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False

            if event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1:
                    for button in self.buttons:
                        if button.is_clicked(event.pos):
                            button.handle_click()

            if event.type == pygame.MOUSEMOTION:
                for button in self.buttons:
                    button.update_hover(event.pos)

        return True

    def _draw_wrapped_text(self, text: str, pos: Tuple[int, int], max_width: int,
                          max_height: int, color: Tuple[int, int, int],
                          line_spacing: int = 28) -> int:
        """改进的换行文本绘制 - 支持中文和英文混合

        Args:
            text: 要绘制的文本
            pos: 起始位置 (x, y)
            max_width: 最大宽度
            max_height: 最大高度
            color: 文本颜色
            line_spacing: 行高

        Returns:
            实际使用的高度
        """
        x, y = pos
        lines = []
        current_line = ""

        # 逐字符处理,支持中英文混合
        for char in text:
            test_line = current_line + char
            test_surface = self.font_normal.render(test_line, True, color)

            if test_surface.get_width() > max_width:
                # 当前行已满,开始新行
                if current_line:
                    lines.append(current_line)
                current_line = char
            else:
                current_line = test_line

        if current_line:
            lines.append(current_line)

        # 绘制所有行,受max_height限制
        actual_height = 0
        for i, line in enumerate(lines):
            line_y = y + i * line_spacing

            # 检查是否超出高度限制
            if line_y + line_spacing > y + max_height:
                break

            text_surface = self.font_normal.render(line, True, color)
            self.surface.blit(text_surface, (x, line_y))
            actual_height = line_y - y + line_spacing

        return actual_height

    def _draw_list_items(self, title: str, items: List[str], pos: Tuple[int, int],
                        max_width: int, title_color: Tuple[int, int, int],
                        item_color: Tuple[int, int, int]) -> int:
        """绘制列表项目(物品、NPC等)

        Returns:
            占用的高度
        """
        x, y = pos

        if not items:
            return 0

        # 绘制标题
        title_surface = self.font_normal.render(title, True, title_color)
        self.surface.blit(title_surface, (x, y))

        current_y = y + 35
        for item in items:
            # 如果单个项目过长,进行换行处理
            if isinstance(item, str):
                display_text = item[:20] + "..." if len(item) > 20 else item
            else:
                display_text = str(item)

            item_text = self.font_small.render(f"  • {display_text}", True, item_color)
            self.surface.blit(item_text, (x, current_y))
            current_y += 28

        return current_y - y

    def draw(self):
        """绘制游戏界面 - Step 5-6: 完整绘制循环"""
        self.surface.fill(self.COLOR_BG)

        # 绘制主面板
        pygame.draw.rect(self.surface, self.COLOR_PANEL, self.main_panel_rect, border_radius=10)
        pygame.draw.rect(self.surface, self.COLOR_PANEL_BORDER, self.main_panel_rect, 3, border_radius=10)

        # 绘制地点标题
        title_text = self.font_title.render(self.game_engine.get_location_name(), True, self.COLOR_TITLE)
        self.surface.blit(title_text, (35, 30))

        # 绘制装饰线
        pygame.draw.line(self.surface, self.COLOR_PANEL_BORDER, (35, 75), (945, 75), 2)

        # 绘制地点描述(改进的换行处理)
        description = self.game_engine.get_location_description()
        desc_height = self._draw_wrapped_text(description, (35, 95), 900, 160, self.COLOR_TEXT)

        # 计算下一个元素的Y位置
        current_y = 95 + desc_height + 20

        # 绘制地点物品
        items = self.game_engine.current_location.items if self.game_engine.current_location else []
        if items and current_y < 400:
            items_height = self._draw_list_items(
                "地点物品:", items, (35, current_y),
                900, (255, 200, 100), self.COLOR_TEXT
            )
            current_y += items_height + 15

        # 绘制地点NPC
        npcs = self.game_engine.current_location.npcs if self.game_engine.current_location else []
        if npcs and current_y < 400:
            npc_names = [self.game_engine.npcs[npc_id].name for npc_id in npcs]
            npc_height = self._draw_list_items(
                "人物:", npc_names, (35, current_y),
                900, (150, 200, 150), self.COLOR_TEXT
            )
            current_y += npc_height + 15

        # 绘制消息提示
        if self.message_time > 0:
            msg_color = (255, 200, 100) if "✓" in self.message else (255, 100, 100)
            msg_surface = self.font_normal.render(self.message, True, msg_color)
            pygame.draw.rect(self.surface, (60, 60, 80), (30, 430, 900, 50), border_radius=5)
            self.surface.blit(msg_surface, (40, 440))
            self.message_time -= 1

        # 绘制按钮
        for button in self.buttons:
            button.draw(self.surface, self.font_small)

        # 绘制侧面板(物品栏)
        pygame.draw.rect(self.surface, self.COLOR_PANEL, self.side_panel_rect, border_radius=10)
        pygame.draw.rect(self.surface, self.COLOR_PANEL_BORDER, self.side_panel_rect, 3, border_radius=10)

        # 绘制物品栏标题
        inv_title = self.font_title.render("背包", True, self.COLOR_TITLE)
        self.surface.blit(inv_title, (1005, 30))

        # 装饰线
        pygame.draw.line(self.surface, self.COLOR_PANEL_BORDER, (1005, 75), (1370, 75), 2)

        # 绘制物品栏内容
        if self.game_engine.player_inventory:
            y_offset = 100
            for item in self.game_engine.player_inventory:
                item_rect = pygame.Rect(1005, y_offset - 5, 360, 50)
                pygame.draw.rect(self.surface, (50, 60, 80), item_rect, border_radius=5)
                item_text = self.font_normal.render(f"■ {item}", True, self.COLOR_TEXT)
                self.surface.blit(item_text, (1015, y_offset))
                y_offset += 60
        else:
            empty_text = self.font_normal.render("(空)", True, (150, 150, 150))
            self.surface.blit(empty_text, (1005, 100))

        # 统计信息
        stats_y = 800
        visited_text = self.font_small.render(f"✓ 已访问地点: {len(self.game_engine.visited_locations)}",
                                             True, self.COLOR_TEXT)
        self.surface.blit(visited_text, (1005, stats_y))

        items_count = len(self.game_engine.player_inventory)
        items_text = self.font_small.render(f"◆ 物品数: {items_count}", True, self.COLOR_TEXT)
        self.surface.blit(items_text, (1005, stats_y + 35))

        pygame.display.flip()

    def run(self):
        """运行游戏主循环"""
        running = True
        while running:
            running = self.handle_events()
            self.draw()
            self.clock.tick(60)

        pygame.quit()


# 游戏数据
game_data = {
    "start_location": "town_square",
    "locations": [
        {
            "id": "town_square",
            "name": "小镇广场",
            "description": "你站在一个古老的小镇广场上。中央有一个精美的喷泉,水流在阳光下闪闪发光。周围是各种各样的建筑,北边是铁匠铺,东边是繁华的市场,西边是温暖的酒馆。微风吹过,传来烤面包和鲜花的香气。",
            "connections": {
                "north": "blacksmith",
                "east": "market",
                "west": "tavern"
            },
            "items": ["古老的硬币"],
            "npcs": ["elder"]
        },
        {
            "id": "blacksmith",
            "name": "铁匠铺",
            "description": "铁匠铺里传来有节奏的打铁声,叮叮当当的声音回响在四周。墙上挂着各种精美的武器和工具。熊熊的炉火照亮了整个房间,空气中弥漫着金属和烟火的气味。一股热浪扑面而来。",
            "connections": {
                "south": "town_square"
            },
            "items": ["铁剑"],
            "npcs": ["blacksmith"]
        },
        {
            "id": "market",
            "name": "市场",
            "description": "市场上人来人往,到处是商人们的叫卖声。各种颜色的摊位排列在街道两旁,出售从新鲜食物到精美工艺品的一切。空气中充满了各种香料和新鲜货物的气味。",
            "connections": {
                "west": "town_square"
            },
            "items": ["新鲜面包", "草药"],
            "npcs": []
        },
        {
            "id": "tavern",
            "name": "酒馆",
            "description": "酒馆里充满了欢乐和温暖的气氛。人们聚集在木制的桌子前谈笑风生,酒保在吧台后擦拭玻璃杯。地面上铺着深色木板,天花板上挂着旧灯笼,光线柔和而温暖。",
            "connections": {
                "east": "town_square"
            },
            "items": [],
            "npcs": ["bartender"]
        }
    ],
    "npcs": [
        {
            "id": "elder",
            "name": "村长",
            "dialogue_tree": {
                "greeting": [
                    "欢迎来到我们的小镇,旅行者。",
                    "最近有些奇怪的事情发生..."
                ]
            },
            "quests": []
        },
        {
            "id": "blacksmith",
            "name": "铁匠",
            "dialogue_tree": {
                "greeting": [
                    "需要什么武器吗?",
                    "我的剑是最好的!"
                ]
            },
            "quests": []
        },
        {
            "id": "bartender",
            "name": "酒保",
            "dialogue_tree": {
                "greeting": [
                    "欢迎来到我的酒馆!",
                    "来杯饮料吗?"
                ]
            },
            "quests": []
        }
    ]
}


def main():
    """主游戏程序入口"""
    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f:
        json.dump(game_data, f, ensure_ascii=False, indent=2)
        data_file = f.name

    try:
        ui = GameUI(1400, 900)
        ui.load_game(data_file)
        ui.run()
    finally:
        os.unlink(data_file)


if __name__ == "__main__":
    main()

这个文字冒险游戏引擎展示了Python在处理复杂游戏逻辑方面的能力。它包括了位置管理、物品系统、NPC交互等核心功能,同时使用了Python 3.13的类型提示特性,提高了代码的可维护性。

3.2 益智类游戏的算法实现

益智类游戏如数独、扫雷、2048等,主要依赖于算法和数据结构。Python的强大标准库和清晰的语法使得实现这些游戏变得相对简单。

数独游戏引擎

复制代码
import pygame
import random
import copy
import sys
from typing import List, Optional, Tuple, Set
from dataclasses import dataclass
from enum import Enum


class GameState(Enum):
    """游戏状态枚举"""
    MENU = "menu"
    PLAYING = "playing"
    WON = "won"
    LOST = "lost"
    PAUSED = "paused"


@dataclass
class SudokuCell:
    """数独单元格"""
    row: int
    col: int
    value: int = 0
    possible_values: Set[int] = None
    is_given: bool = False
    rect: Optional[pygame.Rect] = None  # GUI属性

    def __post_init__(self):
        if self.possible_values is None:
            self.possible_values = set(range(1, 10))


class SudokuGenerator:
    """数独生成器"""

    def __init__(self):
        self.grid: List[List[SudokuCell]] = []
        self.solution: List[List[int]] = []

    def create_empty_grid(self) -> List[List[SudokuCell]]:
        """创建空数独网格"""
        grid = []
        for i in range(9):
            row = []
            for j in range(9):
                row.append(SudokuCell(i, j))
            grid.append(row)
        return grid

    def is_valid_placement(self, grid: List[List[SudokuCell]], row: int, col: int, num: int) -> bool:
        """检查数字放置是否有效"""
        # 检查行
        for j in range(9):
            if grid[row][j].value == num:
                return False

        # 检查列
        for i in range(9):
            if grid[i][col].value == num:
                return False

        # 检查3x3宫格
        start_row, start_col = 3 * (row // 3), 3 * (col // 3)
        for i in range(start_row, start_row + 3):
            for j in range(start_col, start_col + 3):
                if grid[i][j].value == num:
                    return False

        return True

    def solve_sudoku(self, grid: List[List[SudokuCell]]) -> bool:
        """使用回溯算法解数独"""
        for i in range(9):
            for j in range(9):
                if grid[i][j].value == 0:
                    for num in range(1, 10):
                        if self.is_valid_placement(grid, i, j, num):
                            grid[i][j].value = num
                            if self.solve_sudoku(grid):
                                return True
                            grid[i][j].value = 0
                    return False
        return True

    def generate_complete_grid(self) -> List[List[SudokuCell]]:
        """生成完整的数独解答"""
        grid = self.create_empty_grid()

        # 填充对角线上的3个3x3宫格(这些宫格互相独立)
        for i in range(0, 9, 3):
            self.fill_box(grid, i, i)

        # 解剩下的格子
        self.solve_sudoku(grid)

        return grid

    def fill_box(self, grid: List[List[SudokuCell]], row: int, col: int) -> None:
        """填充3x3宫格"""
        nums = list(range(1, 10))
        random.shuffle(nums)

        for i in range(3):
            for j in range(3):
                grid[row + i][col + j].value = nums[i * 3 + j]

    def remove_numbers(self, grid: List[List[SudokuCell]], difficulty: int) -> List[List[SudokuCell]]:
        """根据难度移除数字"""
        attempts = {
            1: 30,    # 简单
            2: 40,    # 中等
            3: 50     # 困难
        }

        cells_to_remove = attempts.get(difficulty, 30)
        puzzle = copy.deepcopy(grid)

        while cells_to_remove > 0:
            row = random.randint(0, 8)
            col = random.randint(0, 8)

            if puzzle[row][col].value != 0:
                backup = puzzle[row][col].value
                puzzle[row][col].value = 0

                # 检查是否仍然有唯一解
                temp_grid = copy.deepcopy(puzzle)
                if self.has_unique_solution(temp_grid):
                    cells_to_remove -= 1
                else:
                    puzzle[row][col].value = backup

        # 标记初始给定数字
        for i in range(9):
            for j in range(9):
                if puzzle[i][j].value != 0:
                    puzzle[i][j].is_given = True

        return puzzle

    def has_unique_solution(self, grid: List[List[SudokuCell]]) -> bool:
        """检查数独是否有唯一解"""
        solutions = [0]

        def count_solutions(g: List[List[SudokuCell]]) -> None:
            if solutions[0] > 1:
                return

            for i in range(9):
                for j in range(9):
                    if g[i][j].value == 0:
                        for num in range(1, 10):
                            if self.is_valid_placement(g, i, j, num):
                                g[i][j].value = num
                                count_solutions(g)
                                g[i][j].value = 0
                        return

            solutions[0] += 1

        count_solutions(grid)
        return solutions[0] == 1

    def generate_puzzle(self, difficulty: int = 1) -> Tuple[List[List[SudokuCell]], List[List[int]]]:
        """生成数独谜题"""
        complete_grid = self.generate_complete_grid()

        # 保存解答
        solution = [[cell.value for cell in row] for row in complete_grid]

        # 生成谜题
        puzzle = self.remove_numbers(complete_grid, difficulty)

        return puzzle, solution


class SudokuGame:
    """数独游戏类"""

    def __init__(self, difficulty: int = 1):
        self.generator = SudokuGenerator()
        self.puzzle, self.solution = self.generator.generate_puzzle(difficulty)
        self.current_grid = copy.deepcopy(self.puzzle)
        self.mistakes = 0
        self.max_mistakes = 3
        self.game_over = False
        self.won = False
        self.difficulty = difficulty
        self.hints_used = 0

    def make_move(self, row: int, col: int, value: int) -> bool:
        """玩家尝试放置数字"""
        if self.game_over:
            return False

        cell = self.current_grid[row][col]

        # 不能修改初始给定数字
        if cell.is_given:
            return False

        # 检查是否正确
        if value == self.solution[row][col]:
            cell.value = value
            self.check_win()
            return True
        else:
            self.mistakes += 1
            if self.mistakes >= self.max_mistakes:
                self.game_over = True
            return False

    def check_win(self) -> None:
        """检查是否获胜"""
        for i in range(9):
            for j in range(9):
                if self.current_grid[i][j].value == 0:
                    return
                if self.current_grid[i][j].value != self.solution[i][j]:
                    return

        self.won = True
        self.game_over = True

    def get_hint(self) -> Optional[Tuple[int, int, int]]:
        """获取提示"""
        empty_cells = []
        for i in range(9):
            for j in range(9):
                if self.current_grid[i][j].value == 0:
                    empty_cells.append((i, j))

        if not empty_cells:
            return None

        row, col = random.choice(empty_cells)
        self.hints_used += 1
        return (row, col, self.solution[row][col])


class Button:
    """按钮类"""
    def __init__(self, x: int, y: int, width: int, height: int, text: str,
                 callback=None, color: Tuple[int, int, int] = (70, 100, 140),
                 text_color: Tuple[int, int, int] = (255, 255, 255)):
        self.rect = pygame.Rect(x, y, width, height)
        self.text = text
        self.callback = callback
        self.color = color
        self.text_color = text_color
        self.hover = False

    def draw(self, surface: pygame.Surface, font: pygame.font.Font):
        """绘制按钮"""
        color = tuple(min(c + 50, 255) for c in self.color) if self.hover else self.color
        pygame.draw.rect(surface, color, self.rect, border_radius=8)
        pygame.draw.rect(surface, (200, 200, 200), self.rect, 2, border_radius=8)

        text_surface = font.render(self.text, True, self.text_color)
        text_rect = text_surface.get_rect(center=self.rect.center)
        surface.blit(text_surface, text_rect)

    def is_clicked(self, pos: Tuple[int, int]) -> bool:
        """检查是否被点击"""
        return self.rect.collidepoint(pos)

    def update_hover(self, pos: Tuple[int, int]):
        """更新悬停状态"""
        self.hover = self.rect.collidepoint(pos)

    def handle_click(self):
        """处理点击"""
        if self.callback:
            self.callback()


class SudokuGameUI:
    """数独游戏UI"""

    def __init__(self, width: int = 1200, height: int = 900):
        pygame.init()
        self.width = width
        self.height = height
        self.surface = pygame.display.set_mode((width, height))
        pygame.display.set_caption("数独游戏 | Sudoku Game")
        self.clock = pygame.time.Clock()

        # 字体加载
        self.font_title = self._load_font(40)
        self.font_normal = self._load_font(20)
        self.font_small = self._load_font(16)
        self.font_cell = self._load_font(28)

        # 颜色定义
        self.COLOR_BG = (25, 28, 35)
        self.COLOR_PANEL = (45, 50, 65)
        self.COLOR_PANEL_BORDER = (80, 120, 180)
        self.COLOR_TEXT = (220, 220, 230)
        self.COLOR_TITLE = (100, 200, 255)
        self.COLOR_CELL_EMPTY = (50, 60, 75)
        self.COLOR_CELL_FILLED = (60, 75, 100)
        self.COLOR_CELL_GIVEN = (70, 85, 110)
        self.COLOR_CELL_SELECTED = (120, 150, 200)
        self.COLOR_CELL_ERROR = (200, 80, 80)
        self.COLOR_GRID_LINE = (100, 120, 150)
        self.COLOR_GRID_THICK = (150, 180, 220)

        # 游戏状态
        self.game_state = GameState.MENU
        self.game: Optional[SudokuGame] = None
        self.selected_cell: Optional[Tuple[int, int]] = None
        self.error_message = ""
        self.error_time = 0
        self.difficulty = 1

        # UI布局
        self.grid_start_x = 50
        self.grid_start_y = 100
        self.cell_size = 70
        self.buttons: List[Button] = []

    def _load_font(self, size: int) -> pygame.font.Font:
        """加载中文字体"""
        font_names = ["Microsoft YaHei", "SimHei", "STHeiti", "WenQuanYi Micro Hei"]
        for font_name in font_names:
            try:
                return pygame.font.SysFont(font_name, size)
            except:
                continue
        return pygame.font.Font(None, size)

    def show_menu(self):
        """显示菜单界面"""
        self.game_state = GameState.MENU
        self.buttons = [
            Button(400, 250, 400, 80, "简单 (30个空格)",
                   lambda: self.start_game(1), (100, 140, 100)),
            Button(400, 380, 400, 80, "中等 (40个空格)",
                   lambda: self.start_game(2), (140, 140, 100)),
            Button(400, 510, 400, 80, "困难 (50个空格)",
                   lambda: self.start_game(3), (140, 100, 100)),
        ]

    def start_game(self, difficulty: int):
        """开始游戏"""
        self.difficulty = difficulty
        self.game = SudokuGame(difficulty)
        self.game_state = GameState.PLAYING
        self.selected_cell = None
        self.error_message = ""
        self.update_game_buttons()

    def update_game_buttons(self):
        """更新游戏中的按钮"""
        self.buttons = [
            Button(800, 150, 150, 50, "提示",
                   self.give_hint, (100, 140, 100)),
            Button(800, 220, 150, 50, "重新开始",
                   self.show_menu, (140, 140, 100)),
            Button(800, 290, 150, 50, "返回菜单",
                   self.show_menu, (140, 100, 100)),
        ]

        # 数字按钮(1-9)
        for i in range(9):
            btn_x = 800 + (i % 3) * 55
            btn_y = 380 + (i // 3) * 55
            self.buttons.append(Button(
                btn_x, btn_y, 45, 45, str(i + 1),
                lambda v=i + 1: self.input_number(v),
                (80, 100, 130)
            ))

        # 删除按钮
        self.buttons.append(Button(
            800, 620, 155, 50, "删除",
            self.delete_number,
            (150, 100, 100)
        ))

    def give_hint(self):
        """提供提示"""
        if self.game and not self.game.game_over:
            hint = self.game.get_hint()
            if hint:
                row, col, value = hint
                self.game.current_grid[row][col].value = value
                self.selected_cell = (row, col)

    def delete_number(self):
        """删除当前选中格子的数字"""
        if self.selected_cell and self.game:
            row, col = self.selected_cell
            cell = self.game.current_grid[row][col]
            if not cell.is_given:
                cell.value = 0

    def input_number(self, num: int):
        """输入数字"""
        if self.selected_cell and self.game and self.game_state == GameState.PLAYING:
            row, col = self.selected_cell
            cell = self.game.current_grid[row][col]

            if cell.is_given:
                self.error_message = "不能修改给定的数字!"
                self.error_time = 150
                return

            if self.game.make_move(row, col, num):
                self.selected_cell = None
                self.error_message = ""
                if self.game.won:
                    self.game_state = GameState.WON
                elif self.game.game_over:
                    self.game_state = GameState.LOST
            else:
                self.error_message = f"错误!还有 {self.game.max_mistakes - self.game.mistakes} 次机会"
                self.error_time = 150

    def handle_events(self) -> bool:
        """处理事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False

            if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                pos = event.pos
                # 检查按钮点击
                for button in self.buttons:
                    if button.is_clicked(pos):
                        button.handle_click()

                # 检查网格点击
                if self.game_state == GameState.PLAYING:
                    self.handle_grid_click(pos)

            if event.type == pygame.MOUSEMOTION:
                for button in self.buttons:
                    button.update_hover(event.pos)

            if event.type == pygame.KEYDOWN:
                if event.unicode.isdigit() and self.game_state == GameState.PLAYING:
                    num = int(event.unicode)
                    if 1 <= num <= 9:
                        self.input_number(num)
                elif event.key == pygame.K_DELETE or event.key == pygame.K_BACKSPACE:
                    self.delete_number()

        return True

    def handle_grid_click(self, pos: Tuple[int, int]):
        """处理网格点击"""
        x, y = pos
        col = (x - self.grid_start_x) // self.cell_size
        row = (y - self.grid_start_y) // self.cell_size

        if 0 <= row < 9 and 0 <= col < 9:
            self.selected_cell = (row, col)

    def draw_menu(self):
        """绘制菜单"""
        self.surface.fill(self.COLOR_BG)

        # 标题
        title = self.font_title.render("数独游戏", True, self.COLOR_TITLE)
        title_rect = title.get_rect(center=(self.width // 2, 100))
        self.surface.blit(title, title_rect)

        # 副标题
        subtitle = self.font_normal.render("请选择难度级别", True, self.COLOR_TEXT)
        subtitle_rect = subtitle.get_rect(center=(self.width // 2, 160))
        self.surface.blit(subtitle, subtitle_rect)

        # 按钮
        for button in self.buttons:
            button.draw(self.surface, self.font_normal)

    def draw_game(self):
        """绘制游戏界面"""
        self.surface.fill(self.COLOR_BG)

        # 标题
        difficulty_text = ["简单", "中等", "困难"]
        title = self.font_title.render(
            f"数独游戏 - {difficulty_text[self.difficulty - 1]}",
            True, self.COLOR_TITLE
        )
        self.surface.blit(title, (50, 20))

        # 绘制网格
        self.draw_grid()

        # 右侧面板
        pygame.draw.rect(self.surface, self.COLOR_PANEL,
                        pygame.Rect(780, 80, 400, 700), border_radius=10)
        pygame.draw.rect(self.surface, self.COLOR_PANEL_BORDER,
                        pygame.Rect(780, 80, 400, 700), 3, border_radius=10)

        # 游戏信息
        y_offset = 100
        error_text = self.font_small.render(
            f"❌ 错误: {self.game.mistakes}/{self.game.max_mistakes}",
            True, (255, 100, 100)
        )
        self.surface.blit(error_text, (800, y_offset))
        y_offset += 40

        hint_text = self.font_small.render(
            f"💡 提示使用: {self.game.hints_used}",
            True, (200, 200, 100)
        )
        self.surface.blit(hint_text, (800, y_offset))

        # 按钮
        for button in self.buttons:
            button.draw(self.surface, self.font_small)

        # 错误提示
        if self.error_time > 0:
            error_surface = self.font_normal.render(self.error_message, True, (255, 100, 100))
            self.surface.blit(error_surface, (50, 750))
            self.error_time -= 1

    def draw_grid(self):
        """绘制数独网格"""
        if not self.game:
            return

        for i in range(9):
            for j in range(9):
                cell = self.game.current_grid[i][j]
                x = self.grid_start_x + j * self.cell_size
                y = self.grid_start_y + i * self.cell_size
                cell.rect = pygame.Rect(x, y, self.cell_size, self.cell_size)

                # 确定背景色
                if self.selected_cell == (i, j):
                    bg_color = self.COLOR_CELL_SELECTED
                elif cell.is_given:
                    bg_color = self.COLOR_CELL_GIVEN
                elif cell.value != 0:
                    bg_color = self.COLOR_CELL_FILLED
                else:
                    bg_color = self.COLOR_CELL_EMPTY

                pygame.draw.rect(self.surface, bg_color, cell.rect)

                # 绘制数字
                if cell.value != 0:
                    text_color = self.COLOR_TEXT if not cell.is_given else (150, 200, 255)
                    value_text = self.font_cell.render(str(cell.value), True, text_color)
                    text_rect = value_text.get_rect(center=cell.rect.center)
                    self.surface.blit(value_text, text_rect)

        # 绘制网格线
        # 细线
        for i in range(10):
            if i % 3 == 0:
                continue
            # 竖线
            x = self.grid_start_x + i * self.cell_size
            pygame.draw.line(self.surface, self.COLOR_GRID_LINE,
                            (x, self.grid_start_y), (x, self.grid_start_y + 630), 1)
            # 横线
            y = self.grid_start_y + i * self.cell_size
            pygame.draw.line(self.surface, self.COLOR_GRID_LINE,
                            (self.grid_start_x, y), (self.grid_start_x + 630, y), 1)

        # 粗线(3x3宫格)
        for i in range(0, 10, 3):
            # 竖线
            x = self.grid_start_x + i * self.cell_size
            pygame.draw.line(self.surface, self.COLOR_GRID_THICK,
                            (x, self.grid_start_y), (x, self.grid_start_y + 630), 3)
            # 横线
            y = self.grid_start_y + i * self.cell_size
            pygame.draw.line(self.surface, self.COLOR_GRID_THICK,
                            (self.grid_start_x, y), (self.grid_start_x + 630, y), 3)

    def draw_win_screen(self):
        """绘制胜利界面"""
        self.draw_game()

        # 半透明覆盖层
        overlay = pygame.Surface((self.width, self.height))
        overlay.set_alpha(200)
        overlay.fill((0, 0, 0))
        self.surface.blit(overlay, (0, 0))

        # 胜利文字
        win_text = self.font_title.render("🎉 恭喜你!", True, (100, 255, 100))
        win_rect = win_text.get_rect(center=(self.width // 2, self.height // 2 - 80))
        self.surface.blit(win_text, win_rect)

        info_text = self.font_normal.render(
            f"完成时间高效!提示使用: {self.game.hints_used} 次",
            True, self.COLOR_TEXT
        )
        info_rect = info_text.get_rect(center=(self.width // 2, self.height // 2))
        self.surface.blit(info_text, info_rect)

        # 按钮
        retry_btn = Button(350, self.height // 2 + 100, 150, 50, "再来一局",
                          self.show_menu, (100, 140, 100))
        retry_btn.draw(self.surface, self.font_normal)
        if retry_btn.is_clicked(pygame.mouse.get_pos()):
            retry_btn.hover = True

    def draw_lose_screen(self):
        """绘制失败界面"""
        self.draw_game()

        # 半透明覆盖层
        overlay = pygame.Surface((self.width, self.height))
        overlay.set_alpha(200)
        overlay.fill((0, 0, 0))
        self.surface.blit(overlay, (0, 0))

        # 失败文字
        lose_text = self.font_title.render("💔 游戏结束", True, (255, 100, 100))
        lose_rect = lose_text.get_rect(center=(self.width // 2, self.height // 2 - 80))
        self.surface.blit(lose_text, lose_rect)

        info_text = self.font_normal.render(
            f"你达到了最大错误次数: {self.game.max_mistakes}",
            True, self.COLOR_TEXT
        )
        info_rect = info_text.get_rect(center=(self.width // 2, self.height // 2))
        self.surface.blit(info_text, info_rect)

        # 按钮
        retry_btn = Button(350, self.height // 2 + 100, 150, 50, "重新开始",
                          self.show_menu, (100, 140, 100))
        retry_btn.draw(self.surface, self.font_normal)

    def draw(self):
        """绘制界面"""
        if self.game_state == GameState.MENU:
            self.draw_menu()
        elif self.game_state == GameState.WON:
            self.draw_win_screen()
        elif self.game_state == GameState.LOST:
            self.draw_lose_screen()
        else:
            self.draw_game()

        pygame.display.flip()

    def run(self):
        """运行游戏主循环"""
        self.show_menu()
        running = True

        while running:
            running = self.handle_events()
            self.draw()
            self.clock.tick(60)

        pygame.quit()


def main():
    """主程序入口"""
    ui = SudokuGameUI(1200, 900)
    ui.run()


if __name__ == "__main__":
    main()

这个数独游戏展示了Python在算法实现方面的优势。它包含了数独生成、求解、验证等核心算法,同时也展示了如何使用面向对象的设计来组织游戏逻辑。

3.3 休闲类游戏的交互设计

休闲类游戏通常具有简单的玩法和轻松的氛围,适合短暂的娱乐体验。这类游戏包括点击类游戏、消除类游戏、收集类游戏等。

点击消除游戏实现

复制代码
import pygame
import random
from typing import List, Tuple, Dict, Optional
from dataclasses import dataclass, field
from enum import Enum
import math
import time

# 初始化pygame
pygame.init()

class Color(Enum):
    """游戏颜色定义"""
    BACKGROUND = (240, 248, 255)
    GRID = (200, 200, 200)
    TEXT = (50, 50, 50)
    BUTTON = (70, 130, 180)
    BUTTON_HOVER = (100, 149, 237)
    SCORE = (255, 140, 0)

    # 宝石颜色
    RED = (255, 107, 107)
    BLUE = (78, 205, 196)
    GREEN = (85, 239, 196)
    YELLOW = (255, 234, 167)
    PURPLE = (175, 122, 197)
    ORANGE = (255, 183, 77)

@dataclass
class Gem:
    """宝石类"""
    row: int
    col: int
    gem_type: int
    color: Tuple[int, int, int]
    scale: float = 1.0
    removing: bool = False

    # 动画属性
    target_row: float = field(default=None)
    target_col: float = field(default=None)
    animation_progress: float = 0.0
    animation_type: str = None  # 'swap', 'fall', 'remove'
    removal_progress: float = 0.0  # 消除动画进度

    def __post_init__(self):
        if self.target_row is None:
            self.target_row = float(self.row)
        if self.target_col is None:
            self.target_col = float(self.col)

    def get_display_pos(self, cell_size: int) -> Tuple[float, float]:
        """获取显示位置,考虑动画偏移"""
        row = self.row + (self.target_row - self.row) * self.animation_progress
        col = self.col + (self.target_col - self.col) * self.animation_progress
        return row, col

    def draw(self, screen: pygame.Surface, cell_size: int, offset_x: int, offset_y: int) -> None:
        """绘制宝石"""
        display_row, display_col = self.get_display_pos(cell_size)

        center_x = offset_x + display_col * cell_size + cell_size // 2
        center_y = offset_y + display_row * cell_size + cell_size // 2

        # 根据消除进度调整缩放和透明度
        if self.animation_type == 'remove':
            scale = self.scale * (1.0 - self.removal_progress)
            alpha = int(255 * (1.0 - self.removal_progress))
        else:
            scale = self.scale
            alpha = 255

        size = int(cell_size * 0.8 * scale)

        if size > 0:
            # 创建透明宝石表面
            gem_surface = pygame.Surface((size, size), pygame.SRCALPHA)

            # 绘制圆形宝石
            pygame.draw.circle(gem_surface, (*self.color, alpha), (size // 2, size // 2), size // 2)

            # 添加高光效果
            highlight_pos = (size // 4, size // 4)
            pygame.draw.circle(gem_surface, (255, 255, 255, alpha // 2), highlight_pos, size // 8)

            # 将宝石绘制到屏幕上
            screen.blit(gem_surface, (center_x - size // 2, center_y - size // 2))

class Match3Game:
    """三消游戏"""

    def __init__(self, width: int = 1000, height: int = 700):
        self.width = width
        self.height = height
        self.screen = pygame.display.set_mode((width, height))
        pygame.display.set_caption("Python三消游戏")

        # 游戏配置
        self.grid_rows = 8
        self.grid_cols = 8
        self.cell_size = 60
        self.gem_types = 5
        self.grid_offset_x = 50
        self.grid_offset_y = 150  # 增加到150,为顶部文字留出空间

        # 游戏状态
        self.grid: List[List[Optional[Gem]]] = []
        self.selected_gem: Optional[Tuple[int, int]] = None
        self.score = 0
        self.game_state = "playing"  # playing, game_over, animating

        # 时间系统
        self.time_limit = 120  # 2分钟 (秒)
        self.game_start_time = 0
        self.time_remaining = self.time_limit

        # 刷新系统
        self.refresh_count = 3  # 手动刷新次数
        self.refresh_attempts = 0  # 自动刷新次数(仅用于显示)
        self.last_check_time = 0  # 上次检查无牌可消的时间

        # 动画控制
        self.swap_animation = None
        self.fall_animations: List[Gem] = []
        self.removal_animations: List[Gem] = []
        self.animation_duration = 20  # 帧数

        # 字体 - 使用支持中文的字体
        self.font_large = self._load_font(48)
        self.font_medium = self._load_font(36)
        self.font_small = self._load_font(24)

        # 按钮
        self.buttons: Dict[str, Dict] = {}
        self.create_buttons()

        # 初始化游戏
        self.init_game()

    def _load_font(self, size: int) -> pygame.font.Font:
        """加载支持中文的字体"""
        font_names = [
            "Microsoft YaHei",      # 微软雅黑
            "SimHei",               # 黑体
            "SimSun",               # 宋体
            "STHeiti",              # 华文黑体
            "WenQuanYi Micro Hei",  # 文泉驿微米黑
            "DejaVu Sans"           # 备选字体
        ]

        for font_name in font_names:
            try:
                return pygame.font.SysFont(font_name, size)
            except Exception:
                continue

        return pygame.font.Font(None, size)

    def create_buttons(self) -> None:
        """创建游戏按钮 - 放在右侧面板"""
        button_width = 120
        button_height = 45
        panel_x = self.grid_offset_x + self.grid_cols * self.cell_size + 50
        panel_y = 320

        self.buttons = {
            'refresh': {
                'rect': pygame.Rect(panel_x, panel_y, button_width, button_height),
                'text': '手动刷新',
                'action': self.manual_refresh,
                'color': (150, 120, 100),
                'hover': False
            },
            'restart': {
                'rect': pygame.Rect(panel_x, panel_y + 70, button_width, button_height),
                'text': '重新开始',
                'action': self.restart_game,
                'color': (100, 140, 100),
                'hover': False
            },
            'quit': {
                'rect': pygame.Rect(panel_x, panel_y + 140, button_width, button_height),
                'text': '退出游戏',
                'action': self.quit_game,
                'color': (180, 100, 100),
                'hover': False
            }
        }

    def init_game(self) -> None:
        """初始化游戏"""
        self.score = 0
        self.game_state = "playing"
        self.selected_gem = None
        self.swap_animation = None
        self.fall_animations = []
        self.removal_animations = []
        self.refresh_count = 3
        self.refresh_attempts = 0
        self.time_remaining = self.time_limit
        self.game_start_time = time.time()
        self.last_check_time = time.time()
        self.create_grid()

        # 确保初始状态没有匹配
        while self.find_matches():
            self.create_grid()

    def create_grid(self) -> None:
        """创建游戏网格"""
        self.grid = [[None for _ in range(self.grid_cols)] for _ in range(self.grid_rows)]

        colors = [
            Color.RED.value, Color.BLUE.value, Color.GREEN.value,
            Color.YELLOW.value, Color.PURPLE.value
        ]

        for row in range(self.grid_rows):
            for col in range(self.grid_cols):
                gem_type = random.randint(0, self.gem_types - 1)
                gem = Gem(row, col, gem_type, colors[gem_type])
                self.grid[row][col] = gem

    def find_matches(self) -> List[Tuple[int, int]]:
        """查找所有匹配"""
        matches = set()

        # 检查水平匹配
        for row in range(self.grid_rows):
            for col in range(self.grid_cols - 2):
                if (self.grid[row][col] and
                    self.grid[row][col + 1] and
                    self.grid[row][col + 2]):

                    gem_type = self.grid[row][col].gem_type
                    if (self.grid[row][col + 1].gem_type == gem_type and
                        self.grid[row][col + 2].gem_type == gem_type):
                        matches.add((row, col))
                        matches.add((row, col + 1))
                        matches.add((row, col + 2))

        # 检查垂直匹配
        for col in range(self.grid_cols):
            for row in range(self.grid_rows - 2):
                if (self.grid[row][col] and
                    self.grid[row + 1][col] and
                    self.grid[row + 2][col]):

                    gem_type = self.grid[row][col].gem_type
                    if (self.grid[row + 1][col].gem_type == gem_type and
                        self.grid[row + 2][col].gem_type == gem_type):
                        matches.add((row, col))
                        matches.add((row + 1, col))
                        matches.add((row + 2, col))

        return list(matches)

    def has_possible_moves(self) -> bool:
        """检查是否还有可能的移动"""
        # 尝试所有可能的相邻交换
        for row in range(self.grid_rows):
            for col in range(self.grid_cols):
                # 检查右边的宝石
                if col + 1 < self.grid_cols:
                    # 临时交换
                    self.grid[row][col], self.grid[row][col + 1] = \
                        self.grid[row][col + 1], self.grid[row][col]

                    # 检查是否有匹配
                    if self.find_matches():
                        # 交换回来
                        self.grid[row][col], self.grid[row][col + 1] = \
                            self.grid[row][col + 1], self.grid[row][col]
                        return True

                    # 交换回来
                    self.grid[row][col], self.grid[row][col + 1] = \
                        self.grid[row][col + 1], self.grid[row][col]

                # 检查下面的宝石
                if row + 1 < self.grid_rows:
                    # 临时交换
                    self.grid[row][col], self.grid[row + 1][col] = \
                        self.grid[row + 1][col], self.grid[row][col]

                    # 检查是否有匹配
                    if self.find_matches():
                        # 交换回来
                        self.grid[row][col], self.grid[row + 1][col] = \
                            self.grid[row + 1][col], self.grid[row][col]
                        return True

                    # 交换回来
                    self.grid[row][col], self.grid[row + 1][col] = \
                        self.grid[row + 1][col], self.grid[row][col]

        return False

    def handle_click(self, pos: Tuple[int, int]) -> None:
        """处理鼠标点击"""
        if self.game_state != "playing" or self.swap_animation or self.fall_animations:
            return

        # 检查是否点击了按钮
        for button_name, button in self.buttons.items():
            if button['rect'].collidepoint(pos):
                button['action']()
                return

        # 检查是否点击了网格
        grid_rect = pygame.Rect(
            self.grid_offset_x, self.grid_offset_y,
            self.grid_cols * self.cell_size, self.grid_rows * self.cell_size
        )

        if grid_rect.collidepoint(pos):
            col = (pos[0] - self.grid_offset_x) // self.cell_size
            row = (pos[1] - self.grid_offset_y) // self.cell_size

            if 0 <= row < self.grid_rows and 0 <= col < self.grid_cols:
                self.select_gem(row, col)

    def select_gem(self, row: int, col: int) -> None:
        """选择宝石"""
        if self.selected_gem is None:
            self.selected_gem = (row, col)
        else:
            prev_row, prev_col = self.selected_gem

            # 检查是否选择了相邻的宝石
            if ((abs(row - prev_row) == 1 and col == prev_col) or
                (abs(col - prev_col) == 1 and row == prev_row)):
                self.swap_gems(prev_row, prev_col, row, col)
                self.selected_gem = None
            else:
                self.selected_gem = (row, col)

    def swap_gems(self, row1: int, col1: int, row2: int, col2: int) -> None:
        """交换两个宝石 - 启动动画"""
        gem1 = self.grid[row1][col1]
        gem2 = self.grid[row2][col2]

        # 立即交换网格位置(这样 find_matches 会使用交换后的位置)
        self.grid[row1][col1] = gem2
        self.grid[row2][col2] = gem1

        # 设置宝石的动画目标(新网格位置)
        if gem1:
            gem1.target_row = float(row2)
            gem1.target_col = float(col2)
            gem1.animation_type = 'swap'
            gem1.animation_progress = 0.0

        if gem2:
            gem2.target_row = float(row1)
            gem2.target_col = float(col1)
            gem2.animation_type = 'swap'
            gem2.animation_progress = 0.0

        # 启动交换动画
        self.swap_animation = {
            'gem1': gem1,
            'gem2': gem2,
            'progress': 0.0,
            'reverse': False,
            'gem1_pos': (row1, col1),
            'gem2_pos': (row2, col2)
        }

        # 检查是否有匹配
        matches = self.find_matches()
        if not matches:
            # 如果没有匹配,交换回来(需要反向动画)
            self.swap_animation['reverse'] = True

    def update_swap_animation(self) -> None:
        """更新交换动画"""
        if not self.swap_animation:
            return

        self.swap_animation['progress'] += 1.0 / self.animation_duration

        if self.swap_animation['progress'] >= 1.0:
            self.swap_animation['progress'] = 1.0

            # 动画完成
            if self.swap_animation.get('reverse'):
                # 需要反向交换回去
                gem1 = self.swap_animation['gem1']
                gem2 = self.swap_animation['gem2']
                row1, col1 = self.swap_animation['gem1_pos']
                row2, col2 = self.swap_animation['gem2_pos']

                # 交换回来
                self.grid[row1][col1] = gem1
                self.grid[row2][col2] = gem2

                # 重置宝石动画状态
                if gem1:
                    gem1.target_row = float(row1)
                    gem1.target_col = float(col1)
                    gem1.animation_progress = 0.0

                if gem2:
                    gem2.target_row = float(row2)
                    gem2.target_col = float(col2)
                    gem2.animation_progress = 0.0

                self.swap_animation = None
            else:
                # 正常完成,更新宝石的逻辑位置并处理匹配
                gem1 = self.swap_animation['gem1']
                gem2 = self.swap_animation['gem2']

                # 更新宝石的逻辑位置
                if gem1:
                    gem1.row = int(gem1.target_row)
                    gem1.col = int(gem1.target_col)
                    gem1.animation_progress = 0.0

                if gem2:
                    gem2.row = int(gem2.target_row)
                    gem2.col = int(gem2.target_col)
                    gem2.animation_progress = 0.0

                matches = self.find_matches()
                self.process_matches(matches)
                self.swap_animation = None
        else:
            # 更新宝石动画进度
            if self.swap_animation['gem1']:
                self.swap_animation['gem1'].animation_progress = self.swap_animation['progress']
            if self.swap_animation['gem2']:
                self.swap_animation['gem2'].animation_progress = self.swap_animation['progress']

    def process_matches(self, matches: List[Tuple[int, int]]) -> None:
        """处理匹配"""
        if not matches:
            return

        # 计算得分
        self.score += len(matches) * 10

        # 标记要移除的宝石
        for row, col in matches:
            if self.grid[row][col]:
                self.grid[row][col].animation_type = 'remove'
                self.removal_animations.append(self.grid[row][col])
                self.grid[row][col] = None

        # 安排下落动画
        self.schedule_fall_animations()

    def schedule_fall_animations(self) -> None:
        """安排下落动画"""
        self.fall_animations = []

        # 从下往上处理每列
        for col in range(self.grid_cols):
            fall_distance = 0

            # 从下往上遍历
            for row in range(self.grid_rows - 1, -1, -1):
                if self.grid[row][col] is None:
                    fall_distance += 1
                elif fall_distance > 0:
                    # 获取要下落的宝石
                    gem = self.grid[row][col]

                    # 将宝石下移
                    self.grid[row + fall_distance][col] = gem
                    self.grid[row][col] = None

                    # 设置宝石的动画参数
                    # row/col 保持原始位置用于动画起点
                    gem.target_row = float(row + fall_distance)
                    gem.target_col = float(col)
                    gem.animation_type = 'fall'
                    gem.animation_progress = 0.0

                    # 记录下落动画
                    self.fall_animations.append(gem)

            # 在顶部填充新宝石
            colors = [
                Color.RED.value, Color.BLUE.value, Color.GREEN.value,
                Color.YELLOW.value, Color.PURPLE.value
            ]

            for spawn_row in range(fall_distance):
                gem_type = random.randint(0, self.gem_types - 1)
                # 新宝石从上方掉下来,所以初始 row 设置为负数
                gem = Gem(-fall_distance + spawn_row, col, gem_type, colors[gem_type])
                gem.animation_type = 'fall'
                gem.animation_progress = 0.0
                gem.target_row = float(spawn_row)
                gem.target_col = float(col)
                self.grid[spawn_row][col] = gem
                self.fall_animations.append(gem)

    def update_fall_animations(self) -> None:
        """更新下落动画"""
        if not self.fall_animations:
            return

        finished = []
        for i, gem in enumerate(self.fall_animations):
            if gem.animation_progress < 1.0:
                gem.animation_progress += 1.0 / self.animation_duration
                if gem.animation_progress >= 1.0:
                    gem.animation_progress = 1.0
                    finished.append(i)

        # 移除完成的动画
        for i in reversed(finished):
            gem = self.fall_animations[i]
            # 更新宝石的逻辑位置
            gem.row = int(gem.target_row)
            gem.col = int(gem.target_col)
            self.fall_animations.pop(i)

        # 如果所有下落动画完成,检查新的匹配
        if not self.fall_animations and not self.removal_animations:
            new_matches = self.find_matches()
            if new_matches:
                self.process_matches(new_matches)

    def update_removal_animations(self) -> None:
        """更新消除动画"""
        if not self.removal_animations:
            return

        finished = []
        for i, gem in enumerate(self.removal_animations):
            gem.removal_progress += 1.0 / (self.animation_duration * 0.8)
            if gem.removal_progress >= 1.0:
                gem.removal_progress = 1.0
                finished.append(i)

        # 移除完成的动画
        for i in reversed(finished):
            self.removal_animations.pop(i)

    def auto_refresh(self) -> None:
        """自动刷新(无手动次数限制)"""
        colors = [
            Color.RED.value, Color.BLUE.value, Color.GREEN.value,
            Color.YELLOW.value, Color.PURPLE.value
        ]

        # 重新生成网格
        for row in range(self.grid_rows):
            for col in range(self.grid_cols):
                gem_type = random.randint(0, self.gem_types - 1)
                gem = Gem(row, col, gem_type, colors[gem_type])
                self.grid[row][col] = gem

        # 确保刷新后没有匹配
        while self.find_matches():
            for row in range(self.grid_rows):
                for col in range(self.grid_cols):
                    gem_type = random.randint(0, self.gem_types - 1)
                    gem = Gem(row, col, gem_type, colors[gem_type])
                    self.grid[row][col] = gem

        self.refresh_attempts += 1
        self.last_check_time = time.time()

    def manual_refresh(self) -> None:
        """手动刷新"""
        if self.game_state != "playing":
            return

        if self.refresh_count > 0:
            self.auto_refresh()
            self.refresh_count -= 1
        else:
            # 刷新次数已用尽
            pass

    def restart_game(self) -> None:
        """重新开始游戏"""
        self.init_game()

    def quit_game(self) -> None:
        """退出游戏"""
        pygame.quit()
        exit()

    def update(self) -> None:
        """更新游戏状态"""
        # 更新时间
        if self.game_state == "playing":
            self.time_remaining = self.time_limit - (time.time() - self.game_start_time)

            # 检查是否时间用尽
            if self.time_remaining <= 0:
                self.time_remaining = 0
                self.game_state = "game_over"
                return

        self.update_swap_animation()
        self.update_fall_animations()
        self.update_removal_animations()

        # 检查是否需要自动刷新(只在没有动画时检查)
        if (self.game_state == "playing" and
            not self.swap_animation and
            not self.fall_animations and
            not self.removal_animations):

            # 每隔1秒检查一次
            if time.time() - self.last_check_time > 1.0:
                if not self.has_possible_moves():
                    self.auto_refresh()

        # 更新按钮悬停状态
        mouse_pos = pygame.mouse.get_pos()
        for button in self.buttons.values():
            button['hover'] = button['rect'].collidepoint(mouse_pos)

    def draw(self) -> None:
        """绘制游戏"""
        self.screen.fill(Color.BACKGROUND.value)

        # 绘制标题
        title = self.font_large.render("Python三消游戏", True, Color.TEXT.value)
        title_rect = title.get_rect(center=(self.width // 2, 40))
        self.screen.blit(title, title_rect)

        # 绘制分数和时间
        score_text = self.font_medium.render(f"分数: {self.score}", True, Color.SCORE.value)

        # 格式化时间显示
        minutes = int(self.time_remaining) // 60
        seconds = int(self.time_remaining) % 60
        time_color = (255, 0, 0) if self.time_remaining < 30 else Color.TEXT.value
        time_text = self.font_medium.render(f"时间: {minutes:02d}:{seconds:02d}", True, time_color)

        self.screen.blit(score_text, (self.grid_offset_x, 75))
        self.screen.blit(time_text, (self.grid_offset_x + 300, 75))

        # 绘制网格背景
        grid_rect = pygame.Rect(
            self.grid_offset_x - 5, self.grid_offset_y - 5,
            self.grid_cols * self.cell_size + 10, self.grid_rows * self.cell_size + 10
        )
        pygame.draw.rect(self.screen, Color.GRID.value, grid_rect, border_radius=10)

        # 绘制网格线
        for i in range(self.grid_rows + 1):
            pygame.draw.line(
                self.screen, (180, 180, 180),
                (self.grid_offset_x, self.grid_offset_y + i * self.cell_size),
                (self.grid_offset_x + self.grid_cols * self.cell_size, self.grid_offset_y + i * self.cell_size)
            )

        for j in range(self.grid_cols + 1):
            pygame.draw.line(
                self.screen, (180, 180, 180),
                (self.grid_offset_x + j * self.cell_size, self.grid_offset_y),
                (self.grid_offset_x + j * self.cell_size, self.grid_offset_y + self.grid_rows * self.cell_size)
            )

        # 绘制宝石
        for row in range(self.grid_rows):
            for col in range(self.grid_cols):
                if self.grid[row][col]:
                    self.grid[row][col].draw(self.screen, self.cell_size,
                                           self.grid_offset_x, self.grid_offset_y)

        # 绘制正在消除的宝石
        for gem in self.removal_animations:
            gem.draw(self.screen, self.cell_size, self.grid_offset_x, self.grid_offset_y)

        # 绘制选中效果
        if self.selected_gem:
            row, col = self.selected_gem
            highlight_rect = pygame.Rect(
                self.grid_offset_x + col * self.cell_size,
                self.grid_offset_y + row * self.cell_size,
                self.cell_size, self.cell_size
            )
            pygame.draw.rect(self.screen, (255, 255, 0), highlight_rect, 3, border_radius=5)

        # 绘制按钮
        for button_name, button in self.buttons.items():
            color = button['color']
            if button['hover']:
                color = tuple(min(c + 40, 255) for c in color)

            pygame.draw.rect(self.screen, color, button['rect'], border_radius=5)
            pygame.draw.rect(self.screen, (100, 100, 100), button['rect'], 2, border_radius=5)

            text = self.font_small.render(button['text'], True, (255, 255, 255))
            text_rect = text.get_rect(center=button['rect'].center)
            self.screen.blit(text, text_rect)

        # 绘制右侧信息面板
        panel_x = self.grid_offset_x + self.grid_cols * self.cell_size + 30
        info_text = self.font_small.render("游戏操作:", True, Color.TEXT.value)
        self.screen.blit(info_text, (panel_x, 160))

        info_texts = [
            "• 点击宝石选中",
            "• 点击相邻宝石交换",
        ]

        for i, text in enumerate(info_texts):
            info_surface = self.font_small.render(text, True, (100, 100, 100))
            self.screen.blit(info_surface, (panel_x, 190 + i * 25))

        # 绘制刷新次数
        refresh_text = self.font_small.render(f"手动刷新: {self.refresh_count}次", True, Color.TEXT.value)
        self.screen.blit(refresh_text, (panel_x, 275))

        # 绘制游戏结束信息
        if self.game_state == "game_over":
            overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
            overlay.fill((0, 0, 0, 128))
            self.screen.blit(overlay, (0, 0))

            game_over_text = self.font_large.render("游戏结束!", True, (255, 255, 255))
            final_score_text = self.font_medium.render(f"最终分数: {self.score}", True, (255, 255, 255))

            game_over_rect = game_over_text.get_rect(center=(self.width // 2, self.height // 2 - 30))
            score_rect = final_score_text.get_rect(center=(self.width // 2, self.height // 2 + 30))

            self.screen.blit(game_over_text, game_over_rect)
            self.screen.blit(final_score_text, score_rect)

        pygame.display.flip()

    def run(self) -> None:
        """运行游戏主循环"""
        clock = pygame.time.Clock()
        running = True

        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 1:  # 左键点击
                        self.handle_click(event.pos)

            self.update()
            self.draw()
            clock.tick(60)

        pygame.quit()

def main():
    """主函数"""
    game = Match3Game()
    game.run()

if __name__ == "__main__":
    main()

这个三消游戏展示了Python在图形界面游戏开发方面的能力。它使用了Pygame库来处理图形渲染和用户交互,同时也展示了如何设计游戏的状态管理和用户界面。

第4章 动作类游戏开发技术

4.1 实时碰撞检测算法

动作类游戏的核心技术之一是碰撞检测,它决定了游戏世界中物体之间的交互。Python提供了多种实现碰撞检测的方法,从简单的矩形碰撞到复杂的像素级碰撞。

碰撞检测系统实现

复制代码
import pygame
import math
from typing import List, Tuple, Optional, Dict, Callable
from dataclasses import dataclass
from enum import Enum
import random


class CollisionType(Enum):
    """碰撞类型"""
    NONE = "none"
    AABB = "aabb"
    CIRCLE = "circle"
    PIXEL = "pixel"
    POLYGON = "polygon"


@dataclass
class BoundingBox:
    """包围盒"""
    x: float
    y: float
    width: float
    height: float

    def get_center(self) -> Tuple[float, float]:
        """获取中心点"""
        return (self.x + self.width / 2, self.y + self.height / 2)

    def get_rect(self) -> pygame.Rect:
        """获取pygame矩形对象"""
        return pygame.Rect(int(self.x), int(self.y), int(self.width), int(self.height))

    def contains_point(self, point: Tuple[float, float]) -> bool:
        """检查点是否在包围盒内"""
        x, y = point
        return (self.x <= x <= self.x + self.width and
                self.y <= y <= self.y + self.height)


@dataclass
class Circle:
    """圆形碰撞体"""
    x: float
    y: float
    radius: float

    def get_center(self) -> Tuple[float, float]:
        """获取圆心"""
        return (self.x, self.y)

    def contains_point(self, point: Tuple[float, float]) -> bool:
        """检查点是否在圆内"""
        px, py = point
        distance = math.sqrt((px - self.x) ** 2 + (py - self.y) ** 2)
        return distance <= self.radius


class CollisionSystem:
    """碰撞检测系统"""

    def __init__(self):
        self.colliders: Dict[int, Dict] = {}
        self.collision_info: Dict[Tuple[int, int], Dict] = {}
        self.next_id = 0

    def add_collider(self, obj_id: int, collision_type: CollisionType,
                     collider_data: Dict) -> None:
        """添加碰撞体"""
        self.colliders[obj_id] = {
            'type': collision_type,
            'data': collider_data,
            'id': obj_id
        }

    def remove_collider(self, obj_id: int) -> None:
        """移除碰撞体"""
        if obj_id in self.colliders:
            del self.colliders[obj_id]

    def check_collision(self, obj1_id: int, obj2_id: int) -> bool:
        """检查两个物体是否碰撞"""
        if obj1_id not in self.colliders or obj2_id not in self.colliders:
            return False

        collider1 = self.colliders[obj1_id]
        collider2 = self.colliders[obj2_id]

        if collider1['type'] == CollisionType.AABB and collider2['type'] == CollisionType.AABB:
            return self.check_aabb_aabb(collider1['data'], collider2['data'])
        elif collider1['type'] == CollisionType.CIRCLE and collider2['type'] == CollisionType.CIRCLE:
            return self.check_circle_circle(collider1['data'], collider2['data'])
        elif collider1['type'] == CollisionType.AABB and collider2['type'] == CollisionType.CIRCLE:
            return self.check_aabb_circle(collider1['data'], collider2['data'])
        elif collider1['type'] == CollisionType.CIRCLE and collider2['type'] == CollisionType.AABB:
            return self.check_aabb_circle(collider2['data'], collider1['data'])

        return False

    def check_aabb_aabb(self, box1: BoundingBox, box2: BoundingBox) -> bool:
        """检查两个包围盒是否碰撞"""
        return (box1.x < box2.x + box2.width and
                box1.x + box1.width > box2.x and
                box1.y < box2.y + box2.height and
                box1.y + box1.height > box2.y)

    def check_circle_circle(self, circle1: Circle, circle2: Circle) -> bool:
        """检查两个圆是否碰撞"""
        distance = math.sqrt((circle1.x - circle2.x) ** 2 + (circle1.y - circle2.y) ** 2)
        return distance < (circle1.radius + circle2.radius)

    def check_aabb_circle(self, box: BoundingBox, circle: Circle) -> bool:
        """检查包围盒和圆是否碰撞,并计算碰撞法线"""
        closest_x = max(box.x, min(circle.x, box.x + box.width))
        closest_y = max(box.y, min(circle.y, box.y + box.height))

        distance_x = circle.x - closest_x
        distance_y = circle.y - closest_y
        distance_squared = distance_x ** 2 + distance_y ** 2

        return distance_squared < (circle.radius ** 2)

    def get_collision_normal(self, box: BoundingBox, circle: Circle) -> Tuple[float, float]:
        """获取AABB-Circle碰撞的法线(从box指向circle)"""
        closest_x = max(box.x, min(circle.x, box.x + box.width))
        closest_y = max(box.y, min(circle.y, box.y + box.height))

        diff_x = circle.x - closest_x
        diff_y = circle.y - closest_y

        distance = math.sqrt(diff_x ** 2 + diff_y ** 2)

        if distance < 0.001:
            center_x = box.x + box.width / 2
            center_y = box.y + box.height / 2

            diff_x = circle.x - center_x
            diff_y = circle.y - center_y
            distance = math.sqrt(diff_x ** 2 + diff_y ** 2)

            if distance < 0.001:
                return (0, -1)

        return (diff_x / distance, diff_y / distance)

    def get_collision_depth(self, box: BoundingBox, circle: Circle) -> float:
        """获取碰撞深度(重叠距离)"""
        closest_x = max(box.x, min(circle.x, box.x + box.width))
        closest_y = max(box.y, min(circle.y, box.y + box.height))

        distance_x = circle.x - closest_x
        distance_y = circle.y - closest_y
        distance = math.sqrt(distance_x ** 2 + distance_y ** 2)

        return circle.radius - distance

    def get_collisions(self) -> List[Tuple[int, int]]:
        """获取所有碰撞对"""
        collisions = []
        obj_ids = list(self.colliders.keys())

        for i in range(len(obj_ids)):
            for j in range(i + 1, len(obj_ids)):
                if self.check_collision(obj_ids[i], obj_ids[j]):
                    collisions.append((obj_ids[i], obj_ids[j]))

        return collisions


class PhysicsObject:
    """物理对象"""

    def __init__(self, obj_id: int, position: Tuple[float, float],
                 velocity: Tuple[float, float] = (0, 0),
                 mass: float = 1.0):
        self.id = obj_id
        self.position = list(position)
        self.velocity = list(velocity)
        self.acceleration = [0.0, 0.0]
        self.mass = mass
        self.restitution = 0.8
        self.friction = 0.99

    def apply_force(self, force: Tuple[float, float]) -> None:
        """施加力"""
        fx, fy = force
        if self.mass != float('inf'):
            self.acceleration[0] += fx / self.mass
            self.acceleration[1] += fy / self.mass

    def update(self, delta_time: float) -> None:
        """更新物理状态"""
        if self.mass == float('inf'):
            return

        self.velocity[0] += self.acceleration[0] * delta_time
        self.velocity[1] += self.acceleration[1] * delta_time

        self.velocity[0] *= self.friction
        self.velocity[1] *= self.friction

        self.position[0] += self.velocity[0] * delta_time
        self.position[1] += self.velocity[1] * delta_time

        self.acceleration = [0.0, 0.0]


class PhysicsWorld:
    """物理世界"""

    def __init__(self, gravity: Tuple[float, float] = (0, 500)):
        self.gravity = gravity
        self.objects: Dict[int, PhysicsObject] = {}
        self.collision_system = CollisionSystem()

    def add_object(self, obj: PhysicsObject, collision_type: CollisionType,
                   collider_data: Dict) -> None:
        """添加物理对象"""
        self.objects[obj.id] = obj
        self.collision_system.add_collider(obj.id, collision_type, collider_data)

    def remove_object(self, obj_id: int) -> None:
        """移除物理对象"""
        if obj_id in self.objects:
            del self.objects[obj_id]
        self.collision_system.remove_collider(obj_id)

    def update(self, delta_time: float, before_collision_check: Optional[Callable] = None) -> List[Tuple[int, int]]:
        """更新物理世界"""
        for obj in self.objects.values():
            obj.apply_force(self.gravity)

        for obj in self.objects.values():
            obj.update(delta_time)

        if before_collision_check:
            before_collision_check()

        collisions = self.collision_system.get_collisions()

        for obj1_id, obj2_id in collisions:
            self.resolve_collision(obj1_id, obj2_id)

        return collisions

    def resolve_collision(self, obj1_id: int, obj2_id: int) -> None:
        """解决碰撞 - 包括速度和位置修正"""
        if obj1_id not in self.objects or obj2_id not in self.objects:
            return

        obj1 = self.objects[obj1_id]
        obj2 = self.objects[obj2_id]

        collider1 = self.collision_system.colliders[obj1_id]
        collider2 = self.collision_system.colliders[obj2_id]

        # AABB-AABB碰撞 - 两个都是静态物体,直接返回
        if (collider1['type'] == CollisionType.AABB and
            collider2['type'] == CollisionType.AABB):
            return

        # AABB-Circle碰撞
        if ((collider1['type'] == CollisionType.AABB and collider2['type'] == CollisionType.CIRCLE) or
            (collider1['type'] == CollisionType.CIRCLE and collider2['type'] == CollisionType.AABB)):

            if collider1['type'] == CollisionType.AABB:
                box = collider1['data']
                circle = collider2['data']
                box_obj = obj1
                circle_obj = obj2
            else:
                box = collider2['data']
                circle = collider1['data']
                box_obj = obj2
                circle_obj = obj1

            # 获取碰撞法线
            normal_x, normal_y = self.collision_system.get_collision_normal(box, circle)

            # 计算相对速度
            rel_vel_x = circle_obj.velocity[0] - box_obj.velocity[0]
            rel_vel_y = circle_obj.velocity[1] - box_obj.velocity[1]

            rel_vel_normal = rel_vel_x * normal_x + rel_vel_y * normal_y

            if rel_vel_normal > 0:
                return

            # 速度修正
            restitution = min(box_obj.restitution, circle_obj.restitution)

            if box_obj.mass == float('inf'):
                box_inv_mass = 0
            else:
                box_inv_mass = 1 / box_obj.mass

            if circle_obj.mass == float('inf'):
                circle_inv_mass = 0
            else:
                circle_inv_mass = 1 / circle_obj.mass

            total_inv_mass = box_inv_mass + circle_inv_mass

            if total_inv_mass < 0.001:
                return

            j = -(1 + restitution) * rel_vel_normal / total_inv_mass

            impulse_x = j * normal_x
            impulse_y = j * normal_y

            if box_obj.mass != float('inf'):
                box_obj.velocity[0] -= impulse_x * box_inv_mass
                box_obj.velocity[1] -= impulse_y * box_inv_mass

            if circle_obj.mass != float('inf'):
                circle_obj.velocity[0] += impulse_x * circle_inv_mass
                circle_obj.velocity[1] += impulse_y * circle_inv_mass

            # 位置修正 - 防止球陷入地面!
            collision_depth = self.collision_system.get_collision_depth(box, circle)
            if collision_depth > 0:
                # 分离向量(带有小的误差修正)
                separation = collision_depth + 0.01
                correction_x = normal_x * separation
                correction_y = normal_y * separation

                # 根据质量分配修正
                if circle_obj.mass != float('inf'):
                    circle_obj.position[0] += correction_x
                    circle_obj.position[1] += correction_y
                elif box_obj.mass != float('inf'):
                    box_obj.position[0] -= correction_x
                    box_obj.position[1] -= correction_y

        else:
            # Circle-Circle碰撞 - 添加无穷质量检查
            # 处理无穷质量的情况
            if obj1.mass == float('inf') and obj2.mass == float('inf'):
                return  # 两个都是静态的,不处理

            rel_vel_x = obj1.velocity[0] - obj2.velocity[0]
            rel_vel_y = obj1.velocity[1] - obj2.velocity[1]

            pos_diff_x = obj2.position[0] - obj1.position[0]
            pos_diff_y = obj2.position[1] - obj1.position[1]

            distance = math.sqrt(pos_diff_x ** 2 + pos_diff_y ** 2)
            if distance < 0.001:
                return

            normal_x = pos_diff_x / distance
            normal_y = pos_diff_y / distance

            rel_vel_normal = rel_vel_x * normal_x + rel_vel_y * normal_y

            if rel_vel_normal > 0:
                return

            restitution = min(obj1.restitution, obj2.restitution)

            # 使用倒数质量的方式,处理无穷质量
            if obj1.mass == float('inf'):
                inv_mass1 = 0
            else:
                inv_mass1 = 1 / obj1.mass

            if obj2.mass == float('inf'):
                inv_mass2 = 0
            else:
                inv_mass2 = 1 / obj2.mass

            total_inv_mass = inv_mass1 + inv_mass2

            if total_inv_mass < 0.001:
                return

            j = -(1 + restitution) * rel_vel_normal / total_inv_mass

            impulse_x = j * normal_x
            impulse_y = j * normal_y

            if obj1.mass != float('inf'):
                obj1.velocity[0] -= impulse_x * inv_mass1
                obj1.velocity[1] -= impulse_y * inv_mass1

            if obj2.mass != float('inf'):
                obj2.velocity[0] += impulse_x * inv_mass2
                obj2.velocity[1] += impulse_y * inv_mass2


class GameObject:
    """游戏对象 - 结合物理和渲染"""

    def __init__(self, obj_id: int, position: Tuple[float, float],
                 color: Tuple[int, int, int] = (255, 255, 255),
                 mass: float = 1.0, shape_type: str = "circle",
                 size: float = 20, width: Optional[float] = None,
                 height: Optional[float] = None, is_drain: bool = False):
        self.physics = PhysicsObject(obj_id, position, mass=mass)
        self.color = color
        self.shape_type = shape_type
        self.size = size
        self.width = width if width is not None else size
        self.height = height if height is not None else size
        self.collider_data = None
        self.collision_type = None
        self.is_drain = is_drain  # 是否为下水道

    def setup_collider(self, collision_type: CollisionType):
        """设置碰撞体"""
        self.collision_type = collision_type

        if collision_type == CollisionType.CIRCLE:
            self.collider_data = Circle(
                self.physics.position[0],
                self.physics.position[1],
                self.size / 2
            )
        elif collision_type == CollisionType.AABB:
            self.collider_data = BoundingBox(
                self.physics.position[0] - self.width / 2,
                self.physics.position[1] - self.height / 2,
                self.width,
                self.height
            )

    def update_collider(self):
        """更新碰撞体位置"""
        if self.collision_type == CollisionType.CIRCLE and self.collider_data:
            self.collider_data.x = self.physics.position[0]
            self.collider_data.y = self.physics.position[1]
        elif self.collision_type == CollisionType.AABB and self.collider_data:
            self.collider_data.x = self.physics.position[0] - self.width / 2
            self.collider_data.y = self.physics.position[1] - self.height / 2

    def render(self, surface: pygame.Surface):
        """渲染对象"""
        x, y = int(self.physics.position[0]), int(self.physics.position[1])

        if self.shape_type == "circle":
            pygame.draw.circle(surface, self.color, (x, y), int(self.size / 2))
            pygame.draw.circle(surface, (0, 0, 0), (x, y), int(self.size / 2), 2)
        elif self.shape_type == "rect":
            rect = pygame.Rect(x - self.width / 2, y - self.height / 2,
                             self.width, self.height)
            pygame.draw.rect(surface, self.color, rect)
            pygame.draw.rect(surface, (0, 0, 0), rect, 2)

            # 下水道特殊绘制
            if self.is_drain:
                # 绘制栅栏效果
                for i in range(0, int(self.width), 10):
                    pygame.draw.line(surface, (50, 50, 50),
                                   (x - self.width / 2 + i, y - self.height / 2),
                                   (x - self.width / 2 + i, y + self.height / 2), 2)


class Game:
    """游戏主类"""

    def __init__(self, width: int = 1200, height: int = 800):
        pygame.init()

        self.width = width
        self.height = height
        self.screen = pygame.display.set_mode((width, height))
        pygame.display.set_caption("物理碰撞演示 - Physics Collision Demo")

        self.font_large = None
        self.font_small = None

        font_names = ['Microsoft YaHei', 'SimHei', 'KaiTi', 'FangSong']

        for font_name in font_names:
            try:
                self.font_large = pygame.font.SysFont(font_name, 32)
                self.font_small = pygame.font.SysFont(font_name, 20)
                print(f"成功加载字体: {font_name}")
                break
            except Exception as e:
                print(f"无法加载字体 {font_name}: {e}")
                continue

        if self.font_large is None:
            self.font_large = pygame.font.Font(None, 32)
            self.font_small = pygame.font.Font(None, 20)
            print("使用默认字体")

        self.clock = pygame.time.Clock()
        self.running = True
        self.fps = 60
        self.frame_count = 0

        self.physics_world = PhysicsWorld(gravity=(0, 500))

        self.game_objects: Dict[int, GameObject] = {}
        self.object_id_counter = 0

        self.collision_count = 0
        self.total_objects = 0

        # 下水道ID
        self.drain_id = None

        # 冲水效果
        self.is_flushing = False
        self.flush_timer = 0
        self.flush_duration = 0.5

        self.setup_scene()

    def setup_scene(self):
        """设置游戏场景"""
        # 创建地面
        ground_id = self.object_id_counter
        self.object_id_counter += 1

        ground = GameObject(ground_id, (self.width / 2, self.height - 15),
                           color=(100, 100, 100), mass=float('inf'),
                           shape_type="rect", size=0,
                           width=self.width, height=30)
        ground.setup_collider(CollisionType.AABB)
        ground.physics.velocity = [0, 0]

        self.game_objects[ground_id] = ground
        self.physics_world.add_object(
            ground.physics,
            ground.collision_type,
            ground.collider_data
        )

        # 创建左墙
        left_wall_id = self.object_id_counter
        self.object_id_counter += 1

        left_wall = GameObject(left_wall_id, (15, self.height / 2),
                              color=(80, 80, 80), mass=float('inf'),
                              shape_type="rect", size=0,
                              width=30, height=self.height)
        left_wall.setup_collider(CollisionType.AABB)
        left_wall.physics.velocity = [0, 0]

        self.game_objects[left_wall_id] = left_wall
        self.physics_world.add_object(
            left_wall.physics,
            left_wall.collision_type,
            left_wall.collider_data
        )

        # 创建右墙
        right_wall_id = self.object_id_counter
        self.object_id_counter += 1

        right_wall = GameObject(right_wall_id, (self.width - 15, self.height / 2),
                               color=(80, 80, 80), mass=float('inf'),
                               shape_type="rect", size=0,
                               width=30, height=self.height)
        right_wall.setup_collider(CollisionType.AABB)
        right_wall.physics.velocity = [0, 0]

        self.game_objects[right_wall_id] = right_wall
        self.physics_world.add_object(
            right_wall.physics,
            right_wall.collision_type,
            right_wall.collider_data
        )

        # 创建下水道(在地面右侧)
        drain_id = self.object_id_counter
        self.object_id_counter += 1
        self.drain_id = drain_id

        drain = GameObject(drain_id, (self.width - 80, self.height - 15),
                          color=(60, 60, 60), mass=float('inf'),
                          shape_type="rect", size=0,
                          width=130, height=30, is_drain=True)
        drain.setup_collider(CollisionType.AABB)
        drain.physics.velocity = [0, 0]

        self.game_objects[drain_id] = drain
        self.physics_world.add_object(
            drain.physics,
            drain.collision_type,
            drain.collider_data
        )

        # 创建初始球体
        for i in range(5):
            obj_id = self.object_id_counter
            self.object_id_counter += 1

            x = 150 + i * 150
            y = 50 + i * 60

            obj = GameObject(obj_id, (x, y),
                           color=(200 + i * 10, 100 + i * 20, 150 - i * 15),
                           mass=1.0, shape_type="circle", size=30)
            obj.setup_collider(CollisionType.CIRCLE)
            obj.physics.velocity = [50 + i * 30, 0]

            self.game_objects[obj_id] = obj
            self.physics_world.add_object(
                obj.physics,
                obj.collision_type,
                obj.collider_data
            )

        self.total_objects = len(self.game_objects)

    def handle_events(self):
        """处理事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    self.running = False
                elif event.key == pygame.K_SPACE:
                    self.add_random_ball()
                elif event.key == pygame.K_c:
                    self.clear_balls()
                elif event.key == pygame.K_r:
                    self.reset_scene()
                elif event.key == pygame.K_w:
                    self.flush_water()
            elif event.type == pygame.MOUSEBUTTONDOWN:
                x, y = pygame.mouse.get_pos()
                self.add_ball_at_position(x, y)

    def add_random_ball(self):
        """添加随机球"""
        obj_id = self.object_id_counter
        self.object_id_counter += 1

        x = random.randint(100, self.width - 100)
        y = random.randint(50, 200)

        color = (random.randint(100, 255),
                random.randint(100, 255),
                random.randint(100, 255))

        obj = GameObject(obj_id, (x, y),
                       color=color, mass=1.0,
                       shape_type="circle", size=30)
        obj.setup_collider(CollisionType.CIRCLE)
        obj.physics.velocity = [random.randint(-100, 100), 0]

        self.game_objects[obj_id] = obj
        self.physics_world.add_object(
            obj.physics,
            obj.collision_type,
            obj.collider_data
        )
        self.total_objects += 1

    def add_ball_at_position(self, x: int, y: int):
        """在指定位置添加球"""
        obj_id = self.object_id_counter
        self.object_id_counter += 1

        color = (255, 200, 50)

        obj = GameObject(obj_id, (x, y),
                       color=color, mass=1.0,
                       shape_type="circle", size=25)
        obj.setup_collider(CollisionType.CIRCLE)

        self.game_objects[obj_id] = obj
        self.physics_world.add_object(
            obj.physics,
            obj.collision_type,
            obj.collider_data
        )
        self.total_objects += 1

    def clear_balls(self):
        """清除所有球"""
        ids_to_remove = [obj_id for obj_id in self.game_objects
                        if self.game_objects[obj_id].shape_type == "circle"]

        for obj_id in ids_to_remove:
            self.physics_world.remove_object(obj_id)
            del self.game_objects[obj_id]

        self.total_objects = len(self.game_objects)

    def reset_scene(self):
        """重置场景"""
        self.running = False
        self.__init__(self.width, self.height)
        self.running = True

    def flush_water(self):
        """冲水 - 从左边产生冲击波"""
        self.is_flushing = True
        self.flush_timer = 0

        # 对所有球施加向右的冲击力
        for obj_id, game_obj in self.game_objects.items():
            if game_obj.shape_type == "circle":
                # 根据球的高度,计算冲击力(下面的球冲击力更大)
                force_magnitude = 800 * (1 + (self.height - game_obj.physics.position[1]) / self.height)
                game_obj.physics.velocity[0] = force_magnitude

    def update(self, delta_time: float):
        """更新游戏状态"""
        def update_all_colliders():
            for game_obj in self.game_objects.values():
                game_obj.update_collider()

        collisions = self.physics_world.update(delta_time, update_all_colliders)
        self.collision_count = len(collisions)

        # 更新冲水效果
        if self.is_flushing:
            self.flush_timer += delta_time
            if self.flush_timer >= self.flush_duration:
                self.is_flushing = False

        # 检查球是否进入下水道
        drain = self.game_objects[self.drain_id]
        ids_to_remove = []

        for obj_id, game_obj in self.game_objects.items():
            if game_obj.shape_type == "circle":
                # 检查球是否在下水道内
                if drain.collider_data.contains_point(game_obj.physics.position):
                    ids_to_remove.append(obj_id)
                # 检查球是否超出屏幕
                elif (game_obj.physics.position[1] > self.height + 100 or
                      game_obj.physics.position[0] < -100 or
                      game_obj.physics.position[0] > self.width + 100):
                    ids_to_remove.append(obj_id)

        for obj_id in ids_to_remove:
            self.physics_world.remove_object(obj_id)
            del self.game_objects[obj_id]
            self.total_objects -= 1

    def render(self):
        """渲染游戏"""
        self.screen.fill((240, 240, 240))

        for game_obj in self.game_objects.values():
            game_obj.render(self.screen)

        # 绘制冲水效果
        if self.is_flushing:
            alpha = int(255 * (1 - self.flush_timer / self.flush_duration))
            flush_surface = pygame.Surface((150, self.height))
            flush_surface.set_alpha(alpha)
            flush_surface.fill((150, 200, 255))
            self.screen.blit(flush_surface, (0, 0))

        self.draw_ui()

        pygame.display.flip()

    def draw_ui(self):
        """绘制UI界面"""
        fps = self.clock.get_fps()

        help_y = 15
        help_title = self.font_small.render("操作说明:", True, (80, 80, 80))
        self.screen.blit(help_title, (self.width - 250, help_y))

        help_texts = [
            "Space - 添加随机球",
            "Click - 在点击位置添加球",
            "W - 冲水(从左边)",
            "C - 清除所有球",
            "R - 重置场景",
            "ESC - 退出游戏",
        ]

        help_y += 28
        for text_content in help_texts:
            text_surface = self.font_small.render(text_content, True, (120, 120, 120))
            self.screen.blit(text_surface, (self.width - 250, help_y))
            help_y += 24

        title_text = self.font_large.render("物理碰撞演示", True, (30, 30, 30))
        self.screen.blit(title_text, (30, 15))

        stats_texts = [
            f"FPS: {fps:.1f}",
            f"物体数量: {self.total_objects}",
            f"碰撞数: {self.collision_count}",
            f"物理帧: {self.frame_count}",
        ]

        y_offset = 65
        for text_content in stats_texts:
            text_surface = self.font_small.render(text_content, True, (80, 80, 80))
            self.screen.blit(text_surface, (30, y_offset))
            y_offset += 28

        # 绘制下水道标签
        drain_label = self.font_small.render("下水道", True, (100, 50, 50))
        self.screen.blit(drain_label, (self.width - 140, self.height - 40))

    def run(self):
        """运行游戏"""
        while self.running:
            delta_time = self.clock.tick(self.fps) / 1000.0
            self.frame_count += 1

            self.handle_events()
            self.update(delta_time)
            self.render()

        pygame.quit()
        print("游戏已退出")


if __name__ == "__main__":
    game = Game(width=1200, height=700)
    game.run()

这个物理引擎和碰撞检测系统展示了Python在处理复杂游戏物理方面的能力。它包括了多种碰撞检测算法、物理模拟和碰撞响应等功能。

4.2 粒子系统与特效实现

粒子系统是现代游戏中常用的特效技术,它通过模拟大量小粒子的运动来创建火焰、烟雾、爆炸等视觉效果。Python虽然不是性能最强的语言,但通过优化算法和利用现代硬件加速,也能实现不错的粒子效果。

粒子系统实现

复制代码
import pygame
import random
import math
from typing import List, Tuple, Optional, Dict
from dataclasses import dataclass, field
from enum import Enum


class ParticleType(Enum):
    """粒子类型"""
    FIRE = "fire"
    SMOKE = "smoke"
    EXPLOSION = "explosion"
    MAGIC = "magic"
    RAIN = "rain"
    SNOW = "snow"


@dataclass
class Particle:
    """粒子类"""
    x: float
    y: float
    vx: float
    vy: float
    life: float
    max_life: float
    size: float
    color: Tuple[int, int, int]
    alpha: int = 255
    gravity: float = 0.0
    fade_rate: float = 2.0
    shrink_rate: float = 0.0

    def update(self, delta_time: float) -> bool:
        """更新粒子状态"""
        # 应用重力
        self.vy += self.gravity * delta_time

        # 更新位置
        self.x += self.vx * delta_time
        self.y += self.vy * delta_time

        # 更新生命周期
        self.life -= delta_time

        # 更新透明度
        self.alpha = max(0, int((self.life / self.max_life) * 255))

        # 更新大小
        self.size = max(0, self.size - self.shrink_rate * delta_time)

        # 返回是否仍然存活
        return self.life > 0 and self.size > 0

    def draw(self, surface: pygame.Surface, offset_x: float = 0, offset_y: float = 0) -> None:
        """绘制粒子"""
        if self.size <= 0 or self.alpha <= 0:
            return

        # 创建粒子表面
        particle_surface = pygame.Surface((int(self.size * 2), int(self.size * 2)), pygame.SRCALPHA)

        # 绘制圆形粒子
        center = (int(self.size), int(self.size))
        color_with_alpha = (*self.color, self.alpha)
        pygame.draw.circle(particle_surface, color_with_alpha, center, int(self.size))

        # 添加高光效果
        highlight_pos = (int(self.size * 0.7), int(self.size * 0.7))
        highlight_size = max(1, int(self.size * 0.3))
        highlight_color = (255, 255, 255, min(255, self.alpha + 50))
        pygame.draw.circle(particle_surface, highlight_color, highlight_pos, highlight_size)

        # 绘制到主表面
        draw_pos = (int(self.x - self.size + offset_x), int(self.y - self.size + offset_y))
        surface.blit(particle_surface, draw_pos)


class Emitter:
    """粒子发射器"""

    def __init__(self, x: float, y: float, particle_type: ParticleType):
        self.x = x
        self.y = y
        self.particle_type = particle_type
        self.particles: List[Particle] = []
        self.active = True
        self.emission_rate = 10.0  # 每秒发射粒子数
        self.emission_timer = 0.0

        # 发射参数
        self.spread_angle = math.pi / 4  # 发射角度范围
        self.speed_min = 50.0
        self.speed_max = 150.0
        self.life_min = 1.0
        self.life_max = 2.0
        self.size_min = 2.0
        self.size_max = 5.0

        # 粒子颜色范围
        self.color_start = (255, 255, 255)
        self.color_end = (255, 255, 255)

    def set_emission_direction(self, angle: float, spread: float = None) -> None:
        """设置发射方向"""
        self.emission_angle = angle
        if spread is not None:
            self.spread_angle = spread

    def set_emission_speed(self, min_speed: float, max_speed: float) -> None:
        """设置发射速度范围"""
        self.speed_min = min_speed
        self.speed_max = max_speed

    def set_particle_life(self, min_life: float, max_life: float) -> None:
        """设置粒子生命周期范围"""
        self.life_min = min_life
        self.life_max = max_life

    def set_particle_size(self, min_size: float, max_size: float) -> None:
        """设置粒子大小范围"""
        self.size_min = min_size
        self.size_max = max_size

    def set_color_gradient(self, start_color: Tuple[int, int, int],
                           end_color: Tuple[int, int, int]) -> None:
        """设置颜色渐变"""
        self.color_start = start_color
        self.color_end = end_color

    def emit_particle(self) -> None:
        """发射一个粒子"""
        # 计算随机角度
        angle_offset = random.uniform(-self.spread_angle / 2, self.spread_angle / 2)
        angle = getattr(self, 'emission_angle', -math.pi / 2) + angle_offset

        # 计算随机速度
        speed = random.uniform(self.speed_min, self.speed_max)
        vx = math.cos(angle) * speed
        vy = math.sin(angle) * speed

        # 计算随机生命周期
        life = random.uniform(self.life_min, self.life_max)

        # 计算随机大小
        size = random.uniform(self.size_min, self.size_max)

        # 计算颜色
        color = self.color_start  # 简化处理,实际可以做渐变

        # 创建粒子
        particle = Particle(
            x=self.x,
            y=self.y,
            vx=vx,
            vy=vy,
            life=life,
            max_life=life,
            size=size,
            color=color
        )

        # 根据粒子类型设置特定属性
        if self.particle_type == ParticleType.FIRE:
            particle.gravity = -50.0  # 火焰向上飘
            particle.fade_rate = 3.0
            particle.shrink_rate = 1.0
            color_choice = random.choice([
                (255, 200, 0),  # 黄色
                (255, 100, 0),  # 橙色
                (255, 50, 0),  # 红色
                (200, 50, 0)  # 深红色
            ])
            particle.color = color_choice

        elif self.particle_type == ParticleType.SMOKE:
            particle.gravity = -20.0
            particle.fade_rate = 1.5
            particle.vx *= 0.5  # 烟雾横向扩散慢
            particle.vy *= 0.3
            gray_value = random.randint(100, 200)
            particle.color = (gray_value, gray_value, gray_value)

        elif self.particle_type == ParticleType.EXPLOSION:
            particle.gravity = 0.0
            particle.fade_rate = 4.0
            particle.shrink_rate = 2.0
            color_choice = random.choice([
                (255, 255, 200),  # 白黄色
                (255, 200, 50),  # 金色
                (255, 100, 0),  # 橙色
                (255, 50, 50)  # 红色
            ])
            particle.color = color_choice

        elif self.particle_type == ParticleType.MAGIC:
            particle.gravity = 0.0
            particle.fade_rate = 2.0
            particle.shrink_rate = 0.5
            color_choice = random.choice([
                (150, 100, 255),  # 紫色
                (100, 150, 255),  # 蓝色
                (200, 100, 255),  # 洋红色
                (150, 255, 200)  # 青绿色
            ])
            particle.color = color_choice

        self.particles.append(particle)

    def update(self, delta_time: float) -> None:
        """更新发射器状态"""
        if not self.active:
            return

        # 发射新粒子
        self.emission_timer += delta_time
        emission_interval = 1.0 / self.emission_rate

        while self.emission_timer >= emission_interval:
            self.emit_particle()
            self.emission_timer -= emission_interval

        # 更新现有粒子
        self.particles = [p for p in self.particles if p.update(delta_time)]

    def draw(self, surface: pygame.Surface, offset_x: float = 0, offset_y: float = 0) -> None:
        """绘制所有粒子"""
        for particle in self.particles:
            particle.draw(surface, offset_x, offset_y)

    def move_to(self, x: float, y: float) -> None:
        """移动发射器位置"""
        self.x = x
        self.y = y

    def stop(self) -> None:
        """停止发射新粒子"""
        self.active = False

    def start(self) -> None:
        """开始发射粒子"""
        self.active = True


class ParticleSystem:
    """粒子系统管理器"""

    def __init__(self):
        self.emitters: Dict[int, Emitter] = {}
        self.next_id = 0

    def create_emitter(self, x: float, y: float,
                       particle_type: ParticleType) -> int:
        """创建粒子发射器"""
        emitter_id = self.next_id
        self.next_id += 1

        emitter = Emitter(x, y, particle_type)

        # 根据类型设置默认参数
        if particle_type == ParticleType.FIRE:
            emitter.emission_rate = 30.0
            emitter.set_emission_direction(-math.pi / 2, math.pi / 3)
            emitter.set_emission_speed(30.0, 80.0)
            emitter.set_particle_life(0.5, 1.5)
            emitter.set_particle_size(3.0, 8.0)

        elif particle_type == ParticleType.SMOKE:
            emitter.emission_rate = 10.0
            emitter.set_emission_direction(-math.pi / 2, math.pi / 6)
            emitter.set_emission_speed(10.0, 30.0)
            emitter.set_particle_life(2.0, 4.0)
            emitter.set_particle_size(5.0, 15.0)

        elif particle_type == ParticleType.EXPLOSION:
            emitter.emission_rate = 100.0
            emitter.set_emission_direction(0, math.pi * 2)
            emitter.set_emission_speed(100.0, 300.0)
            emitter.set_particle_life(0.3, 0.8)
            emitter.set_particle_size(2.0, 6.0)

        elif particle_type == ParticleType.MAGIC:
            emitter.emission_rate = 20.0
            emitter.set_emission_direction(-math.pi / 2, math.pi / 4)
            emitter.set_emission_speed(50.0, 100.0)
            emitter.set_particle_life(1.0, 2.0)
            emitter.set_particle_size(2.0, 5.0)

        self.emitters[emitter_id] = emitter
        return emitter_id

    def remove_emitter(self, emitter_id: int) -> None:
        """移除粒子发射器"""
        if emitter_id in self.emitters:
            del self.emitters[emitter_id]

    def update(self, delta_time: float) -> None:
        """更新所有发射器"""
        # 更新所有发射器
        for emitter in self.emitters.values():
            emitter.update(delta_time)

        # 移除没有活动粒子且不活跃的发射器
        self.emitters = {
            id: emitter for id, emitter in self.emitters.items()
            if emitter.active or emitter.particles
        }

    def draw(self, surface: pygame.Surface, offset_x: float = 0, offset_y: float = 0) -> None:
        """绘制所有粒子"""
        for emitter in self.emitters.values():
            emitter.draw(surface, offset_x, offset_y)

    def create_explosion(self, x: float, y: float) -> int:
        """创建爆炸效果"""
        emitter_id = self.create_emitter(x, y, ParticleType.EXPLOSION)
        emitter = self.emitters[emitter_id]
        emitter.stop()  # 只发射一帧
        return emitter_id

    def create_fire(self, x: float, y: float) -> int:
        """创建火焰效果"""
        return self.create_emitter(x, y, ParticleType.FIRE)

    def create_smoke(self, x: float, y: float) -> int:
        """创建烟雾效果"""
        return self.create_emitter(x, y, ParticleType.SMOKE)

    def create_magic_effect(self, x: float, y: float) -> int:
        """创建魔法效果"""
        return self.create_emitter(x, y, ParticleType.MAGIC)


# ============ 运行实测代码 ============

def run_particle_system_demo():
    """运行粒子系统演示"""
    # 初始化pygame
    pygame.init()

    # 设置屏幕参数
    SCREEN_WIDTH = 1200
    SCREEN_HEIGHT = 800
    FPS = 60

    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption("粒子系统演示 - 按1/2/3/4创建效果,鼠标点击创建爆炸")
    clock = pygame.time.Clock()

    # 创建粒子系统
    particle_system = ParticleSystem()

    # 演示发射器ID
    demo_emitters = {
        'fire': None,
        'smoke': None,
        'magic': None
    }

    # 字体用于显示提示信息 - 使用支持中文的系统字体
    try:
        font = pygame.font.SysFont('simhei', 32)  # 黑体
        small_font = pygame.font.SysFont('simhei', 24)
    except:
        # 备选方案:尝试其他中文字体
        try:
            font = pygame.font.SysFont('microsoftyaheibold', 32)  # 微软雅黑
            small_font = pygame.font.SysFont('microsoftyaheibold', 24)
        except:
            # 最后备选:使用默认字体(将显示为方框)
            font = pygame.font.Font(None, 32)
            small_font = pygame.font.Font(None, 24)

    running = True
    show_info = True

    while running:
        delta_time = clock.tick(FPS) / 1000.0  # 转换为秒

        # 处理事件
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

            # 键盘事件
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_1:
                    # 创建火焰效果
                    if demo_emitters['fire']:
                        particle_system.remove_emitter(demo_emitters['fire'])
                    demo_emitters['fire'] = particle_system.create_fire(SCREEN_WIDTH // 4, SCREEN_HEIGHT - 100)

                elif event.key == pygame.K_2:
                    # 创建烟雾效果
                    if demo_emitters['smoke']:
                        particle_system.remove_emitter(demo_emitters['smoke'])
                    demo_emitters['smoke'] = particle_system.create_smoke(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 100)

                elif event.key == pygame.K_3:
                    # 创建魔法效果
                    if demo_emitters['magic']:
                        particle_system.remove_emitter(demo_emitters['magic'])
                    demo_emitters['magic'] = particle_system.create_magic_effect(3 * SCREEN_WIDTH // 4,
                                                                                 SCREEN_HEIGHT - 100)

                elif event.key == pygame.K_4:
                    # 停止所有效果
                    for emitter_id in demo_emitters.values():
                        if emitter_id:
                            particle_system.emitters[emitter_id].stop()

                elif event.key == pygame.K_i:
                    # 切换显示信息
                    show_info = not show_info

                elif event.key == pygame.K_ESCAPE:
                    running = False

            # 鼠标点击事件 - 创建爆炸效果
            elif event.type == pygame.MOUSEBUTTONDOWN:
                mouse_x, mouse_y = pygame.mouse.get_pos()
                particle_system.create_explosion(mouse_x, mouse_y)

        # 更新粒子系统
        particle_system.update(delta_time)

        # 绘制
        screen.fill((30, 30, 40))  # 深蓝灰色背景

        # 绘制粒子
        particle_system.draw(screen)

        # 绘制UI信息
        if show_info:
            # 标题
            title = font.render("粒子系统演示", True, (255, 255, 255))
            screen.blit(title, (20, 20))

            # 操作说明
            info_texts = [
                "按键说明:",
                "1 - 创建火焰效果 (位置: 左下)",
                "2 - 创建烟雾效果 (位置: 中下)",
                "3 - 创建魔法效果 (位置: 右下)",
                "4 - 停止所有发射",
                "鼠标左键 - 点击屏幕创建爆炸效果",
                "I - 显示/隐藏信息",
                "ESC - 退出"
            ]

            y_offset = 80
            for text in info_texts:
                surface = small_font.render(text, True, (200, 200, 220))
                screen.blit(surface, (20, y_offset))
                y_offset += 28

            # 显示粒子统计信息
            total_particles = sum(len(e.particles) for e in particle_system.emitters.values())
            total_emitters = len(particle_system.emitters)
            stats_text = f"发射器数: {total_emitters} | 粒子总数: {total_particles} | FPS: {clock.get_fps():.0f}"
            stats_surface = small_font.render(stats_text, True, (100, 255, 100))
            screen.blit(stats_surface, (20, SCREEN_HEIGHT - 40))

        # 刷新显示
        pygame.display.flip()

    pygame.quit()


if __name__ == "__main__":
    run_particle_system_demo()

这个粒子系统展示了Python在游戏特效开发方面的能力。它包括了多种粒子类型、发射器管理和性能优化等功能。

4.3 AI行为树与敌人设计

动作游戏中敌人的AI是游戏体验的重要组成部分。行为树是一种常用的AI设计模式,它通过树状结构来组织敌人的行为逻辑。

AI行为树实现

复制代码
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any, Callable
from dataclasses import dataclass, field
from enum import Enum
import random
import math
import sys
from datetime import datetime


class NodeStatus(Enum):
    """节点状态"""
    SUCCESS = "success"
    FAILURE = "failure"
    RUNNING = "running"


class BehaviorNode(ABC):
    """行为树节点基类"""

    def __init__(self, name: str = ""):
        self.name = name

    @abstractmethod
    def execute(self, agent: 'GameAgent', delta_time: float) -> NodeStatus:
        """执行节点行为"""
        pass

    def reset(self) -> None:
        """重置节点状态"""
        pass


class ActionNode(BehaviorNode):
    """动作节点"""

    def __init__(self, name: str, action: Callable[['GameAgent', float], NodeStatus]):
        super().__init__(name)
        self.action = action

    def execute(self, agent: 'GameAgent', delta_time: float) -> NodeStatus:
        return self.action(agent, delta_time)


class ConditionNode(BehaviorNode):
    """条件节点"""

    def __init__(self, name: str, condition: Callable[['GameAgent'], bool]):
        super().__init__(name)
        self.condition = condition

    def execute(self, agent: 'GameAgent', delta_time: float) -> NodeStatus:
        return NodeStatus.SUCCESS if self.condition(agent) else NodeStatus.FAILURE


class CompositeNode(BehaviorNode):
    """复合节点基类"""

    def __init__(self, name: str, children: List[BehaviorNode] = None):
        super().__init__(name)
        self.children = children if children is not None else []
        self.current_child_index = 0

    def add_child(self, child: BehaviorNode) -> None:
        """添加子节点"""
        self.children.append(child)

    def reset(self) -> None:
        """重置节点状态"""
        self.current_child_index = 0
        for child in self.children:
            child.reset()


class SequenceNode(CompositeNode):
    """序列节点 - 按顺序执行子节点,直到失败"""

    def execute(self, agent: 'GameAgent', delta_time: float) -> NodeStatus:
        while self.current_child_index < len(self.children):
            status = self.children[self.current_child_index].execute(agent, delta_time)

            if status == NodeStatus.RUNNING:
                return NodeStatus.RUNNING
            elif status == NodeStatus.FAILURE:
                self.reset()
                return NodeStatus.FAILURE

            self.current_child_index += 1

        self.reset()
        return NodeStatus.SUCCESS


class SelectorNode(CompositeNode):
    """选择节点 - 按顺序尝试子节点,直到成功"""

    def execute(self, agent: 'GameAgent', delta_time: float) -> NodeStatus:
        while self.current_child_index < len(self.children):
            status = self.children[self.current_child_index].execute(agent, delta_time)

            if status == NodeStatus.RUNNING:
                return NodeStatus.RUNNING
            elif status == NodeStatus.SUCCESS:
                self.reset()
                return NodeStatus.SUCCESS

            self.current_child_index += 1

        self.reset()
        return NodeStatus.FAILURE


class ParallelNode(CompositeNode):
    """并行节点 - 同时执行所有子节点"""

    def __init__(self, name: str, children: List[BehaviorNode] = None,
                 success_threshold: int = 1):
        super().__init__(name, children)
        self.success_threshold = success_threshold

    def execute(self, agent: 'GameAgent', delta_time: float) -> NodeStatus:
        success_count = 0
        failure_count = 0
        running_count = 0

        for child in self.children:
            status = child.execute(agent, delta_time)

            if status == NodeStatus.SUCCESS:
                success_count += 1
            elif status == NodeStatus.FAILURE:
                failure_count += 1
            else:
                running_count += 1

        # 如果成功数量达到阈值,返回成功
        if success_count >= self.success_threshold:
            self.reset()
            return NodeStatus.SUCCESS

        # 如果剩余子节点无法达到成功阈值,返回失败
        if len(self.children) - failure_count < self.success_threshold:
            self.reset()
            return NodeStatus.FAILURE

        # 如果还有子节点在运行,返回运行状态
        if running_count > 0:
            return NodeStatus.RUNNING

        self.reset()
        return NodeStatus.FAILURE


class DecoratorNode(BehaviorNode):
    """装饰器节点基类"""

    def __init__(self, name: str, child: BehaviorNode):
        super().__init__(name)
        self.child = child

    def reset(self) -> None:
        """重置节点状态"""
        if self.child:
            self.child.reset()


class InverterNode(DecoratorNode):
    """反转节点 - 反转子节点的结果"""

    def execute(self, agent: 'GameAgent', delta_time: float) -> NodeStatus:
        status = self.child.execute(agent, delta_time)

        if status == NodeStatus.SUCCESS:
            return NodeStatus.FAILURE
        elif status == NodeStatus.FAILURE:
            return NodeStatus.SUCCESS
        else:
            return NodeStatus.RUNNING


class RepeatNode(DecoratorNode):
    """重复节点 - 重复执行子节点"""

    def __init__(self, name: str, child: BehaviorNode, repeat_times: int = -1):
        super().__init__(name, child)
        self.repeat_times = repeat_times  # -1 表示无限重复
        self.current_count = 0

    def execute(self, agent: 'GameAgent', delta_time: float) -> NodeStatus:
        status = self.child.execute(agent, delta_time)

        if status == NodeStatus.RUNNING:
            return NodeStatus.RUNNING

        self.current_count += 1

        if self.repeat_times > 0 and self.current_count >= self.repeat_times:
            self.reset()
            return NodeStatus.SUCCESS

        self.child.reset()
        return NodeStatus.RUNNING

    def reset(self) -> None:
        """重置节点状态"""
        self.current_count = 0
        super().reset()


class CooldownNode(DecoratorNode):
    """冷却节点 - 子节点有冷却时间"""

    def __init__(self, name: str, child: BehaviorNode, cooldown_time: float):
        super().__init__(name, child)
        self.cooldown_time = cooldown_time
        self.current_cooldown = 0.0

    def execute(self, agent: 'GameAgent', delta_time: float) -> NodeStatus:
        if self.current_cooldown > 0:
            self.current_cooldown -= delta_time
            return NodeStatus.FAILURE

        status = self.child.execute(agent, delta_time)

        if status == NodeStatus.SUCCESS:
            self.current_cooldown = self.cooldown_time

        return status

    def reset(self) -> None:
        """重置节点状态"""
        self.current_cooldown = 0.0
        super().reset()


@dataclass
class AgentState:
    """代理状态"""
    position: tuple[float, float]
    velocity: tuple[float, float]
    health: float
    max_health: float
    attack_range: float
    sight_range: float
    attack_damage: float
    attack_cooldown: float
    current_attack_cooldown: float = 0.0
    target: Optional['GameAgent'] = None
    patrol_points: List[tuple[float, float]] = None
    current_patrol_index: int = 0
    state: str = "idle"  # idle, patrol, chase, attack, flee


class GameAgent:
    """游戏代理"""

    def __init__(self, agent_id: int, state: AgentState, behavior_tree: BehaviorNode):
        self.id = agent_id
        self.state = state
        self.behavior_tree = behavior_tree
        self.last_state = "idle"

    def update(self, delta_time: float, all_agents: Dict[int, 'GameAgent']) -> None:
        """更新代理状态"""
        # 更新攻击冷却
        if self.state.current_attack_cooldown > 0:
            self.state.current_attack_cooldown -= delta_time

        # 寻找目标
        self.find_target(all_agents)

        # 执行行为树
        status = self.behavior_tree.execute(self, delta_time)

        # 更新位置
        self.state.position = (
            self.state.position[0] + self.state.velocity[0] * delta_time,
            self.state.position[1] + self.state.velocity[1] * delta_time
        )

    def find_target(self, all_agents: Dict[int, 'GameAgent']) -> None:
        """寻找目标"""
        closest_target = None
        closest_distance = self.state.sight_range

        for agent_id, agent in all_agents.items():
            if agent_id == self.id:
                continue

            # 计算距离
            dx = agent.state.position[0] - self.state.position[0]
            dy = agent.state.position[1] - self.state.position[1]
            distance = math.sqrt(dx * dx + dy * dy)

            if distance < closest_distance:
                closest_distance = distance
                closest_target = agent

        self.state.target = closest_target

    def move_towards(self, target_pos: tuple[float, float], speed: float) -> None:
        """向目标位置移动"""
        dx = target_pos[0] - self.state.position[0]
        dy = target_pos[1] - self.state.position[1]
        distance = math.sqrt(dx * dx + dy * dy)

        if distance > 0:
            self.state.velocity = (
                (dx / distance) * speed,
                (dy / distance) * speed
            )
        else:
            self.state.velocity = (0, 0)

    def move_away(self, target_pos: tuple[float, float], speed: float) -> None:
        """远离目标位置"""
        dx = target_pos[0] - self.state.position[0]
        dy = target_pos[1] - self.state.position[1]
        distance = math.sqrt(dx * dx + dy * dy)

        if distance > 0:
            self.state.velocity = (
                -(dx / distance) * speed,
                -(dy / distance) * speed
            )
        else:
            self.state.velocity = (0, 0)

    def attack(self, target: 'GameAgent') -> bool:
        """攻击目标"""
        if self.state.current_attack_cooldown > 0:
            return False

        if target:
            target.state.health -= self.state.attack_damage
            self.state.current_attack_cooldown = self.state.attack_cooldown
            return True

        return False

    def is_dead(self) -> bool:
        """检查是否死亡"""
        return self.state.health <= 0

    def get_distance_to(self, other: 'GameAgent') -> float:
        """获取到另一个代理的距离"""
        dx = other.state.position[0] - self.state.position[0]
        dy = other.state.position[1] - self.state.position[1]
        return math.sqrt(dx * dx + dy * dy)


class BehaviorTreeBuilder:
    """行为树构建器"""

    @staticmethod
    def create_enemy_behavior_tree() -> BehaviorNode:
        """创建敌人行为树"""
        # 创建条件节点
        has_target = ConditionNode(
            "HasTarget",
            lambda agent: agent.state.target is not None
        )

        in_attack_range = ConditionNode(
            "InAttackRange",
            lambda agent: (agent.state.target and
                           agent.get_distance_to(agent.state.target) <= agent.state.attack_range)
        )

        low_health = ConditionNode(
            "LowHealth",
            lambda agent: agent.state.health < agent.state.max_health * 0.3
        )

        can_attack = ConditionNode(
            "CanAttack",
            lambda agent: agent.state.current_attack_cooldown <= 0
        )

        # 创建动作节点
        chase_action = ActionNode(
            "ChaseTarget",
            lambda agent, dt: BehaviorTreeBuilder.chase_target(agent, dt)
        )

        attack_action = ActionNode(
            "AttackTarget",
            lambda agent, dt: BehaviorTreeBuilder.attack_target(agent, dt)
        )

        flee_action = ActionNode(
            "FleeFromTarget",
            lambda agent, dt: BehaviorTreeBuilder.flee_from_target(agent, dt)
        )

        patrol_action = ActionNode(
            "Patrol",
            lambda agent, dt: BehaviorTreeBuilder.patrol(agent, dt)
        )

        idle_action = ActionNode(
            "Idle",
            lambda agent, dt: BehaviorTreeBuilder.idle(agent, dt)
        )

        # 创建复合节点
        attack_sequence = SequenceNode("AttackSequence", [
            can_attack,
            CooldownNode("AttackCooldown", attack_action, 1.0)
        ])

        combat_selector = SelectorNode("CombatSelector", [
            SequenceNode("AttackSequence", [in_attack_range, attack_sequence]),
            chase_action
        ])

        survival_selector = SelectorNode("SurvivalSelector", [
            SequenceNode("FleeSequence", [low_health, flee_action]),
            SequenceNode("CombatSequence", [has_target, combat_selector]),
            patrol_action
        ])

        main_selector = SelectorNode("MainSelector", [
            survival_selector,
            idle_action
        ])

        return main_selector

    @staticmethod
    def chase_target(agent: GameAgent, delta_time: float) -> NodeStatus:
        """追逐目标"""
        if agent.state.target:
            agent.move_towards(agent.state.target.state.position, 100.0)
            agent.state.state = "chase"
            return NodeStatus.SUCCESS
        return NodeStatus.FAILURE

    @staticmethod
    def attack_target(agent: GameAgent, delta_time: float) -> NodeStatus:
        """攻击目标"""
        if agent.state.target and agent.attack(agent.state.target):
            agent.state.state = "attack"
            return NodeStatus.SUCCESS
        return NodeStatus.FAILURE

    @staticmethod
    def flee_from_target(agent: GameAgent, delta_time: float) -> NodeStatus:
        """逃离目标"""
        if agent.state.target:
            agent.move_away(agent.state.target.state.position, 150.0)
            agent.state.state = "flee"
            return NodeStatus.SUCCESS
        return NodeStatus.FAILURE

    @staticmethod
    def patrol(agent: GameAgent, delta_time: float) -> NodeStatus:
        """巡逻"""
        if agent.state.patrol_points and len(agent.state.patrol_points) > 0:
            target_point = agent.state.patrol_points[agent.state.current_patrol_index]

            # 检查是否到达巡逻点
            dx = target_point[0] - agent.state.position[0]
            dy = target_point[1] - agent.state.position[1]
            distance = math.sqrt(dx * dx + dy * dy)

            if distance < 10:  # 到达巡逻点
                agent.state.current_patrol_index = (agent.state.current_patrol_index + 1) % len(
                    agent.state.patrol_points)
            else:
                agent.move_towards(target_point, 50.0)

            agent.state.state = "patrol"
            return NodeStatus.SUCCESS

        return NodeStatus.FAILURE

    @staticmethod
    def idle(agent: GameAgent, delta_time: float) -> NodeStatus:
        """待机"""
        agent.state.velocity = (0, 0)
        agent.state.state = "idle"
        return NodeStatus.SUCCESS


@dataclass
class GameConfig:
    """游戏配置"""
    # 窗口配置
    screen_width: int = 1200
    screen_height: int = 800
    fps: int = 60

    # 游戏世界配置
    world_width: float = 1200.0
    world_height: float = 800.0

    # 代理配置
    agent_count: int = 4
    agent_radius: float = 10.0
    agent_health: float = 100.0
    agent_attack_range: float = 60.0
    agent_sight_range: float = 200.0
    agent_attack_damage: float = 10.0
    agent_attack_cooldown: float = 1.0

    # 巡逻路径配置
    patrol_points: List[tuple[float, float]] = field(default_factory=list)


class EventLogger:
    """事件日志系统"""

    def __init__(self):
        self.events: List[str] = []
        self.max_events = 100

    def log(self, message: str) -> None:
        """记录事件"""
        timestamp = datetime.now().strftime("%H:%M:%S")
        event = f"[{timestamp}] {message}"
        self.events.append(event)

        if len(self.events) > self.max_events:
            self.events.pop(0)

        print(event)

    def get_recent_events(self, count: int = 10) -> List[str]:
        """获取最近的事件"""
        return self.events[-count:]


class GameWorld:
    """游戏世界管理器"""

    def __init__(self, config: GameConfig):
        self.config = config
        self.agents: Dict[int, GameAgent] = {}
        self.logger = EventLogger()
        self.elapsed_time = 0.0
        self.frame_count = 0
        self._create_agents()

    def _create_agents(self) -> None:
        """创建代理"""
        # 默认巡逻点
        if not self.config.patrol_points:
            self.config.patrol_points = [
                (200, 200),
                (self.config.world_width - 200, 200),
                (self.config.world_width - 200, self.config.world_height - 200),
                (200, self.config.world_height - 200)
            ]

        for i in range(self.config.agent_count):
            # 随机初始位置
            x = random.uniform(100, self.config.world_width - 100)
            y = random.uniform(100, self.config.world_height - 100)

            state = AgentState(
                position=(x, y),
                velocity=(0, 0),
                health=self.config.agent_health,
                max_health=self.config.agent_health,
                attack_range=self.config.agent_attack_range,
                sight_range=self.config.agent_sight_range,
                attack_damage=self.config.agent_attack_damage,
                attack_cooldown=self.config.agent_attack_cooldown,
                patrol_points=self.config.patrol_points.copy()
            )

            behavior_tree = BehaviorTreeBuilder.create_enemy_behavior_tree()
            agent = GameAgent(i, state, behavior_tree)
            self.agents[i] = agent

            self.logger.log(f"代理 {i} 已创建,位置: ({x:.1f}, {y:.1f})")

    def update(self, delta_time: float) -> None:
        """更新游戏世界"""
        self.elapsed_time += delta_time
        self.frame_count += 1

        # 更新所有代理
        dead_agents = []
        for agent_id, agent in list(self.agents.items()):
            if not agent.is_dead():
                agent.update(delta_time, self.agents)

                # 限制位置在世界范围内
                x, y = agent.state.position
                x = max(0, min(x, self.config.world_width))
                y = max(0, min(y, self.config.world_height))
                agent.state.position = (x, y)
            else:
                dead_agents.append(agent_id)
                self.logger.log(f"代理 {agent_id} 已死亡")

        # 移除死亡的代理
        for agent_id in dead_agents:
            del self.agents[agent_id]

        # 检查游戏结束条件
        if len(self.agents) <= 1:
            self.logger.log("游戏结束!只剩下一个或没有代理")

    def get_alive_agents_count(self) -> int:
        """获取活着的代理数"""
        return len(self.agents)

    def is_game_over(self) -> bool:
        """检查游戏是否结束"""
        return len(self.agents) <= 1

    def get_game_info(self) -> str:
        """获取游戏信息"""
        info = f"FPS: {self.frame_count // max(self.elapsed_time, 1):.1f} | "
        info += f"时间: {self.elapsed_time:.1f}s | "
        info += f"活跃代理: {self.get_alive_agents_count()}/{self.config.agent_count}"
        return info


class GameRenderer:
    """游戏渲染器"""

    def __init__(self, world: GameWorld):
        self.world = world
        try:
            import pygame
            self.pygame = pygame
            self.pygame.init()
            self.display = self.pygame.display.set_mode(
                (self.world.config.screen_width, self.world.config.screen_height)
            )
            self.pygame.display.set_caption("AI行为树演示")
            self.clock = self.pygame.time.Clock()

            # 修复中文字体显示 - 使用系统中文字体
            font_names = ['SimHei', 'SimSun', 'Microsoft YaHei', 'KaiTi']
            self.font = None
            self.small_font = None

            for font_name in font_names:
                try:
                    self.font = self.pygame.font.SysFont(font_name, 24)
                    self.small_font = self.pygame.font.SysFont(font_name, 16)
                    break
                except:
                    continue

            # 如果没有找到中文字体,使用默认字体
            if not self.font:
                self.font = self.pygame.font.Font(None, 24)
                self.small_font = self.pygame.font.Font(None, 16)

        except ImportError:
            self.pygame = None
            
    def render(self) -> bool:
        """渲染游戏画面"""
        if not self.pygame:
            return self._render_text()

        return self._render_pygame()

    def _render_pygame(self) -> bool:
        """使用pygame渲染"""
        for event in self.pygame.event.get():
            if event.type == self.pygame.QUIT:
                return False

        # 清空屏幕
        self.display.fill((255, 255, 255))

        # 绘制巡逻点
        for point in self.world.config.patrol_points:
            self.pygame.draw.circle(self.display, (200, 200, 200),
                                    (int(point[0]), int(point[1])), 5)

        # 绘制代理
        color_map = {
            "idle": (100, 149, 237),  # 玉米花蓝
            "patrol": (144, 238, 144),  # 浅绿
            "chase": (255, 165, 0),  # 橙色
            "attack": (220, 20, 60),  # 深红
            "flee": (255, 192, 203)  # 浅粉红
        }

        for agent in self.world.agents.values():
            x, y = agent.state.position
            color = color_map.get(agent.state.state, (128, 128, 128))

            # 绘制代理圆形
            self.pygame.draw.circle(self.display, color,
                                    (int(x), int(y)),
                                    int(self.world.config.agent_radius))

            # 绘制视野范围
            self.pygame.draw.circle(self.display, (200, 200, 200),
                                    (int(x), int(y)),
                                    int(agent.state.sight_range), 1)

            # 绘制攻击范围
            self.pygame.draw.circle(self.display, (255, 0, 0),
                                    (int(x), int(y)),
                                    int(agent.state.attack_range), 1)

            # 绘制生命条
            bar_width = 30
            bar_height = 5
            health_ratio = agent.state.health / agent.state.max_health

            # 背景条
            self.pygame.draw.rect(self.display, (200, 0, 0),
                                  (int(x - bar_width / 2), int(y - 25), bar_width, bar_height))
            # 生命条
            self.pygame.draw.rect(self.display, (0, 200, 0),
                                  (int(x - bar_width / 2), int(y - 25),
                                   bar_width * health_ratio, bar_height))

            # 绘制目标连线
            if agent.state.target:
                target_x, target_y = agent.state.target.state.position
                self.pygame.draw.line(self.display, (0, 0, 255),
                                      (int(x), int(y)),
                                      (int(target_x), int(target_y)), 1)

            # 绘制ID和状态
            text = self.small_font.render(f"A{agent.id}", True, (0, 0, 0))
            self.display.blit(text, (int(x - 10), int(y - 20)))

        # 绘制游戏信息
        info_text = self.world.get_game_info()
        info_surface = self.font.render(info_text, True, (0, 0, 0))
        self.display.blit(info_surface, (10, 10))

        # 绘制最近事件
        recent_events = self.world.logger.get_recent_events(5)
        for i, event in enumerate(recent_events):
            event_surface = self.small_font.render(event, True, (0, 0, 0))
            self.display.blit(event_surface, (10, 50 + i * 25))

        self.pygame.display.flip()
        self.clock.tick(self.world.config.fps)

        return True

    def _render_text(self) -> bool:
        """文本模式渲染"""
        if self.world.frame_count % 30 == 0:  # 每秒打印一次
            print(f"\n{self.world.get_game_info()}")
            for agent in self.world.agents.values():
                print(f"  代理{agent.id}: 位置({agent.state.position[0]:.1f}, "
                      f"{agent.state.position[1]:.1f}) "
                      f"生命: {agent.state.health:.1f} 状态: {agent.state.state}")

        return True


class Game:
    """主游戏类"""

    def __init__(self, config: GameConfig = None):
        self.config = config or GameConfig()
        self.world = GameWorld(self.config)
        self.renderer = GameRenderer(self.world)
        self.running = True

    def run(self) -> None:
        """运行游戏主循环"""
        self.world.logger.log("游戏启动!")

        while self.running:
            # 更新游戏
            delta_time = 1.0 / self.config.fps
            self.world.update(delta_time)

            # 渲染游戏
            if not self.renderer.render():
                self.running = False

            # 检查游戏结束
            if self.world.is_game_over():
                self.world.logger.log("游戏结束,所有代理已死亡或只剩一个")
                self.running = False

        self.world.logger.log("游戏退出")
        print("\n=== 游戏统计 ===")
        print(f"游戏持续时间: {self.world.elapsed_time:.1f}秒")
        print(f"总帧数: {self.world.frame_count}")
        print(f"平均FPS: {self.world.frame_count / max(self.world.elapsed_time, 1):.1f}")


def main():
    """主程序入口"""
    # 创建游戏配置
    config = GameConfig(
        agent_count=6,
        agent_health=100.0,
        agent_attack_range=60.0,
        agent_sight_range=200.0,
        agent_attack_damage=15.0
    )

    # 创建并运行游戏
    game = Game(config)
    game.run()


if __name__ == "__main__":
    main()

这个行为树系统展示了Python在游戏AI开发方面的应用。它包括了行为树的基本节点类型、复合节点、装饰器节点,以及一个完整的敌人AI实现。

第5章 角色扮演与冒险游戏

5.1 RPG游戏核心系统设计

角色扮演游戏(RPG)是游戏开发中最复杂的类型之一,它包含角色系统、战斗系统、装备系统、技能系统等多个子系统。Python的面向对象特性使得RPG系统的模块化设计变得相对简单。

RPG核心系统实现

复制代码
from abc import ABC, abstractmethod
from typing import List, Dict, Optional, Tuple, Set
from dataclasses import dataclass, field
from enum import Enum
import random
import math

class CharacterClass(Enum):
    """角色职业"""
    WARRIOR = "warrior"
    MAGE = "mage"
    ROGUE = "rogue"
    HEALER = "healer"

class StatType(Enum):
    """属性类型"""
    HEALTH = "health"
    MANA = "mana"
    STRENGTH = "strength"
    INTELLIGENCE = "intelligence"
    DEXTERITY = "dexterity"
    DEFENSE = "defense"
    MAGIC_DEFENSE = "magic_defense"
    SPEED = "speed"
    LUCK = "luck"

class SkillType(Enum):
    """技能类型"""
    ACTIVE = "active"
    PASSIVE = "passive"
    ULTIMATE = "ultimate"

class ItemType(Enum):
    """物品类型"""
    WEAPON = "weapon"
    ARMOR = "armor"
    ACCESSORY = "accessory"
    CONSUMABLE = "consumable"
    QUEST_ITEM = "quest_item"
    MATERIAL = "material"

class rarity(Enum):
    """稀有度"""
    COMMON = "common"
    UNCOMMON = "uncommon"
    RARE = "rare"
    EPIC = "epic"
    LEGENDARY = "legendary"

@dataclass
class StatModifier:
    """属性修饰符"""
    stat_type: StatType
    value: float
    is_percentage: bool = False
    duration: int = -1  # -1 表示永久
    
    def apply(self, base_value: float) -> float:
        """应用修饰符"""
        if self.is_percentage:
            return base_value * (1 + self.value / 100)
        else:
            return base_value + self.value

@dataclass
class Skill:
    """技能"""
    id: str
    name: str
    description: str
    skill_type: SkillType
    mana_cost: int = 0
    cooldown: int = 0
    current_cooldown: int = 0
    level: int = 1
    max_level: int = 10
    
    def can_use(self, character: 'Character') -> bool:
        """检查是否可以使用技能"""
        if self.current_cooldown > 0:
            return False
        if character.mana < self.mana_cost:
            return False
        return True
    
    def use(self, character: 'Character', targets: List['Character']) -> bool:
        """使用技能"""
        if not self.can_use(character):
            return False
        
        character.mana -= self.mana_cost
        self.current_cooldown = self.cooldown
        return True
    
    def update(self) -> None:
        """更新技能冷却"""
        if self.current_cooldown > 0:
            self.current_cooldown -= 1

@dataclass
class Item:
    """物品"""
    id: str
    name: str
    description: str
    item_type: ItemType
    rarity: rarity = rarity.COMMON
    value: int = 0
    stack_size: int = 1
    stat_modifiers: List[StatModifier] = field(default_factory=list)
    required_level: int = 1
    is_equippable: bool = False
    
    def get_stat_bonus(self, stat_type: StatType) -> float:
        """获取属性加成"""
        total_bonus = 0.0
        for modifier in self.stat_modifiers:
            if modifier.stat_type == stat_type:
                total_bonus += modifier.value
        return total_bonus

class Inventory:
    """背包系统"""
    
    def __init__(self, capacity: int = 20):
        self.capacity = capacity
        self.items: Dict[str, Tuple[Item, int]] = {}  # item_id: (item, quantity)
        self.gold: int = 0
    
    def add_item(self, item: Item, quantity: int = 1) -> bool:
        """添加物品"""
        if item.id in self.items:
            current_item, current_quantity = self.items[item.id]
            new_quantity = current_quantity + quantity
            
            # 检查堆叠限制
            if new_quantity <= item.stack_size:
                self.items[item.id] = (item, new_quantity)
                return True
            else:
                # 超过堆叠限制
                can_add = item.stack_size - current_quantity
                if can_add > 0:
                    self.items[item.id] = (item, item.stack_size)
                    quantity -= can_add
        
        # 检查背包容量
        slots_needed = (quantity + item.stack_size - 1) // item.stack_size
        if len(self.items) + slots_needed > self.capacity:
            return False
        
        # 添加新物品
        remaining = quantity
        while remaining > 0:
            stack_quantity = min(remaining, item.stack_size)
            self.items[f"{item.id}_{len(self.items)}"] = (item, stack_quantity)
            remaining -= stack_quantity
        
        return True
    
    def remove_item(self, item_id: str, quantity: int = 1) -> bool:
        """移除物品"""
        if item_id not in self.items:
            return False
        
        item, current_quantity = self.items[item_id]
        
        if current_quantity <= quantity:
            del self.items[item_id]
            return True
        else:
            self.items[item_id] = (item, current_quantity - quantity)
            return True
    
    def get_item(self, item_id: str) -> Optional[Item]:
        """获取物品"""
        if item_id in self.items:
            return self.items[item_id][0]
        return None
    
    def get_item_quantity(self, item_id: str) -> int:
        """获取物品数量"""
        if item_id in self.items:
            return self.items[item_id][1]
        return 0
    
    def get_all_items(self) -> List[Tuple[Item, int]]:
        """获取所有物品"""
        return list(self.items.values())
    
    def has_item(self, item_id: str) -> bool:
        """检查是否拥有物品"""
        return item_id in self.items
    
    def get_total_value(self) -> int:
        """获取背包物品总价值"""
        total = 0
        for item, quantity in self.items.values():
            total += item.value * quantity
        return total

class Equipment:
    """装备系统"""
    
    def __init__(self):
        self.slots: Dict[str, Optional[Item]] = {
            'weapon': None,
            'armor': None,
            'helmet': None,
            'gloves': None,
            'boots': None,
            'accessory1': None,
            'accessory2': None
        }
    
    def equip(self, item: Item, slot: str) -> bool:
        """装备物品"""
        if slot not in self.slots:
            return False
        if not item.is_equippable:
            return False
        
        self.slots[slot] = item
        return True
    
    def unequip(self, slot: str) -> Optional[Item]:
        """卸下装备"""
        if slot not in self.slots:
            return None
        
        item = self.slots[slot]
        self.slots[slot] = None
        return item
    
    def get_equipped_item(self, slot: str) -> Optional[Item]:
        """获取指定槽位的装备"""
        return self.slots.get(slot)
    
    def get_all_equipped_items(self) -> List[Item]:
        """获取所有装备"""
        return [item for item in self.slots.values() if item is not None]
    
    def get_stat_bonus(self, stat_type: StatType) -> float:
        """获取装备属性加成"""
        total_bonus = 0.0
        for item in self.get_all_equipped_items():
            total_bonus += item.get_stat_bonus(stat_type)
        return total_bonus

class Character:
    """角色类"""
    
    def __init__(self, char_id: str, name: str, char_class: CharacterClass, level: int = 1):
        self.id = char_id
        self.name = name
        self.char_class = char_class
        self.level = level
        self.experience = 0
        self.experience_to_next_level = self.calculate_experience_needed(level)
        
        # 基础属性
        self.base_stats: Dict[StatType, float] = self.get_base_stats_for_class(char_class, level)
        self.current_stats: Dict[StatType, float] = self.base_stats.copy()
        
        # 当前状态
        self.health = self.base_stats[StatType.HEALTH]
        self.mana = self.base_stats[StatType.MANA]
        self.max_health = self.base_stats[StatType.HEALTH]
        self.max_mana = self.base_stats[StatType.MANA]
        
        # 战斗状态
        self.is_alive = True
        self.buffs: List[StatModifier] = []
        self.debuffs: List[StatModifier] = []
        
        # 技能
        self.skills: Dict[str, Skill] = {}
        self.learn_default_skills()
        
        # 背包和装备
        self.inventory = Inventory()
        self.equipment = Equipment()
    
    def get_base_stats_for_class(self, char_class: CharacterClass, level: int) -> Dict[StatType, float]:
        """根据职业获取基础属性"""
        base_stats = {
            StatType.HEALTH: 100,
            StatType.MANA: 50,
            StatType.STRENGTH: 10,
            StatType.INTELLIGENCE: 10,
            StatType.DEXTERITY: 10,
            StatType.DEFENSE: 5,
            StatType.MAGIC_DEFENSE: 5,
            StatType.SPEED: 10,
            StatType.LUCK: 5
        }
        
        # 根据职业调整属性
        class_bonuses = {
            CharacterClass.WARRIOR: {
                StatType.STRENGTH: 5,
                StatType.DEFENSE: 3,
                StatType.HEALTH: 20
            },
            CharacterClass.MAGE: {
                StatType.INTELLIGENCE: 5,
                StatType.MANA: 30,
                StatType.MAGIC_DEFENSE: 2
            },
            CharacterClass.ROGUE: {
                StatType.DEXTERITY: 5,
                StatType.SPEED: 3,
                StatType.LUCK: 2
            },
            CharacterClass.HEALER: {
                StatType.INTELLIGENCE: 3,
                StatType.MANA: 20,
                StatType.MAGIC_DEFENSE: 3
            }
        }
        
        # 应用职业加成
        for stat_type, bonus in class_bonuses.get(char_class, {}).items():
            base_stats[stat_type] += bonus * level
        
        # 根据等级成长
        for stat_type in base_stats:
            base_stats[stat_type] *= (1 + (level - 1) * 0.1)
        
        return base_stats
    
    def calculate_experience_needed(self, level: int) -> int:
        """计算升级所需经验"""
        return int(100 * (level ** 1.5))
    
    def learn_default_skills(self) -> None:
        """学习默认技能"""
        if self.char_class == CharacterClass.WARRIOR:
            self.skills['slash'] = Skill('slash', '斩击', '强力斩击攻击敌人', SkillType.ACTIVE, mana_cost=5, cooldown=1)
            self.skills['shield_bash'] = Skill('shield_bash', '盾击', '用盾牌击晕敌人', SkillType.ACTIVE, mana_cost=10, cooldown=3)
        
        elif self.char_class == CharacterClass.MAGE:
            self.skills['fireball'] = Skill('fireball', '火球术', '发射火球攻击敌人', SkillType.ACTIVE, mana_cost=15, cooldown=2)
            self.skills['ice_armor'] = Skill('ice_armor', '冰甲术', '增加防御力', SkillType.ACTIVE, mana_cost=20, cooldown=5)
        
        elif self.char_class == CharacterClass.ROGUE:
            self.skills['backstab'] = Skill('backstab', '背刺', '从背后攻击敌人', SkillType.ACTIVE, mana_cost=8, cooldown=2)
            self.skills['stealth'] = Skill('stealth', '潜行', '进入隐身状态', SkillType.ACTIVE, mana_cost=12, cooldown=4)
        
        elif self.char_class == CharacterClass.HEALER:
            self.skills['heal'] = Skill('heal', '治疗术', '恢复队友生命值', SkillType.ACTIVE, mana_cost=10, cooldown=1)
            self.skills['holy_light'] = Skill('holy_light', '圣光', '对敌人造成光属性伤害', SkillType.ACTIVE, mana_cost=15, cooldown=3)
    
    def update_stats(self) -> None:
        """更新角色属性"""
        # 计算装备加成
        equipment_bonuses = {}
        for stat_type in StatType:
            equipment_bonuses[stat_type] = self.equipment.get_stat_bonus(stat_type)
        
        # 计算buff/debuff加成
        buff_bonuses = {}
        for stat_type in StatType:
            buff_bonuses[stat_type] = 0.0
        
        for modifier in self.buffs:
            buff_bonuses[modifier.stat_type] += modifier.value
        
        for modifier in self.debuffs:
            buff_bonuses[modifier.stat_type] -= modifier.value
        
        # 更新当前属性
        for stat_type in StatType:
            base = self.base_stats[stat_type]
            equip = equipment_bonuses[stat_type]
            buffs = buff_bonuses[stat_type]
            self.current_stats[stat_type] = base + equip + buffs
        
        # 更新最大生命值和魔法值
        new_max_health = self.current_stats[StatType.HEALTH]
        new_max_mana = self.current_stats[StatType.MANA]
        
        # 按比例调整当前生命值和魔法值
        if self.max_health > 0:
            health_ratio = self.health / self.max_health
            self.health = new_max_health * health_ratio
        else:
            self.health = new_max_health
        
        if self.max_mana > 0:
            mana_ratio = self.mana / self.max_mana
            self.mana = new_max_mana * mana_ratio
        else:
            self.mana = new_max_mana
        
        self.max_health = new_max_health
        self.max_mana = new_max_mana
    
    def gain_experience(self, amount: int) -> None:
        """获得经验值"""
        self.experience += amount
        
        while self.experience >= self.experience_to_next_level:
            self.level_up()
    
    def level_up(self) -> None:
        """升级"""
        self.experience -= self.experience_to_next_level
        self.level += 1
        self.experience_to_next_level = self.calculate_experience_needed(self.level)
        
        # 增加基础属性
        for stat_type in self.base_stats:
            growth_rate = 0.1  # 每级增长10%
            if stat_type in [StatType.HEALTH, StatType.MANA]:
                growth_rate = 0.15  # 生命值和魔法值增长更多
            
            self.base_stats[stat_type] *= (1 + growth_rate)
        
        # 恢复生命值和魔法值
        self.health = self.base_stats[StatType.HEALTH]
        self.mana = self.base_stats[StatType.MANA]
        
        # 更新属性
        self.update_stats()
    
    def take_damage(self, damage: float, damage_type: str = "physical") -> float:
        """受到伤害"""
        actual_damage = damage
        
        if damage_type == "physical":
            defense = self.current_stats[StatType.DEFENSE]
            actual_damage = max(1, damage - defense)
        
        elif damage_type == "magic":
            magic_defense = self.current_stats[StatType.MAGIC_DEFENSE]
            actual_damage = max(1, damage - magic_defense)
        
        self.health -= actual_damage
        
        if self.health <= 0:
            self.health = 0
            self.is_alive = False
        
        return actual_damage
    
    def heal(self, amount: float) -> float:
        """恢复生命值"""
        old_health = self.health
        self.health = min(self.max_health, self.health + amount)
        return self.health - old_health
    
    def restore_mana(self, amount: float) -> float:
        """恢复魔法值"""
        old_mana = self.mana
        self.mana = min(self.max_mana, self.mana + amount)
        return self.mana - old_mana
    
    def use_skill(self, skill_id: str, targets: List['Character']) -> bool:
        """使用技能"""
        if skill_id not in self.skills:
            return False
        
        skill = self.skills[skill_id]
        return skill.use(self, targets)
    
    def update_buffs_and_debuffs(self) -> None:
        """更新buff和debuff"""
        # 更新持续时间
        self.buffs = [buff for buff in self.buffs if buff.duration > 0]
        self.debuffs = [debuff for debuff in self.debuffs if debuff.duration > 0]
        
        for buff in self.buffs:
            if buff.duration > 0:
                buff.duration -= 1
        
        for debuff in self.debuffs:
            if debuff.duration > 0:
                debuff.duration -= 1
        
        # 更新属性
        self.update_stats()
    
    def attack(self, target: 'Character') -> Tuple[float, str]:
        """普通攻击"""
        if not self.is_alive or not target.is_alive:
            return 0.0, "invalid_target"
        
        # 计算攻击力
        attack_power = self.current_stats[StatType.STRENGTH]
        
        # 计算暴击
        is_critical = random.random() < (self.current_stats[StatType.LUCK] / 100)
        if is_critical:
            attack_power *= 2.0
        
        # 计算闪避
        dodge_chance = target.current_stats[StatType.DEXTERITY] / 100
        if random.random() < dodge_chance:
            return 0.0, "dodged"
        
        # 计算伤害
        damage = max(1, attack_power - target.current_stats[StatType.DEFENSE] * 0.5)
        actual_damage = target.take_damage(damage, "physical")
        
        attack_type = "critical_attack" if is_critical else "normal_attack"
        return actual_damage, attack_type

class Party:
    """队伍系统"""
    
    def __init__(self, max_members: int = 4):
        self.max_members = max_members
        self.members: List[Character] = []
        self.leader: Optional[Character] = None
    
    def add_member(self, character: Character) -> bool:
        """添加成员"""
        if len(self.members) >= self.max_members:
            return False
        
        if character not in self.members:
            self.members.append(character)
            
            # 如果是第一个成员,设为队长
            if len(self.members) == 1:
                self.leader = character
            
            return True
        
        return False
    
    def remove_member(self, character: Character) -> bool:
        """移除成员"""
        if character in self.members:
            self.members.remove(character)
            
            # 如果移除的是队长,重新指定队长
            if character == self.leader and self.members:
                self.leader = self.members[0]
            
            return True
        
        return False
    
    def get_alive_members(self) -> List[Character]:
        """获取存活的成员"""
        return [member for member in self.members if member.is_alive]
    
    def is_all_dead(self) -> bool:
        """检查是否全队阵亡"""
        return len(self.get_alive_members()) == 0
    
    def get_total_level(self) -> int:
        """获取队伍总等级"""
        return sum(member.level for member in self.members)
    
    def get_average_level(self) -> float:
        """获取队伍平均等级"""
        if not self.members:
            return 0.0
        return self.get_total_level() / len(self.members)

class Quest:
    """任务系统"""
    
    def __init__(self, quest_id: str, name: str, description: str):
        self.id = quest_id
        self.name = name
        self.description = description
        self.objectives: List[Dict] = []
        self.rewards: Dict[str, int] = {}
        self.is_completed = False
        self.is_active = False
    
    def add_objective(self, objective_type: str, target_id: str, target_quantity: int) -> None:
        """添加任务目标"""
        self.objectives.append({
            'type': objective_type,
            'target_id': target_id,
            'target_quantity': target_quantity,
            'current_quantity': 0,
            'is_completed': False
        })
    
    def add_reward(self, reward_type: str, amount: int) -> None:
        """添加任务奖励"""
        self.rewards[reward_type] = amount
    
    def update_objective(self, objective_type: str, target_id: str, quantity: int = 1) -> None:
        """更新任务目标进度"""
        for objective in self.objectives:
            if (objective['type'] == objective_type and 
                objective['target_id'] == target_id and 
                not objective['is_completed']):
                
                objective['current_quantity'] = min(
                    objective['target_quantity'],
                    objective['current_quantity'] + quantity
                )
                
                if objective['current_quantity'] >= objective['target_quantity']:
                    objective['is_completed'] = True
        
        # 检查任务是否完成
        self.check_completion()
    
    def check_completion(self) -> None:
        """检查任务是否完成"""
        if all(objective['is_completed'] for objective in self.objectives):
            self.is_completed = True
    
    def get_progress(self) -> float:
        """获取任务进度"""
        if not self.objectives:
            return 0.0
        
        total = len(self.objectives)
        completed = sum(1 for obj in self.objectives if obj['is_completed'])
        return completed / total

class QuestManager:
    """任务管理器"""
    
    def __init__(self):
        self.available_quests: Dict[str, Quest] = {}
        self.active_quests: Dict[str, Quest] = {}
        self.completed_quests: Set[str] = set()
    
    def add_available_quest(self, quest: Quest) -> None:
        """添加可用任务"""
        if quest.id not in self.completed_quests:
            self.available_quests[quest.id] = quest
    
    def accept_quest(self, quest_id: str) -> bool:
        """接受任务"""
        if quest_id in self.available_quests:
            quest = self.available_quests.pop(quest_id)
            quest.is_active = True
            self.active_quests[quest_id] = quest
            return True
        return False
    
    def update_quest(self, objective_type: str, target_id: str, quantity: int = 1) -> None:
        """更新任务进度"""
        for quest in self.active_quests.values():
            quest.update_objective(objective_type, target_id, quantity)
    
    def complete_quest(self, quest_id: str) -> Optional[Quest]:
        """完成任务"""
        if quest_id in self.active_quests:
            quest = self.active_quests.pop(quest_id)
            if quest.is_completed:
                self.completed_quests.add(quest_id)
                return quest
            else:
                # 任务未完成,放回活跃任务列表
                self.active_quests[quest_id] = quest
        
        return None
    
    def get_quest_rewards(self, quest: Quest) -> Dict[str, int]:
        """获取任务奖励"""
        return quest.rewards

这个RPG核心系统展示了Python在复杂游戏系统开发方面的能力。它包括了角色系统、装备系统、技能系统、背包系统、队伍系统、任务系统等多个子系统,每个系统都设计得相对独立但又相互关联。

5.2 剧情系统与对话管理

冒险游戏的另一个重要组成部分是剧情系统和对话管理。这些系统需要处理复杂的分支剧情、角色关系和玩家选择。

剧情对话系统

复制代码
from typing import Dict, List, Optional, Callable, Any
from dataclasses import dataclass, field
from enum import Enum
import json

class DialogueNodeType(Enum):
    """对话节点类型"""
    TEXT = "text"           # 纯文本
    CHOICE = "choice"       # 选择分支
    CONDITION = "condition" # 条件判断
    ACTION = "action"       # 执行动作
    END = "end"            # 对话结束

@dataclass
class DialogueChoice:
    """对话选项"""
    choice_id: str
    text: str
    next_node_id: str
    requirements: Optional[Dict[str, Any]] = None
    consequences: Optional[Dict[str, Any]] = None

@dataclass
class DialogueNode:
    """对话节点"""
    node_id: str
    node_type: DialogueNodeType
    speaker: str
    text: str
    choices: List[DialogueChoice] = field(default_factory=list)
    conditions: Optional[Dict[str, Any]] = None
    actions: Optional[List[Dict[str, Any]]] = None
    next_node_id: Optional[str] = None

class DialogueManager:
    """对话管理器"""
    
    def __init__(self):
        self.dialogues: Dict[str, Dict[str, DialogueNode]] = {}
        self.current_dialogue: Optional[str] = None
        self.current_node: Optional[DialogueNode] = None
        self.dialogue_history: List[str] = []
        
        # 游戏状态跟踪
        self.game_state: Dict[str, Any] = {}
        
        # 回调和事件系统
        self.on_dialogue_start: Optional[Callable] = None
        self.on_dialogue_end: Optional[Callable] = None
        self.on_node_change: Optional[Callable] = None
    
    def load_dialogue_from_json(self, dialogue_id: str, json_data: str) -> bool:
        """从JSON加载对话"""
        try:
            data = json.loads(json_data)
            dialogue_nodes = {}
            
            for node_data in data['nodes']:
                choices = []
                for choice_data in node_data.get('choices', []):
                    choice = DialogueChoice(
                        choice_id=choice_data['choice_id'],
                        text=choice_data['text'],
                        next_node_id=choice_data['next_node_id'],
                        requirements=choice_data.get('requirements'),
                        consequences=choice_data.get('consequences')
                    )
                    choices.append(choice)
                
                node = DialogueNode(
                    node_id=node_data['node_id'],
                    node_type=DialogueNodeType(node_data['node_type']),
                    speaker=node_data['speaker'],
                    text=node_data['text'],
                    choices=choices,
                    conditions=node_data.get('conditions'),
                    actions=node_data.get('actions'),
                    next_node_id=node_data.get('next_node_id')
                )
                
                dialogue_nodes[node.node_id] = node
            
            self.dialogues[dialogue_id] = dialogue_nodes
            return True
        
        except Exception as e:
            print(f"加载对话失败: {e}")
            return False
    
    def start_dialogue(self, dialogue_id: str, start_node_id: str = "start") -> bool:
        """开始对话"""
        if dialogue_id not in self.dialogues:
            return False
        
        self.current_dialogue = dialogue_id
        self.dialogue_history = []
        
        if self.on_dialogue_start:
            self.on_dialogue_start(dialogue_id)
        
        return self.go_to_node(start_node_id)
    
    def go_to_node(self, node_id: str) -> bool:
        """跳转到指定节点"""
        if self.current_dialogue is None:
            return False
        
        dialogue_nodes = self.dialogues[self.current_dialogue]
        if node_id not in dialogue_nodes:
            return False
        
        self.current_node = dialogue_nodes[node_id]
        self.dialogue_history.append(node_id)
        
        # 检查条件
        if self.current_node.conditions:
            if not self.check_conditions(self.current_node.conditions):
                # 条件不满足,跳转到下一节点或结束
                if self.current_node.next_node_id:
                    return self.go_to_node(self.current_node.next_node_id)
                else:
                    self.end_dialogue()
                    return True
        
        # 执行动作
        if self.current_node.actions:
            self.execute_actions(self.current_node.actions)
        
        # 触发节点变化回调
        if self.on_node_change:
            self.on_node_change(self.current_node)
        
        # 检查是否结束对话
        if self.current_node.node_type == DialogueNodeType.END:
            self.end_dialogue()
        
        return True
    
    def make_choice(self, choice_id: str) -> bool:
        """玩家做出选择"""
        if not self.current_node:
            return False
        
        # 找到对应的选择
        choice = None
        for c in self.current_node.choices:
            if c.choice_id == choice_id:
                choice = c
                break
        
        if not choice:
            return False
        
        # 检查选择条件
        if choice.requirements:
            if not self.check_conditions(choice.requirements):
                return False
        
        # 执行选择后果
        if choice.consequences:
            self.execute_actions(choice.consequences)
        
        # 跳转到下一节点
        return self.go_to_node(choice.next_node_id)
    
    def check_conditions(self, conditions: Dict[str, Any]) -> bool:
        """检查条件"""
        for condition_type, condition_value in conditions.items():
            if condition_type == "has_item":
                item_id = condition_value
                # 这里需要对接背包系统
                # if not self.game_state.get('inventory', {}).has_item(item_id):
                #     return False
            
            elif condition_type == "level":
                required_level = condition_value
                current_level = self.game_state.get('player_level', 1)
                if current_level < required_level:
                    return False
            
            elif condition_type == "has_flag":
                flag_name = condition_value
                if not self.game_state.get('flags', {}).get(flag_name, False):
                    return False
            
            elif condition_type == "completed_quest":
                quest_id = condition_value
                if quest_id not in self.game_state.get('completed_quests', set()):
                    return False
        
        return True
    
    def execute_actions(self, actions: List[Dict[str, Any]]) -> None:
        """执行动作"""
        for action in actions:
            action_type = action.get('type')
            
            if action_type == "give_item":
                item_id = action.get('item_id')
                quantity = action.get('quantity', 1)
                # 对接背包系统
                # self.game_state['inventory'].add_item(item_id, quantity)
            
            elif action_type == "remove_item":
                item_id = action.get('item_id')
                quantity = action.get('quantity', 1)
                # 对接背包系统
                # self.game_state['inventory'].remove_item(item_id, quantity)
            
            elif action_type == "give_exp":
                exp_amount = action.get('amount', 0)
                # 对接角色系统
                # self.game_state['player'].gain_experience(exp_amount)
            
            elif action_type == "set_flag":
                flag_name = action.get('flag')
                value = action.get('value', True)
                if 'flags' not in self.game_state:
                    self.game_state['flags'] = {}
                self.game_state['flags'][flag_name] = value
            
            elif action_type == "start_quest":
                quest_id = action.get('quest_id')
                # 对接任务系统
                # self.game_state['quest_manager'].accept_quest(quest_id)
            
            elif action_type == "complete_quest":
                quest_id = action.get('quest_id')
                # 对接任务系统
                # self.game_state['quest_manager'].complete_quest(quest_id)
            
            elif action_type == "change_relationship":
                character_id = action.get('character_id')
                value = action.get('value', 0)
                if 'relationships' not in self.game_state:
                    self.game_state['relationships'] = {}
                self.game_state['relationships'][character_id] = \
                    self.game_state['relationships'].get(character_id, 0) + value
            
            elif action_type == "teleport":
                location_id = action.get('location_id')
                # 对接地图系统
                # self.game_state['current_location'] = location_id
    
    def end_dialogue(self) -> None:
        """结束对话"""
        if self.on_dialogue_end:
            self.on_dialogue_end(self.current_dialogue)
        
        self.current_dialogue = None
        self.current_node = None
    
    def get_available_choices(self) -> List[DialogueChoice]:
        """获取可用选项"""
        if not self.current_node:
            return []
        
        available_choices = []
        for choice in self.current_node.choices:
            if not choice.requirements or self.check_conditions(choice.requirements):
                available_choices.append(choice)
        
        return available_choices
    
    def get_current_dialogue_state(self) -> Optional[Dict[str, Any]]:
        """获取当前对话状态"""
        if not self.current_node:
            return None
        
        return {
            'dialogue_id': self.current_dialogue,
            'node_id': self.current_node.node_id,
            'speaker': self.current_node.speaker,
            'text': self.current_node.text,
            'choices': self.get_available_choices(),
            'node_type': self.current_node.node_type.value
        }

class RelationshipManager:
    """关系管理器"""
    
    def __init__(self):
        self.relationships: Dict[str, Dict[str, int]] = {}  # character_id: {target_id: value}
    
    def set_relationship(self, character_id: str, target_id: str, value: int) -> None:
        """设置关系值"""
        if character_id not in self.relationships:
            self.relationships[character_id] = {}
        
        self.relationships[character_id][target_id] = max(-100, min(100, value))
    
    def modify_relationship(self, character_id: str, target_id: str, delta: int) -> None:
        """修改关系值"""
        current_value = self.get_relationship(character_id, target_id)
        self.set_relationship(character_id, target_id, current_value + delta)
    
    def get_relationship(self, character_id: str, target_id: str) -> int:
        """获取关系值"""
        return self.relationships.get(character_id, {}).get(target_id, 0)
    
    def get_relationship_level(self, character_id: str, target_id: str) -> str:
        """获取关系等级"""
        value = self.get_relationship(character_id, target_id)
        
        if value >= 80:
            return "挚友"
        elif value >= 50:
            return "好友"
        elif value >= 20:
            return "朋友"
        elif value >= -20:
            return "认识"
        elif value >= -50:
            return "冷淡"
        elif value >= -80:
            return "敌对"
        else:
            return "仇恨"

这个剧情对话系统展示了Python在处理复杂叙事逻辑方面的能力。它包括了多分支对话、条件判断、动作执行、关系管理等功能,能够支持复杂的剧情发展。

第6章 策略与模拟游戏

6.1 回合制策略游戏引擎

策略游戏通常涉及复杂的游戏逻辑、AI决策和状态管理。Python的高级特性使得构建策略游戏引擎变得更加容易。

回合制策略引擎

复制代码
from abc import ABC, abstractmethod
from typing import List, Dict, Optional, Tuple, Set
from dataclasses import dataclass, field
from enum import Enum
import random
import math

class TileType(Enum):
    """地块类型"""
    GRASS = "grass"
    FOREST = "forest"
    MOUNTAIN = "mountain"
    WATER = "water"
    DESERT = "desert"
    CITY = "city"
    ROAD = "road"

class UnitType(Enum):
    """单位类型"""
    INFANTRY = "infantry"
    CAVALRY = "cavalry"
    ARCHER = "archer"
    SIEGE = "siege"
    HERO = "hero"

class ActionType(Enum):
    """行动类型"""
    MOVE = "move"
    ATTACK = "attack"
    DEFEND = "defend"
    SKILL = "skill"
    WAIT = "wait"

@dataclass
class Tile:
    """地图地块"""
    x: int
    y: int
    tile_type: TileType
    movement_cost: int = 1
    defense_bonus: int = 0
    is_occupied: bool = False
    unit: Optional['Unit'] = None
    building: Optional['Building'] = None

@dataclass
class Unit:
    """游戏单位"""
    unit_id: str
    name: str
    unit_type: UnitType
    owner: str
    level: int = 1
    experience: int = 0
    
    # 基础属性
    health: int = 100
    max_health: int = 100
    attack: int = 10
    defense: int = 5
    movement: int = 3
    movement_range: int = 3
    
    # 战斗属性
    attack_range: int = 1
    counter_attack: bool = True
    
    # 状态
    has_moved: bool = False
    has_attacked: bool = False
    is_defending: bool = False
    
    # 位置
    x: int = 0
    y: int = 0
    
    def can_move(self) -> bool:
        """是否可以移动"""
        return not self.has_moved
    
    def can_attack(self) -> bool:
        """是否可以攻击"""
        return not self.has_attacked
    
    def reset_turn(self) -> None:
        """重置回合状态"""
        self.has_moved = False
        self.has_attacked = False
        self.is_defending = False
    
    def take_damage(self, damage: int) -> int:
        """受到伤害"""
        if self.is_defending:
            damage = int(damage * 0.5)  # 防御状态减伤50%
        
        actual_damage = max(1, damage - self.defense)
        self.health -= actual_damage
        
        if self.health <= 0:
            self.health = 0
        
        return actual_damage
    
    def is_alive(self) -> bool:
        """是否存活"""
        return self.health > 0
    
    def gain_experience(self, amount: int) -> None:
        """获得经验"""
        self.experience += amount
        
        # 升级检查
        exp_needed = self.level * 100
        while self.experience >= exp_needed:
            self.experience -= exp_needed
            self.level_up()
            exp_needed = self.level * 100
    
    def level_up(self) -> None:
        """升级"""
        self.level += 1
        
        # 属性成长
        growth_rates = {
            UnitType.INFANTRY: {'health': 10, 'attack': 2, 'defense': 1},
            UnitType.CAVALRY: {'health': 12, 'attack': 3, 'defense': 1},
            UnitType.ARCHER: {'health': 8, 'attack': 3, 'defense': 0},
            UnitType.SIEGE: {'health': 15, 'attack': 4, 'defense': 2},
            UnitType.HERO: {'health': 15, 'attack': 3, 'defense': 2}
        }
        
        growth = growth_rates.get(self.unit_type, {'health': 10, 'attack': 2, 'defense': 1})
        
        self.max_health += growth['health']
        self.health = self.max_health
        self.attack += growth['attack']
        self.defense += growth['defense']

@dataclass
class Building:
    """建筑"""
    building_id: str
    name: str
    owner: str
    building_type: str
    health: int = 200
    max_health: int = 200
    
    # 功能属性
    production: Optional[str] = None
    income: int = 0
    defense_bonus: int = 0
    
    # 位置
    x: int = 0
    y: int = 0
    
    def is_alive(self) -> bool:
        """是否存活"""
        return self.health > 0

class GameMap:
    """游戏地图"""
    
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height
        self.tiles: List[List[Tile]] = []
        self.units: Dict[str, Unit] = {}
        self.buildings: Dict[str, Building] = {}
        
        self.initialize_map()
    
    def initialize_map(self) -> None:
        """初始化地图"""
        # 创建基础地形
        for y in range(self.height):
            row = []
            for x in range(self.width):
                # 随机生成地形
                rand = random.random()
                if rand < 0.6:
                    tile_type = TileType.GRASS
                    movement_cost = 1
                elif rand < 0.75:
                    tile_type = TileType.FOREST
                    movement_cost = 2
                    defense_bonus = 1
                elif rand < 0.85:
                    tile_type = TileType.MOUNTAIN
                    movement_cost = 3
                    defense_bonus = 2
                elif rand < 0.95:
                    tile_type = TileType.WATER
                    movement_cost = 99  # 不可通行
                else:
                    tile_type = TileType.DESERT
                    movement_cost = 2
                
                tile = Tile(
                    x=x,
                    y=y,
                    tile_type=tile_type,
                    movement_cost=movement_cost,
                    defense_bonus=defense_bonus if tile_type in [TileType.FOREST, TileType.MOUNTAIN] else 0
                )
                row.append(tile)
            
            self.tiles.append(row)
        
        # 添加一些城市
        for _ in range(3):
            city_x = random.randint(1, self.width - 2)
            city_y = random.randint(1, self.height - 2)
            self.tiles[city_y][city_x].tile_type = TileType.CITY
            self.tiles[city_y][city_x].movement_cost = 1
    
    def get_tile(self, x: int, y: int) -> Optional[Tile]:
        """获取地块"""
        if 0 <= x < self.width and 0 <= y < self.height:
            return self.tiles[y][x]
        return None
    
    def is_valid_position(self, x: int, y: int) -> bool:
        """检查位置是否有效"""
        return 0 <= x < self.width and 0 <= y < self.height
    
    def can_move_to(self, unit: Unit, x: int, y: int) -> bool:
        """检查单位是否可以移动到指定位置"""
        tile = self.get_tile(x, y)
        if not tile:
            return False
        
        # 检查地形是否可通行
        if tile.movement_cost >= 99:
            return False
        
        # 检查是否有其他单位
        if tile.unit and tile.unit != unit:
            return False
        
        return True
    
    def get_movement_cost(self, start_x: int, start_y: int, end_x: int, end_y: int) -> int:
        """获取移动消耗"""
        if not self.is_valid_position(end_x, end_y):
            return float('inf')
        
        return self.tiles[end_y][end_x].movement_cost
    
    def place_unit(self, unit: Unit, x: int, y: int) -> bool:
        """放置单位"""
        tile = self.get_tile(x, y)
        if not tile or tile.unit:
            return False
        
        tile.unit = unit
        tile.is_occupied = True
        unit.x = x
        unit.y = y
        self.units[unit.unit_id] = unit
        return True
    
    def remove_unit(self, unit_id: str) -> Optional[Unit]:
        """移除单位"""
        if unit_id not in self.units:
            return None
        
        unit = self.units[unit_id]
        tile = self.get_tile(unit.x, unit.y)
        if tile and tile.unit == unit:
            tile.unit = None
            tile.is_occupied = False
        
        del self.units[unit_id]
        return unit
    
    def place_building(self, building: Building, x: int, y: int) -> bool:
        """放置建筑"""
        tile = self.get_tile(x, y)
        if not tile or tile.building:
            return False
        
        tile.building = building
        building.x = x
        building.y = y
        self.buildings[building.building_id] = building
        return True
    
    def get_units_in_range(self, center_x: int, center_y: int, range_: int) -> List[Unit]:
        """获取指定范围内的单位"""
        units_in_range = []
        
        for unit in self.units.values():
            distance = abs(unit.x - center_x) + abs(unit.y - center_y)
            if distance <= range_:
                units_in_range.append(unit)
        
        return units_in_range
    
    def get_buildings_in_range(self, center_x: int, center_y: int, range_: int) -> List[Building]:
        """获取指定范围内的建筑"""
        buildings_in_range = []
        
        for building in self.buildings.values():
            distance = abs(building.x - center_x) + abs(building.y - center_y)
            if distance <= range_:
                buildings_in_range.append(building)
        
        return buildings_in_range

class PathFinder:
    """寻路系统"""
    
    @staticmethod
    def find_path(game_map: GameMap, start_x: int, start_y: int,
                  end_x: int, end_y: int, unit: Unit) -> Optional[List[Tuple[int, int]]]:
        """使用A*算法寻路"""
        if not game_map.is_valid_position(start_x, start_y) or not game_map.is_valid_position(end_x, end_y):
            return None
        
        # 检查目标位置是否可达
        if not game_map.can_move_to(unit, end_x, end_y):
            return None
        
        # A*算法
        open_set = [(start_x, start_y)]
        came_from = {}
        g_score = {(start_x, start_y): 0}
        f_score = {(start_x, start_y): PathFinder.heuristic(start_x, start_y, end_x, end_y)}
        
        while open_set:
            # 找到f_score最小的节点
            current = min(open_set, key=lambda pos: f_score.get(pos, float('inf')))
            
            if current == (end_x, end_y):
                return PathFinder.reconstruct_path(came_from, current)
            
            open_set.remove(current)
            
            # 检查相邻节点
            for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                neighbor_x = current[0] + dx
                neighbor_y = current[1] + dy
                
                if not game_map.can_move_to(unit, neighbor_x, neighbor_y):
                    continue
                
                # 计算新的g_score
                movement_cost = game_map.get_movement_cost(current[0], current[1], neighbor_x, neighbor_y)
                tentative_g_score = g_score[current] + movement_cost
                
                if tentative_g_score < g_score.get((neighbor_x, neighbor_y), float('inf')):
                    came_from[(neighbor_x, neighbor_y)] = current
                    g_score[(neighbor_x, neighbor_y)] = tentative_g_score
                    f_score[(neighbor_x, neighbor_y)] = tentative_g_score + PathFinder.heuristic(
                        neighbor_x, neighbor_y, end_x, end_y)
                    
                    if (neighbor_x, neighbor_y) not in open_set:
                        open_set.append((neighbor_x, neighbor_y))
        
        return None  # 没有找到路径
    
    @staticmethod
    def heuristic(x1: int, y1: int, x2: int, y2: int) -> int:
        """曼哈顿距离启发式函数"""
        return abs(x1 - x2) + abs(y1 - y2)
    
    @staticmethod
    def reconstruct_path(came_from: Dict[Tuple[int, int], Tuple[int, int]],
                        current: Tuple[int, int]) -> List[Tuple[int, int]]:
        """重建路径"""
        path = [current]
        while current in came_from:
            current = came_from[current]
            path.append(current)
        path.reverse()
        return path
    
    @staticmethod
    def get_movement_range(game_map: GameMap, unit: Unit) -> Set[Tuple[int, int]]:
        """获取单位可移动范围"""
        reachable = set()
        max_movement = unit.movement_range
        
        # 使用广度优先搜索
        from collections import deque
        queue = deque()
        queue.append((unit.x, unit.y, 0))  # (x, y, current_cost)
        visited = {(unit.x, unit.y): 0}
        
        while queue:
            x, y, cost = queue.popleft()
            
            if cost < max_movement:
                for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                    new_x, new_y = x + dx, y + dy
                    
                    if game_map.can_move_to(unit, new_x, new_y):
                        new_cost = cost + game_map.get_movement_cost(x, y, new_x, new_y)
                        
                        if new_cost <= max_movement:
                            if (new_x, new_y) not in visited or new_cost < visited[(new_x, new_y)]:
                                visited[(new_x, new_y)] = new_cost
                                queue.append((new_x, new_y, new_cost))
                                reachable.add((new_x, new_y))
        
        return reachable

class CombatSystem:
    """战斗系统"""
    
    @staticmethod
    def calculate_damage(attacker: Unit, defender: Unit, game_map: GameMap) -> int:
        """计算伤害"""
        base_damage = attacker.attack
        
        # 地形防御加成
        defender_tile = game_map.get_tile(defender.x, defender.y)
        if defender_tile:
            base_damage -= defender_tile.defense_bonus
        
        # 防御状态加成
        if defender.is_defending:
            base_damage = int(base_damage * 0.5)
        
        # 随机浮动
        variance = random.randint(-10, 10)
        final_damage = max(1, base_damage + variance)
        
        return final_damage
    
    @staticmethod
    def execute_attack(attacker: Unit, defender: Unit, game_map: GameMap) -> Dict[str, any]:
        """执行攻击"""
        result = {
            'attacker': attacker.unit_id,
            'defender': defender.unit_id,
            'damage_dealt': 0,
            'counter_damage': 0,
            'attacker_killed': False,
            'defender_killed': False,
            'experience_gained': 0
        }
        
        # 计算伤害
        damage = CombatSystem.calculate_damage(attacker, defender, game_map)
        result['damage_dealt'] = defender.take_damage(damage)
        
        # 检查防御者是否死亡
        if not defender.is_alive():
            result['defender_killed'] = True
            result['experience_gained'] = defender.level * 10
            attacker.gain_experience(result['experience_gained'])
        else:
            # 反击
            if defender.counter_attack and defender.is_alive():
                counter_range = abs(attacker.x - defender.x) + abs(attacker.y - defender.y)
                if counter_range <= defender.attack_range:
                    counter_damage = CombatSystem.calculate_damage(defender, attacker, game_map)
                    result['counter_damage'] = attacker.take_damage(counter_damage)
                    
                    if not attacker.is_alive():
                        result['attacker_killed'] = True
        
        return result

class TurnManager:
    """回合管理器"""
    
    def __init__(self):
        self.turn_order: List[str] = []
        self.current_turn_index: int = 0
        self.current_player: Optional[str] = None
        self.turn_number: int = 1
        self.players: Dict[str, Dict] = {}
    
    def add_player(self, player_id: str, player_name: str, is_ai: bool = False) -> None:
        """添加玩家"""
        self.players[player_id] = {
            'name': player_name,
            'is_ai': is_ai,
            'gold': 100,
            'units': [],
            'buildings': []
        }
        self.turn_order.append(player_id)
    
    def start_turn(self) -> Optional[str]:
        """开始新回合"""
        if not self.turn_order:
            return None
        
        self.current_player = self.turn_order[self.current_turn_index]
        
        # 重置单位状态
        player_data = self.players[self.current_player]
        for unit_id in player_data['units']:
            # 这里需要重置单位的回合状态
            pass
        
        return self.current_player
    
    def end_turn(self) -> Optional[str]:
        """结束当前回合"""
        if not self.current_player:
            return None
        
        # 移动到下一个玩家
        self.current_turn_index = (self.current_turn_index + 1) % len(self.turn_order)
        
        # 如果回到第一个玩家,增加回合数
        if self.current_turn_index == 0:
            self.turn_number += 1
        
        return self.start_turn()
    
    def get_current_player(self) -> Optional[str]:
        """获取当前玩家"""
        return self.current_player
    
    def is_ai_turn(self) -> bool:
        """检查是否是AI回合"""
        if not self.current_player:
            return False
        return self.players.get(self.current_player, {}).get('is_ai', False)

class StrategyGame:
    """策略游戏主类"""
    
    def __init__(self, map_width: int = 20, map_height: int = 20):
        self.game_map = GameMap(map_width, map_height)
        self.turn_manager = TurnManager()
        self.combat_system = CombatSystem()
        self.path_finder = PathFinder()
        
        self.game_state = "playing"  # playing, paused, game_over
        self.winner: Optional[str] = None
    
    def add_player(self, player_id: str, player_name: str, is_ai: bool = False) -> None:
        """添加玩家"""
        self.turn_manager.add_player(player_id, player_name, is_ai)
        
        # 为玩家创建初始单位
        start_positions = [
            (2, 2), (map_width - 3, map_height - 3),
            (2, map_height - 3), (map_width - 3, 2)
        ]
        
        if len(self.turn_manager.players) <= len(start_positions):
            start_x, start_y = start_positions[len(self.turn_manager.players) - 1]
            
            # 创建初始单位
            infantry = Unit(
                unit_id=f"{player_id}_infantry_1",
                name="步兵",
                unit_type=UnitType.INFANTRY,
                owner=player_id,
                health=100,
                attack=15,
                defense=5,
                movement=3,
                attack_range=1
            )
            
            self.game_map.place_unit(infantry, start_x, start_y)
    
    def move_unit(self, unit_id: str, target_x: int, target_y: int) -> bool:
        """移动单位"""
        if unit_id not in self.game_map.units:
            return False
        
        unit = self.game_map.units[unit_id]
        
        # 检查是否可以移动
        if not unit.can_move():
            return False
        
        # 计算路径
        path = self.path_finder.find_path(
            self.game_map, unit.x, unit.y, target_x, target_y, unit
        )
        
        if not path or len(path) > unit.movement_range + 1:
            return False
        
        # 执行移动
        old_tile = self.game_map.get_tile(unit.x, unit.y)
        if old_tile:
            old_tile.unit = None
            old_tile.is_occupied = False
        
        new_tile = self.game_map.get_tile(target_x, target_y)
        if new_tile:
            new_tile.unit = unit
            new_tile.is_occupied = True
            unit.x = target_x
            unit.y = target_y
            unit.has_moved = True
            return True
        
        return False
    
    def attack_unit(self, attacker_id: str, defender_id: str) -> Optional[Dict]:
        """攻击单位"""
        if attacker_id not in self.game_map.units or defender_id not in self.game_map.units:
            return None
        
        attacker = self.game_map.units[attacker_id]
        defender = self.game_map.units[defender_id]
        
        # 检查是否可以攻击
        if not attacker.can_attack():
            return None
        
        # 检查攻击范围
        distance = abs(attacker.x - defender.x) + abs(attacker.y - defender.y)
        if distance > attacker.attack_range:
            return None
        
        # 执行攻击
        result = self.combat_system.execute_attack(attacker, defender, self.game_map)
        
        attacker.has_attacked = True
        
        # 移除死亡单位
        if result['defender_killed']:
            self.game_map.remove_unit(defender_id)
        
        if result['attacker_killed']:
            self.game_map.remove_unit(attacker_id)
        
        # 检查游戏是否结束
        self.check_game_over()
        
        return result
    
    def check_game_over(self) -> None:
        """检查游戏是否结束"""
        alive_players = set()
        
        for unit in self.game_map.units.values():
            alive_players.add(unit.owner)
        
        if len(alive_players) <= 1:
            self.game_state = "game_over"
            self.winner = list(alive_players)[0] if alive_players else None
    
    def end_turn(self) -> Optional[str]:
        """结束回合"""
        next_player = self.turn_manager.end_turn()
        
        # 重置所有单位状态
        for unit in self.game_map.units.values():
            if unit.owner == next_player:
                unit.reset_turn()
        
        return next_player
    
    def get_game_state(self) -> Dict:
        """获取游戏状态"""
        return {
            'game_state': self.game_state,
            'current_player': self.turn_manager.get_current_player(),
            'turn_number': self.turn_manager.turn_number,
            'winner': self.winner,
            'players': self.turn_manager.players,
            'units': {unit_id: {
                'name': unit.name,
                'owner': unit.owner,
                'level': unit.level,
                'health': unit.health,
                'max_health': unit.max_health,
                'position': (unit.x, unit.y),
                'has_moved': unit.has_moved,
                'has_attacked': unit.has_attacked
            } for unit_id, unit in self.game_map.units.items()}
        }

这个回合制策略游戏引擎展示了Python在复杂游戏逻辑开发方面的能力。它包括了地图系统、单位系统、寻路系统、战斗系统、回合管理等多个子系统。

第7章 网络游戏开发技术

7.1 基于Socket的网络通信

网络游戏开发需要处理网络通信、同步、延迟补偿等复杂问题。Python提供了丰富的网络编程库,使得网络游戏开发变得更加可行。

网络游戏服务器实现

复制代码
import socket
import threading
import json
import time
from typing import Dict, List, Optional, Tuple, Any, Callable
from dataclasses import dataclass, field
from enum import Enum
import queue
import hashlib
import secrets

class PacketType(Enum):
    """数据包类型"""
    CONNECT = "connect"
    DISCONNECT = "disconnect"
    MOVE = "move"
    ATTACK = "attack"
    CHAT = "chat"
    GAME_STATE = "game_state"
    PLAYER_INFO = "player_info"
    HEARTBEAT = "heartbeat"
    ERROR = "error"

@dataclass
class Packet:
    """数据包"""
    packet_type: PacketType
    data: Dict[str, Any]
    timestamp: float = field(default_factory=time.time)
    packet_id: str = field(default_factory=lambda: secrets.token_hex(8))

class NetworkPlayer:
    """网络玩家"""
    
    def __init__(self, player_id: str, socket: socket.socket, address: Tuple[str, int]):
        self.player_id = player_id
        self.socket = socket
        self.address = address
        self.connected = True
        self.last_heartbeat = time.time()
        self.player_data: Dict[str, Any] = {}
        self.send_queue: queue.Queue = queue.Queue()
    
    def send_packet(self, packet: Packet) -> bool:
        """发送数据包"""
        try:
            self.send_queue.put(packet)
            return True
        except Exception as e:
            print(f"发送数据包错误: {e}")
            return False
    
    def disconnect(self) -> None:
        """断开连接"""
        self.connected = False
        try:
            self.socket.close()
        except:
            pass

class GameServer:
    """游戏服务器"""
    
    def __init__(self, host: str = '0.0.0.0', port: int = 9999):
        self.host = host
        self.port = port
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        
        self.players: Dict[str, NetworkPlayer] = {}
        self.running = False
        self.player_counter = 0
        
        # 游戏状态
        self.game_state: Dict[str, Any] = {
            'players': {},
            'game_started': False,
            'current_turn': None,
            'turn_number': 0
        }
        
        # 回调函数
        self.on_player_connect: Optional[Callable] = None
        self.on_player_disconnect: Optional[Callable] = None
        self.on_player_move: Optional[Callable] = None
        self.on_player_attack: Optional[Callable] = None
    
    def start(self) -> None:
        """启动服务器"""
        try:
            self.socket.bind((self.host, self.port))
            self.socket.listen(5)
            self.running = True
            
            print(f"服务器启动在 {self.host}:{self.port}")
            
            # 启动接受连接线程
            accept_thread = threading.Thread(target=self.accept_connections)
            accept_thread.daemon = True
            accept_thread.start()
            
            # 启动心跳检查线程
            heartbeat_thread = threading.Thread(target=self.check_heartbeats)
            heartbeat_thread.daemon = True
            heartbeat_thread.start()
            
        except Exception as e:
            print(f"服务器启动失败: {e}")
    
    def accept_connections(self) -> None:
        """接受客户端连接"""
        while self.running:
            try:
                client_socket, address = self.socket.accept()
                print(f"新连接来自: {address}")
                
                # 创建玩家
                self.player_counter += 1
                player_id = f"player_{self.player_counter}"
                player = NetworkPlayer(player_id, client_socket, address)
                self.players[player_id] = player
                
                # 启动玩家处理线程
                player_thread = threading.Thread(target=self.handle_player, args=(player,))
                player_thread.daemon = True
                player_thread.start()
                
                # 发送连接确认
                connect_packet = Packet(
                    packet_type=PacketType.CONNECT,
                    data={
                        'player_id': player_id,
                        'message': '连接成功'
                    }
                )
                player.send_packet(connect_packet)
                
                # 触发连接回调
                if self.on_player_connect:
                    self.on_player_connect(player_id)
                
            except Exception as e:
                print(f"接受连接错误: {e}")
    
    def handle_player(self, player: NetworkPlayer) -> None:
        """处理玩家消息"""
        while player.connected and self.running:
            try:
                # 接收数据
                data = player.socket.recv(4096)
                if not data:
                    break
                
                # 处理数据包
                packets = self.deserialize_packets(data)
                for packet in packets:
                    self.process_packet(player, packet)
                
                # 发送队列中的数据包
                while not player.send_queue.empty():
                    packet = player.send_queue.get_nowait()
                    serialized_data = self.serialize_packet(packet)
                    player.socket.sendall(serialized_data)
                
            except Exception as e:
                print(f"处理玩家消息错误: {e}")
                break
        
        # 玩家断开连接
        self.player_disconnect(player)
    
    def process_packet(self, player: NetworkPlayer, packet: Packet) -> None:
        """处理数据包"""
        player.last_heartbeat = time.time()
        
        if packet.packet_type == PacketType.HEARTBEAT:
            # 心跳包,更新时间戳
            pass
        
        elif packet.packet_type == PacketType.MOVE:
            # 处理移动
            if self.on_player_move:
                self.on_player_move(player.player_id, packet.data)
        
        elif packet.packet_type == PacketType.ATTACK:
            # 处理攻击
            if self.on_player_attack:
                self.on_player_attack(player.player_id, packet.data)
        
        elif packet.packet_type == PacketType.CHAT:
            # 处理聊天消息
            self.broadcast_chat(player.player_id, packet.data.get('message', ''))
        
        elif packet.packet_type == PacketType.PLAYER_INFO:
            # 更新玩家信息
            player.player_data.update(packet.data)
            self.update_game_state()
    
    def player_disconnect(self, player: NetworkPlayer) -> None:
        """玩家断开连接"""
        player.disconnect()
        
        if player.player_id in self.players:
            del self.players[player.player_id]
        
        print(f"玩家 {player.player_id} 断开连接")
        
        # 触发断开连接回调
        if self.on_player_disconnect:
            self.on_player_disconnect(player.player_id)
        
        # 广播玩家离开消息
        disconnect_packet = Packet(
            packet_type=PacketType.DISCONNECT,
            data={'player_id': player.player_id}
        )
        self.broadcast_packet(disconnect_packet, exclude_player=player.player_id)
    
    def broadcast_packet(self, packet: Packet, exclude_player: str = None) -> None:
        """广播数据包"""
        for player_id, player in self.players.items():
            if exclude_player and player_id == exclude_player:
                continue
            
            player.send_packet(packet)
    
    def send_to_player(self, player_id: str, packet: Packet) -> bool:
        """发送数据包给指定玩家"""
        if player_id in self.players:
            return self.players[player_id].send_packet(packet)
        return False
    
    def broadcast_chat(self, sender_id: str, message: str) -> None:
        """广播聊天消息"""
        chat_packet = Packet(
            packet_type=PacketType.CHAT,
            data={
                'sender_id': sender_id,
                'message': message,
                'timestamp': time.time()
            }
        )
        self.broadcast_packet(chat_packet)
    
    def broadcast_game_state(self) -> None:
        """广播游戏状态"""
        game_state_packet = Packet(
            packet_type=PacketType.GAME_STATE,
            data=self.game_state
        )
        self.broadcast_packet(game_state_packet)
    
    def update_game_state(self) -> None:
        """更新游戏状态"""
        # 更新玩家数据
        for player_id, player in self.players.items():
            self.game_state['players'][player_id] = player.player_data
        
        # 广播新的游戏状态
        self.broadcast_game_state()
    
    def check_heartbeats(self) -> None:
        """检查心跳"""
        while self.running:
            current_time = time.time()
            
            for player_id, player in list(self.players.items()):
                # 如果超过30秒没有心跳,断开连接
                if current_time - player.last_heartbeat > 30:
                    print(f"玩家 {player_id} 超时,断开连接")
                    self.player_disconnect(player)
            
            time.sleep(5)  # 每5秒检查一次
    
    def serialize_packet(self, packet: Packet) -> bytes:
        """序列化数据包"""
        packet_dict = {
            'packet_type': packet.packet_type.value,
            'data': packet.data,
            'timestamp': packet.timestamp,
            'packet_id': packet.packet_id
        }
        return json.dumps(packet_dict).encode('utf-8') + b'\n'
    
    def deserialize_packets(self, data: bytes) -> List[Packet]:
        """反序列化数据包"""
        packets = []
        
        try:
            # 分割数据包(每个数据包以换行符结束)
            packet_strings = data.decode('utf-8').strip().split('\n')
            
            for packet_string in packet_strings:
                if packet_string:
                    packet_dict = json.loads(packet_string)
                    packet = Packet(
                        packet_type=PacketType(packet_dict['packet_type']),
                        data=packet_dict['data'],
                        timestamp=packet_dict.get('timestamp', time.time()),
                        packet_id=packet_dict.get('packet_id', '')
                    )
                    packets.append(packet)
        
        except Exception as e:
            print(f"反序列化数据包错误: {e}")
        
        return packets
    
    def stop(self) -> None:
        """停止服务器"""
        self.running = False
        
        # 断开所有玩家
        for player in list(self.players.values()):
            player.disconnect()
        
        # 关闭服务器套接字
        try:
            self.socket.close()
        except:
            pass
        
        print("服务器已停止")

class GameClient:
    """游戏客户端"""
    
    def __init__(self, server_host: str = 'localhost', server_port: int = 9999):
        self.server_host = server_host
        self.server_port = server_port
        self.socket = None
        self.connected = False
        self.player_id: Optional[str] = None
        
        # 接收队列
        self.receive_queue: queue.Queue = queue.Queue()
        
        # 回调函数
        self.on_connect: Optional[Callable] = None
        self.on_disconnect: Optional[Callable] = None
        self.on_game_state: Optional[Callable] = None
        self.on_chat_message: Optional[Callable] = None
        
        # 接收线程
        self.receive_thread: Optional[threading.Thread] = None
    
    def connect(self) -> bool:
        """连接服务器"""
        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.connect((self.server_host, self.server_port))
            self.connected = True
            
            # 启动接收线程
            self.receive_thread = threading.Thread(target=self.receive_packets)
            self.receive_thread.daemon = True
            self.receive_thread.start()
            
            return True
        
        except Exception as e:
            print(f"连接服务器失败: {e}")
            return False
    
    def disconnect(self) -> None:
        """断开连接"""
        self.connected = False
        
        if self.socket:
            try:
                self.socket.close()
            except:
                pass
        
        if self.receive_thread:
            self.receive_thread.join(timeout=1)
    
    def send_packet(self, packet: Packet) -> bool:
        """发送数据包"""
        if not self.connected or not self.socket:
            return False
        
        try:
            serialized_data = self.serialize_packet(packet)
            self.socket.sendall(serialized_data)
            return True
        
        except Exception as e:
            print(f"发送数据包错误: {e}")
            self.connected = False
            return False
    
    def receive_packets(self) -> None:
        """接收数据包"""
        while self.connected:
            try:
                data = self.socket.recv(4096)
                if not data:
                    break
                
                packets = self.deserialize_packets(data)
                for packet in packets:
                    self.process_packet(packet)
            
            except Exception as e:
                print(f"接收数据包错误: {e}")
                break
        
        # 连接断开
        self.connected = False
        if self.on_disconnect:
            self.on_disconnect()
    
    def process_packet(self, packet: Packet) -> None:
        """处理数据包"""
        if packet.packet_type == PacketType.CONNECT:
            self.player_id = packet.data.get('player_id')
            if self.on_connect:
                self.on_connect(packet.data)
        
        elif packet.packet_type == PacketType.DISCONNECT:
            if self.on_disconnect:
                self.on_disconnect(packet.data)
        
        elif packet.packet_type == PacketType.GAME_STATE:
            if self.on_game_state:
                self.on_game_state(packet.data)
        
        elif packet.packet_type == PacketType.CHAT:
            if self.on_chat_message:
                self.on_chat_message(packet.data)
        
        else:
            # 将其他数据包放入接收队列
            self.receive_queue.put(packet)
    
    def send_move(self, position: Tuple[int, int]) -> bool:
        """发送移动指令"""
        packet = Packet(
            packet_type=PacketType.MOVE,
            data={'position': position}
        )
        return self.send_packet(packet)
    
    def send_attack(self, target_id: str) -> bool:
        """发送攻击指令"""
        packet = Packet(
            packet_type=PacketType.ATTACK,
            data={'target_id': target_id}
        )
        return self.send_packet(packet)
    
    def send_chat(self, message: str) -> bool:
        """发送聊天消息"""
        packet = Packet(
            packet_type=PacketType.CHAT,
            data={'message': message}
        )
        return self.send_packet(packet)
    
    def send_heartbeat(self) -> bool:
        """发送心跳包"""
        packet = Packet(
            packet_type=PacketType.HEARTBEAT,
            data={}
        )
        return self.send_packet(packet)
    
    def serialize_packet(self, packet: Packet) -> bytes:
        """序列化数据包"""
        packet_dict = {
            'packet_type': packet.packet_type.value,
            'data': packet.data,
            'timestamp': packet.timestamp,
            'packet_id': packet.packet_id
        }
        return json.dumps(packet_dict).encode('utf-8') + b'\n'
    
    def deserialize_packets(self, data: bytes) -> List[Packet]:
        """反序列化数据包"""
        packets = []
        
        try:
            packet_strings = data.decode('utf-8').strip().split('\n')
            
            for packet_string in packet_strings:
                if packet_string:
                    packet_dict = json.loads(packet_string)
                    packet = Packet(
                        packet_type=PacketType(packet_dict['packet_type']),
                        data=packet_dict['data'],
                        timestamp=packet_dict.get('timestamp', time.time()),
                        packet_id=packet_dict.get('packet_id', '')
                    )
                    packets.append(packet)
        
        except Exception as e:
            print(f"反序列化数据包错误: {e}")
        
        return packets
    
    def get_received_packet(self) -> Optional[Packet]:
        """获取接收到的数据包"""
        try:
            return self.receive_queue.get_nowait()
        except queue.Empty:
            return None

这个网络游戏基础框架展示了Python在多人游戏开发方面的能力。它包括了服务器客户端架构、数据包序列化、连接管理、心跳检测等基础功能。

第8章 Python游戏开发工具链

8.1 开发环境配置与优化

Python游戏开发需要合适的开发环境和工具配置。Python 3.13带来了许多新的开发工具和性能优化特性。

开发环境配置

复制代码
# 开发环境配置和性能监控工具
import sys
import time
import psutil
import pygame
from typing import Dict, List, Optional, Callable
from dataclasses import dataclass, field
from collections import deque
import tracemalloc
import cProfile
import pstats
import io

@dataclass
class PerformanceMetrics:
    """性能指标"""
    fps: float = 0.0
    frame_time: float = 0.0
    cpu_usage: float = 0.0
    memory_usage: float = 0.0
    draw_calls: int = 0
    entity_count: int = 0
    physics_time: float = 0.0
    render_time: float = 0.0
    logic_time: float = 0.0

class PerformanceMonitor:
    """性能监控器"""
    
    def __init__(self, sample_size: int = 60):
        self.sample_size = sample_size
        self.frame_times: deque = deque(maxlen=sample_size)
        self.fps_history: deque = deque(maxlen=sample_size)
        self.metrics = PerformanceMetrics()
        
        # 性能分析器
        self.enable_profiling = False
        self.profiler = None
        
        # 内存跟踪
        self.enable_memory_tracking = False
        self.memory_snapshots: List = []
    
    def start_frame(self) -> None:
        """开始帧"""
        self.frame_start_time = time.time()
        self.physics_start_time = None
        self.render_start_time = None
        self.logic_start_time = None
    
    def end_frame(self) -> None:
        """结束帧"""
        frame_time = time.time() - self.frame_start_time
        self.frame_times.append(frame_time)
        
        # 计算FPS
        if frame_time > 0:
            fps = 1.0 / frame_time
            self.fps_history.append(fps)
        
        # 更新性能指标
        self.metrics.frame_time = frame_time
        self.metrics.fps = sum(self.fps_history) / len(self.fps_history) if self.fps_history else 0
        self.metrics.cpu_usage = psutil.cpu_percent()
        process = psutil.Process()
        self.metrics.memory_usage = process.memory_info().rss / (1024 * 1024)  # MB
    
    def start_physics(self) -> None:
        """开始物理计算"""
        self.physics_start_time = time.time()
    
    def end_physics(self) -> None:
        """结束物理计算"""
        if self.physics_start_time:
            self.metrics.physics_time = time.time() - self.physics_start_time
    
    def start_render(self) -> None:
        """开始渲染"""
        self.render_start_time = time.time()
    
    def end_render(self) -> None:
        """结束渲染"""
        if self.render_start_time:
            self.metrics.render_time = time.time() - self.render_start_time
    
    def start_logic(self) -> None:
        """开始逻辑计算"""
        self.logic_start_time = time.time()
    
    def end_logic(self) -> None:
        """结束逻辑计算"""
        if self.logic_start_time:
            self.metrics.logic_time = time.time() - self.logic_start_time
    
    def start_profiling(self) -> None:
        """开始性能分析"""
        if not self.enable_profiling:
            self.enable_profiling = True
            self.profiler = cProfile.Profile()
            self.profiler.enable()
    
    def stop_profiling(self) -> str:
        """停止性能分析并返回结果"""
        if self.enable_profiling and self.profiler:
            self.enable_profiling = False
            self.profiler.disable()
            
            # 获取分析结果
            stream = io.StringIO()
            stats = pstats.Stats(self.profiler, stream=stream)
            stats.sort_stats('cumulative')
            stats.print_stats(20)  # 打印前20个函数
            
            return stream.getvalue()
        
        return ""
    
    def start_memory_tracking(self) -> None:
        """开始内存跟踪"""
        if not self.enable_memory_tracking:
            self.enable_memory_tracking = True
            tracemalloc.start()
    
    def take_memory_snapshot(self, label: str = "") -> None:
        """获取内存快照"""
        if self.enable_memory_tracking:
            snapshot = tracemalloc.take_snapshot()
            self.memory_snapshots.append((label, time.time(), snapshot))
    
    def stop_memory_tracking(self) -> None:
        """停止内存跟踪"""
        if self.enable_memory_tracking:
            self.enable_memory_tracking = False
            tracemalloc.stop()
    
    def get_memory_diff(self, snapshot1_index: int, snapshot2_index: int) -> str:
        """获取内存差异"""
        if 0 <= snapshot1_index < len(self.memory_snapshots) and \
           0 <= snapshot2_index < len(self.memory_snapshots):
            
            _, _, snapshot1 = self.memory_snapshots[snapshot1_index]
            _, _, snapshot2 = self.memory_snapshots[snapshot2_index]
            
            top_stats = snapshot2.compare_to(snapshot1, 'lineno')
            result = []
            
            for stat in top_stats[:10]:
                result.append(str(stat))
            
            return "\n".join(result)
        
        return "Invalid snapshot indices"
    
    def get_metrics(self) -> PerformanceMetrics:
        """获取性能指标"""
        return self.metrics
    
    def reset(self) -> None:
        """重置监控器"""
        self.frame_times.clear()
        self.fps_history.clear()
        self.metrics = PerformanceMetrics()

class DeveloperConsole:
    """开发者控制台"""
    
    def __init__(self):
        self.commands: Dict[str, Callable] = {}
        self.command_history: List[str] = []
        self.history_index: int = -1
        self.visible = False
        self.input_text = ""
        self.output_text: List[str] = []
        
        # 注册基础命令
        self.register_command("help", self.cmd_help, "显示帮助信息")
        self.register_command("clear", self.cmd_clear, "清除控制台")
        self.register_command("history", self.cmd_history, "显示命令历史")
        self.register_command("echo", self.cmd_echo, "回显文本")
    
    def register_command(self, command_name: str, command_func: Callable, help_text: str = "") -> None:
        """注册命令"""
        self.commands[command_name] = {
            'function': command_func,
            'help': help_text
        }
    
    def execute_command(self, command: str) -> None:
        """执行命令"""
        command = command.strip()
        if not command:
            return
        
        self.command_history.append(command)
        self.history_index = len(self.command_history)
        
        parts = command.split()
        command_name = parts[0]
        args = parts[1:] if len(parts) > 1 else []
        
        if command_name in self.commands:
            try:
                result = self.commands[command_name]['function'](*args)
                if result:
                    self.output_text.append(f"> {command}")
                    self.output_text.append(str(result))
            except Exception as e:
                self.output_text.append(f"> {command}")
                self.output_text.append(f"错误: {e}")
        else:
            self.output_text.append(f"> {command}")
            self.output_text.append(f"未知命令: {command_name}")
    
    def cmd_help(self, *args) -> str:
        """帮助命令"""
        if not args:
            help_text = "可用命令:\n"
            for cmd_name, cmd_info in self.commands.items():
                help_text += f"  {cmd_name}: {cmd_info['help']}\n"
            return help_text
        else:
            command_name = args[0]
            if command_name in self.commands:
                return f"{command_name}: {self.commands[command_name]['help']}"
            else:
                return f"未知命令: {command_name}"
    
    def cmd_clear(self, *args) -> str:
        """清除命令"""
        self.output_text.clear()
        return "控制台已清除"
    
    def cmd_history(self, *args) -> str:
        """历史命令"""
        if not self.command_history:
            return "没有命令历史"
        
        history_text = "命令历史:\n"
        for i, cmd in enumerate(self.command_history):
            history_text += f"  {i + 1}. {cmd}\n"
        return history_text
    
    def cmd_echo(self, *args) -> str:
        """回显命令"""
        return " ".join(args)
    
    def toggle(self) -> None:
        """切换显示状态"""
        self.visible = not self.visible
    
    def handle_input(self, event: pygame.event.Event) -> None:
        """处理输入事件"""
        if not self.visible:
            return
        
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RETURN:
                if self.input_text:
                    self.execute_command(self.input_text)
                    self.input_text = ""
            
            elif event.key == pygame.K_BACKSPACE:
                self.input_text = self.input_text[:-1]
            
            elif event.key == pygame.K_UP:
                if self.history_index > 0:
                    self.history_index -= 1
                    self.input_text = self.command_history[self.history_index]
            
            elif event.key == pygame.K_DOWN:
                if self.history_index < len(self.command_history) - 1:
                    self.history_index += 1
                    self.input_text = self.command_history[self.history_index]
                else:
                    self.history_index = len(self.command_history)
                    self.input_text = ""
            
            elif event.key == pygame.K_ESCAPE:
                self.visible = False
            
            elif event.unicode and event.unicode.isprintable():
                self.input_text += event.unicode
    
    def draw(self, screen: pygame.Surface, font: pygame.font.Font) -> None:
        """绘制控制台"""
        if not self.visible:
            return
        
        # 控制台背景
        console_height = screen.get_height() // 2
        console_rect = pygame.Rect(0, 0, screen.get_width(), console_height)
        
        # 半透明背景
        console_surface = pygame.Surface((screen.get_width(), console_height), pygame.SRCALPHA)
        console_surface.fill((0, 0, 0, 200))
        screen.blit(console_surface, (0, 0))
        
        # 输入提示
        input_text = f"> {self.input_text}_"
        input_surface = font.render(input_text, True, (255, 255, 255))
        screen.blit(input_surface, (10, console_height - 30))
        
        # 输出文本
        output_lines = self.output_text[-10:]  # 显示最后10行
        for i, line in enumerate(output_lines):
            text_surface = font.render(line, True, (200, 200, 200))
            screen.blit(text_surface, (10, console_height - 60 - i * 25))

class AssetManager:
    """资源管理器"""
    
    def __init__(self):
        self.textures: Dict[str, pygame.Surface] = {}
        self.sounds: Dict[str, pygame.mixer.Sound] = {}
        self.music: Dict[str, str] = {}
        self.fonts: Dict[str, pygame.font.Font] = {}
        
        # 资源路径
        self.base_path = "assets"
        self.texture_path = f"{self.base_path}/textures"
        self.sound_path = f"{self.base_path}/sounds"
        self.music_path = f"{self.base_path}/music"
        self.font_path = f"{self.base_path}/fonts"
    
    def load_texture(self, texture_id: str, filename: str) -> bool:
        """加载纹理"""
        try:
            full_path = f"{self.texture_path}/{filename}"
            texture = pygame.image.load(full_path).convert_alpha()
            self.textures[texture_id] = texture
            return True
        except Exception as e:
            print(f"加载纹理失败 {filename}: {e}")
            return False
    
    def get_texture(self, texture_id: str) -> Optional[pygame.Surface]:
        """获取纹理"""
        return self.textures.get(texture_id)
    
    def load_sound(self, sound_id: str, filename: str) -> bool:
        """加载音效"""
        try:
            full_path = f"{self.sound_path}/{filename}"
            sound = pygame.mixer.Sound(full_path)
            self.sounds[sound_id] = sound
            return True
        except Exception as e:
            print(f"加载音效失败 {filename}: {e}")
            return False
    
    def get_sound(self, sound_id: str) -> Optional[pygame.mixer.Sound]:
        """获取音效"""
        return self.sounds.get(sound_id)
    
    def play_sound(self, sound_id: str, volume: float = 1.0) -> None:
        """播放音效"""
        sound = self.get_sound(sound_id)
        if sound:
            sound.set_volume(volume)
            sound.play()
    
    def load_font(self, font_id: str, filename: str, size: int = 24) -> bool:
        """加载字体"""
        try:
            full_path = f"{self.font_path}/{filename}"
            font = pygame.font.Font(full_path, size)
            self.fonts[font_id] = font
            return True
        except Exception as e:
            print(f"加载字体失败 {filename}: {e}")
            return False
    
    def get_font(self, font_id: str) -> Optional[pygame.font.Font]:
        """获取字体"""
        return self.fonts.get(font_id)
    
    def create_texture(self, texture_id: str, size: tuple, color: tuple) -> pygame.Surface:
        """创建纯色纹理"""
        texture = pygame.Surface(size, pygame.SRCALPHA)
        texture.fill(color)
        self.textures[texture_id] = texture
        return texture
    
    def cleanup(self) -> None:
        """清理资源"""
        self.textures.clear()
        self.sounds.clear()
        self.fonts.clear()

这个游戏开发工具集展示了Python 3.13环境下游戏开发的辅助工具。它包括了性能监控、开发者控制台、资源管理等功能,能够显著提高游戏开发效率。

8.2 调试与测试工具

游戏调试和测试是开发过程中不可缺少的环节。Python提供了丰富的调试和测试工具,可以帮助开发者快速定位和解决问题。

游戏调试系统

复制代码
import pygame
import sys
import logging
from typing import Dict, List, Optional, Tuple, Any, Callable
from dataclasses import dataclass, field
from enum import Enum
import json
import time

class LogLevel(Enum):
    """日志级别"""
    DEBUG = "DEBUG"
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"
    CRITICAL = "CRITICAL"

@dataclass
class LogEntry:
    """日志条目"""
    timestamp: float
    level: LogLevel
    category: str
    message: str
    data: Optional[Dict] = None

class GameDebugger:
    """游戏调试器"""
    
    def __init__(self):
        self.logs: List[LogEntry] = []
        self.max_logs = 1000
        self.log_categories: Dict[str, bool] = {
            'general': True,
            'physics': True,
            'rendering': True,
            'ai': True,
            'network': True,
            'audio': True
        }
        
        # 调试渲染
        self.debug_render_enabled = False
        self.debug_draw_callbacks: List[Callable] = []
        
        # 性能分析
        self.timings: Dict[str, List[float]] = {}
        
        # 断点
        self.breakpoints: Dict[str, List[Callable]] = {}
        
        # 变量监视
        self.watched_variables: Dict[str, Any] = {}
        
        # 配置日志
        self.setup_logging()
    
    def setup_logging(self) -> None:
        """配置日志系统"""
        logging.basicConfig(
            level=logging.DEBUG,
            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('game_debug.log'),
                logging.StreamHandler(sys.stdout)
            ]
        )
    
    def log(self, level: LogLevel, category: str, message: str, data: Dict = None) -> None:
        """记录日志"""
        if not self.log_categories.get(category, True):
            return
        
        entry = LogEntry(
            timestamp=time.time(),
            level=level,
            category=category,
            message=message,
            data=data
        )
        
        self.logs.append(entry)
        
        # 保持日志数量限制
        if len(self.logs) > self.max_logs:
            self.logs = self.logs[-self.max_logs:]
        
        # 输出到标准日志
        log_level = getattr(logging, level.value)
        logging.log(log_level, f"[{category}] {message}")
    
    def debug(self, category: str, message: str, data: Dict = None) -> None:
        """调试日志"""
        self.log(LogLevel.DEBUG, category, message, data)
    
    def info(self, category: str, message: str, data: Dict = None) -> None:
        """信息日志"""
        self.log(LogLevel.INFO, category, message, data)
    
    def warning(self, category: str, message: str, data: Dict = None) -> None:
        """警告日志"""
        self.log(LogLevel.WARNING, category, message, data)
    
    def error(self, category: str, message: str, data: Dict = None) -> None:
        """错误日志"""
        self.log(LogLevel.ERROR, category, message, data)
    
    def critical(self, category: str, message: str, data: Dict = None) -> None:
        """严重错误日志"""
        self.log(LogLevel.CRITICAL, category, message, data)
    
    def start_timing(self, name: str) -> None:
        """开始计时"""
        if name not in self.timings:
            self.timings[name] = []
        self.timings[name].append(time.time())
    
    def end_timing(self, name: str) -> float:
        """结束计时"""
        if name in self.timings and self.timings[name]:
            start_time = self.timings[name].pop()
            elapsed = time.time() - start_time
            return elapsed
        return 0.0
    
    def add_breakpoint(self, name: str, condition: Callable[[], bool]) -> None:
        """添加断点"""
        if name not in self.breakpoints:
            self.breakpoints[name] = []
        self.breakpoints[name].append(condition)
    
    def check_breakpoints(self) -> List[str]:
        """检查断点"""
        triggered = []
        for name, conditions in self.breakpoints.items():
            for condition in conditions:
                try:
                    if condition():
                        triggered.append(name)
                except:
                    pass
        return triggered
    
    def watch_variable(self, name: str, value_getter: Callable) -> None:
        """监视变量"""
        self.watched_variables[name] = value_getter
    
    def get_watched_variables(self) -> Dict[str, Any]:
        """获取监视的变量"""
        result = {}
        for name, getter in self.watched_variables.items():
            try:
                result[name] = getter()
            except:
                result[name] = "Error"
        return result
    
    def add_debug_draw(self, callback: Callable) -> None:
        """添加调试绘制回调"""
        self.debug_draw_callbacks.append(callback)
    
    def render_debug(self, screen: pygame.Surface) -> None:
        """渲染调试信息"""
        if not self.debug_render_enabled:
            return
        
        # 执行调试绘制回调
        for callback in self.debug_draw_callbacks:
            try:
                callback(screen)
            except:
                pass
        
        # 绘制性能信息
        self.render_performance_info(screen)
    
    def render_performance_info(self, screen: pygame.Surface) -> None:
        """渲染性能信息"""
        font = pygame.font.Font(None, 24)
        
        y_offset = 10
        for name, times in self.timings.items():
            if times:
                avg_time = sum(times) / len(times)
                text = f"{name}: {avg_time:.4f}s"
                surface = font.render(text, True, (0, 255, 0))
                screen.blit(surface, (10, y_offset))
                y_offset += 25
    
    def export_logs(self, filename: str) -> bool:
        """导出日志"""
        try:
            log_data = [
                {
                    'timestamp': entry.timestamp,
                    'level': entry.level.value,
                    'category': entry.category,
                    'message': entry.message,
                    'data': entry.data
                }
                for entry in self.logs
            ]
            
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(log_data, f, indent=2, ensure_ascii=False)
            
            return True
        
        except Exception as e:
            self.error('general', f'导出日志失败: {e}')
            return False
    
    def get_logs_by_level(self, level: LogLevel) -> List[LogEntry]:
        """按级别获取日志"""
        return [log for log in self.logs if log.level == level]
    
    def get_logs_by_category(self, category: str) -> List[LogEntry]:
        """按类别获取日志"""
        return [log for log in self.logs if log.category == category]
    
    def clear_logs(self) -> None:
        """清除日志"""
        self.logs.clear()
        self.timings.clear()

class CollisionDebugger:
    """碰撞调试器"""
    
    def __init__(self, debugger: GameDebugger):
        self.debugger = debugger
        self.collision_pairs: List[Tuple[Any, Any]] = []
        self.collision_frames: List[int] = []
        self.frame_counter = 0
        self.show_collision_boxes = False
        self.show_collision_normals = False
        self.show_raycasts = False
        self.raycasts: List[Tuple[Tuple[float, float], Tuple[float, float], bool]] = []
    
    def log_collision(self, obj1: Any, obj2: Any) -> None:
        """记录碰撞"""
        self.collision_pairs.append((obj1, obj2))
        self.collision_frames.append(self.frame_counter)
        self.debugger.debug('collision', f'碰撞: {obj1} <-> {obj2}')
    
    def log_raycast(self, start: Tuple[float, float], end: Tuple[float, float], hit: bool) -> None:
        """记录射线检测"""
        self.raycasts.append((start, end, hit))
    
    def update(self) -> None:
        """更新调试器"""
        self.frame_counter += 1
        
        # 清除过期的碰撞记录
        current_time = time.time()
        self.collision_pairs = [
            pair for pair, frame in zip(self.collision_pairs, self.collision_frames)
            if current_time - frame < 1.0  # 只保留最近1秒的碰撞
        ]
        self.collision_frames = [frame for frame in self.collision_frames if current_time - frame < 1.0]
        
        # 清除过期的射线检测
        if len(self.raycasts) > 100:  # 限制射线检测数量
            self.raycasts = self.raycasts[-100:]
    
    def render_debug(self, screen: pygame.Surface) -> None:
        """渲染调试信息"""
        if not (self.show_collision_boxes or self.show_collision_normals or self.show_raycasts):
            return
        
        # 绘制射线检测
        if self.show_raycasts:
            for start, end, hit in self.raycasts:
                color = (255, 0, 0) if hit else (0, 255, 0)
                pygame.draw.line(screen, color, start, end, 2)
                pygame.draw.circle(screen, color, (int(end[0]), int(end[1])), 5)

class AIDebugger:
    """AI调试器"""
    
    def __init__(self, debugger: GameDebugger):
        self.debugger = debugger
        self.ai_states: Dict[str, Dict] = {}
        self.decision_history: List[Dict] = []
        self.show_ai_paths = False
        self.show_ai_states = False
    
    def update_ai_state(self, ai_id: str, state: Dict) -> None:
        """更新AI状态"""
        self.ai_states[ai_id] = state
    
    def log_decision(self, ai_id: str, decision: str, reasoning: str) -> None:
        """记录AI决策"""
        self.decision_history.append({
            'timestamp': time.time(),
            'ai_id': ai_id,
            'decision': decision,
            'reasoning': reasoning
        })
        
        self.debugger.debug('ai', f'AI {ai_id} 决策: {decision} - {reasoning}')
    
    def get_ai_state(self, ai_id: str) -> Optional[Dict]:
        """获取AI状态"""
        return self.ai_states.get(ai_id)
    
    def render_debug(self, screen: pygame.Surface) -> None:
        """渲染调试信息"""
        if not self.show_ai_states:
            return
        
        font = pygame.font.Font(None, 20)
        
        y_offset = 10
        for ai_id, state in self.ai_states.items():
            state_text = f"AI {ai_id}: {state.get('state', 'unknown')}"
            surface = font.render(state_text, True, (255, 255, 0))
            screen.blit(surface, (screen.get_width() - 200, y_offset))
            y_offset += 25

class SaveLoadManager:
    """存档管理器"""
    
    def __init__(self, debugger: GameDebugger):
        self.debugger = debugger
        self.save_directory = "saves"
        self.auto_save_enabled = True
        self.auto_save_interval = 300  # 5分钟
        self.last_auto_save = 0
    
    def save_game(self, save_data: Dict, slot_name: str = "autosave") -> bool:
        """保存游戏"""
        try:
            import os
            os.makedirs(self.save_directory, exist_ok=True)
            
            save_file = f"{self.save_directory}/{slot_name}.json"
            
            save_data_with_metadata = {
                'metadata': {
                    'timestamp': time.time(),
                    'version': '1.0',
                    'slot_name': slot_name
                },
                'game_data': save_data
            }
            
            with open(save_file, 'w', encoding='utf-8') as f:
                json.dump(save_data_with_metadata, f, indent=2, ensure_ascii=False)
            
            self.debugger.info('general', f'游戏已保存到 {slot_name}')
            return True
        
        except Exception as e:
            self.debugger.error('general', f'保存游戏失败: {e}')
            return False
    
    def load_game(self, slot_name: str = "autosave") -> Optional[Dict]:
        """加载游戏"""
        try:
            save_file = f"{self.save_directory}/{slot_name}.json"
            
            if not os.path.exists(save_file):
                self.debugger.warning('general', f'存档文件不存在: {slot_name}')
                return None
            
            with open(save_file, 'r', encoding='utf-8') as f:
                save_data = json.load(f)
            
            self.debugger.info('general', f'游戏已从 {slot_name} 加载')
            return save_data['game_data']
        
        except Exception as e:
            self.debugger.error('general', f'加载游戏失败: {e}')
            return None
    
    def auto_save(self, game_data: Dict) -> bool:
        """自动保存"""
        current_time = time.time()
        
        if self.auto_save_enabled and current_time - self.last_auto_save >= self.auto_save_interval:
            result = self.save_game(game_data, "autosave")
            if result:
                self.last_auto_save = current_time
            return result
        
        return False
    
    def get_save_slots(self) -> List[str]:
        """获取所有存档槽位"""
        try:
            import os
            if not os.path.exists(self.save_directory):
                return []
            
            save_files = [f for f in os.listdir(self.save_directory) if f.endswith('.json')]
            return [f[:-5] for f in save_files]  # 移除.json扩展名
        
        except Exception as e:
            self.debugger.error('general', f'获取存档列表失败: {e}')
            return []
    
    def delete_save(self, slot_name: str) -> bool:
        """删除存档"""
        try:
            import os
            save_file = f"{self.save_directory}/{slot_name}.json"
            
            if os.path.exists(save_file):
                os.remove(save_file)
                self.debugger.info('general', f'存档已删除: {slot_name}')
                return True
            
            return False
        
        except Exception as e:
            self.debugger.error('general', f'删除存档失败: {e}')
            return False

这个调试和测试系统展示了Python游戏开发中的调试工具链。它包括了日志系统、性能监控、碰撞调试、AI调试、存档管理等功能,能够帮助开发者快速定位和解决游戏开发中的问题。

第9章 总结与展望

9.1 Python游戏开发的优势与局限

Python作为一种高级编程语言,在游戏开发领域既有明显的优势,也存在一些局限性。了解这些特点和Python 3.13的发展趋势,对于选择合适的开发工具和技术路线至关重要。

Python游戏开发的优势分析

优势类别 具体表现 实际应用场景
开发效率 语法简洁,代码量少,快速原型开发 独立游戏开发、游戏概念验证、教学项目
生态丰富 大量成熟的第三方库和框架 2D游戏、数据驱动游戏、工具开发
跨平台 一次编写,多处运行 多平台发布、Web游戏、移动游戏
社区支持 活跃的社区,丰富的学习资源 初学者入门、问题解决、技术分享
集成能力 易于与其他语言和系统集成 游戏工具、服务器开发、数据分析
教学友好 语法简单,概念清晰 游戏开发教育、编程教学、算法演示

Python 3.13在性能方面的改进进一步增强了这些优势。新的解释器优化使得游戏循环执行效率提升了15-20%,这对于实时游戏来说是显著的改进。增强的类型系统也让大型游戏项目的代码维护变得更加容易。

Python游戏开发的局限性分析

局限性类别 具体表现 解决方案
性能限制 解释执行,计算密集型任务效率低 使用PyPy、C扩展、关键模块用Cython
内存占用 对象开销大,内存使用较多 对象池技术、内存管理优化
并发处理 GIL限制多线程性能 使用多进程、异步编程、事件驱动
发布部署 解释型语言,需要运行时环境 打包工具、WebAssembly、云游戏
3D性能 缺乏高性能3D引擎 使用绑定引擎、云渲染、混合开发

尽管存在这些局限性,但通过合理的技术选择和架构设计,大多数Python游戏项目都能够达到预期的性能目标。

9.2 未来发展趋势

Python游戏开发的未来发展趋势主要体现在以下几个方面:

1. 性能优化的持续推进

Python 3.13的性能改进只是开始。未来的Python版本将继续优化解释器性能,特别是在以下方面:

  • 更高效的字节码执行机制
  • 改进的垃圾回收算法
  • 更好的JIT编译支持
  • 优化的数据结构实现

这些改进将使Python在游戏开发中的性能瓶颈进一步减少。

2. 游戏引擎的现代化

现代Python游戏引擎正在朝着以下方向发展:

  • 更好的GPU加速支持
  • 现代化的渲染管线
  • 物理引擎集成
  • 跨平台发布支持
  • WebAssembly支持

3. AI集成与智能化

Python在AI领域的优势将直接影响游戏开发:

  • 机器学习驱动的游戏AI
  • 程序化内容生成
  • 玩家行为分析
  • 智能测试和调试

4. 云游戏与分布式架构

Python的网络编程优势将促进云游戏发展:

  • 低延迟网络通信
  • 分布式游戏逻辑
  • 实时数据同步
  • 可扩展的服务器架构

5. 开发工具链的完善

游戏开发工具链将变得更加完善:

  • 集成开发环境
  • 可视化编辑器
  • 自动化测试框架
  • 性能分析工具

9.3 最佳实践建议

基于Python游戏开发的现状和未来趋势,以下是一些最佳实践建议:

1. 合理选择技术栈

根据项目需求选择合适的技术组合:

复制代码
# 技术栈选择指南
TECH_STACK_GUIDE = {
    '小型2D游戏': {
        '推荐框架': ['Pygame', 'Arcade'],
        '适用场景': ['独立游戏', '教学项目', '快速原型'],
        '性能要求': '中等'
    },
    '中型项目': {
        '推荐框架': ['Pygame + 优化', 'Panda3D'],
        '适用场景': ['商业项目', '复杂游戏机制'],
        '性能要求': '较高'
    },
    '大型项目': {
        '推荐方案': ['混合开发', '核心模块C++ + 逻辑Python'],
        '适用场景': ['商业发行', '高性能要求'],
        '性能要求': '很高'
    },
    '网络游戏': {
        '推荐方案': ['Python服务器 + 客户端', '异步框架'],
        '适用场景': ['多人在线', '实时对战'],
        '性能要求': '网络延迟敏感'
    }
}

2. 架构设计原则

良好的架构设计是成功的关键:

  • 模块化设计,功能解耦
  • 数据驱动,配置化开发
  • 事件驱动,松耦合通信
  • 组件化实体,灵活组合
  • 资源管理,统一加载

3. 性能优化策略

系统的性能优化应该贯穿开发全过程:

  • 性能监控,及时发现问题
  • 算法优化,选择合适的数据结构
  • 内存管理,避免不必要的对象创建
  • 渲染优化,减少绘制调用
  • 网络优化,减少带宽占用

4. 开发流程规范

建立规范的开发流程:

  • 版本控制,代码管理
  • 单元测试,质量保证
  • 持续集成,自动化构建
  • 文档编写,知识共享
  • 代码审查,团队协作

Python游戏开发正在经历一个黄金发展期。随着Python 3.13带来的性能提升和生态系统的不断完善,Python在游戏开发领域的应用将更加广泛。无论是独立开发者还是大型游戏工作室,都能从Python的高效开发能力和丰富的生态系统 中受益。

通过合理的技术选择、良好的架构设计和持续的优化改进,Python完全可以胜任各种类型的游戏开发任务,从简单的文字游戏到复杂的3D网络游戏,Python都展现出了强大的潜力和价值。

相关推荐
smj2302_796826527 小时前
解决leetcode第3801题合并有序列表的最小成本
数据结构·python·算法·leetcode
AI数据皮皮侠7 小时前
中国乡村旅游重点村镇数据
大数据·人工智能·python·深度学习·机器学习
小北方城市网8 小时前
第 11 课:Python 全栈项目进阶与职业发展指南|从项目到职场的无缝衔接(课程终章・进阶篇)
大数据·开发语言·人工智能·python·数据库架构·geo
danyang_Q8 小时前
d2l安装(miniforge+cuda+pytorch)
人工智能·pytorch·python
源码梦想家8 小时前
多语言高性能异步任务队列与实时监控实践:Python、Java、Go、C++实战解析
开发语言·python
百***78759 小时前
Gemini 3.0 Pro与2.5深度对比:技术升级与开发实战指南
开发语言·python·gpt
reasonsummer9 小时前
【教学类-122-01】20260105“折纸-东南西北中”(4个方向文字,9个小图案)
python·通义万相
智航GIS9 小时前
9.6 JSON 基本操作
python·json
@zulnger9 小时前
python 学习笔记(文件读写)
笔记·python·学习
weixin_4624462310 小时前
Python 使用 Tkinter + openpyxl 处理 Excel 文件并显示实时进度条
python·excel·tkinter