PyGame游戏开发助手

0 浏览
0 试用
0 购买
Oct 8, 2025更新

本提示词专为Python游戏开发设计,能够根据用户需求生成结构完整、可运行的PyGame游戏代码。通过智能分析游戏类型、核心玩法和视觉风格等关键要素,提供从基础框架到完整功能的代码实现。提示词采用分步推理机制,确保代码逻辑严谨、注释清晰,支持多种游戏类型的开发需求,包括但不限于射击游戏、平台跳跃、益智解谜等。特别适合编程初学者学习游戏开发基础,也可帮助有经验的开发者快速实现原型设计。输出代码包含完整的游戏循环、事件处理、碰撞检测等核心模块,并遵循PyGame最佳实践规范。

示例1

## 游戏代码概述
- 游戏类型说明
  - 平台跳跃(Platformer),像素风格,单关卡
- 核心功能特点
  - 方向键左右移动,向上键或空格跳跃
  - 收集星星计分
  - 敌人简单巡逻与重力,碰到敌人会复位并统计死亡次数
  - 关卡内使用矩形碰撞(平台、敌人、星星)
  - 收集完所有星星后出口开启,到达出口即关卡结束
  - 结束界面显示:用时、收集数、死亡次数与简单评价,支持重新开始
- 技术实现要点
  - 使用PyGame精灵系统和主循环
  - 基于低分辨率虚拟画面+整数缩放的像素风渲染
  - 轴向分离的矩形碰撞处理
  - 巡逻敌人的边缘检测(前方悬空或撞墙时反向)
  - 关卡字符串解析生成Tile、道具、敌人、玩家与出口

## 完整代码
```python
# 完整的PyGame平台跳跃游戏代码(像素风)
# 需求特性:
# - 方向键移动与跳跃
# - 收集星星
# - 敌人简单巡逻
# - 主循环内处理键盘事件与矩形碰撞
# - 关卡结束显示得分与简单报告提示
# - 像素风格(低分辨率渲染到高分辨率窗口)

import pygame
import sys
from pygame.math import Vector2

# ----------------------------
# 基本设置(像素风低分辨率 + 整数放大)
# ----------------------------
pygame.init()
VIRTUAL_W, VIRTUAL_H = 320, 180      # 虚拟渲染分辨率(像素风)
SCALE = 3                            # 窗口缩放倍数(整数,保证像素清晰)
WIN_W, WIN_H = VIRTUAL_W * SCALE, VIRTUAL_H * SCALE
TILE = 16                            # 瓦片大小(像素)
FPS = 60

# 颜色定义(简洁像素调色)
COL_BG = (25, 25, 35)         # 背景深蓝灰
COL_PLATFORM = (60, 60, 80)   # 平台块深灰
COL_PLAYER = (90, 200, 255)   # 玩家青蓝
COL_PLAYER_OUTLINE = (20, 40, 55)
COL_ENEMY = (240, 90, 90)     # 敌人桃红
COL_STAR = (255, 220, 90)     # 星星黄
COL_STAR_GLOW = (255, 255, 180)
COL_TEXT = (240, 240, 240)    # 文本
COL_DOOR_LOCK = (120, 120, 160)
COL_DOOR_OPEN = (120, 200, 120)

# ----------------------------
# 关卡数据(20列 x 11行)
# 字符说明:
# . 空 | X 平台 | S 星星 | E 敌人 | P 玩家出生点 | D 出口
# ----------------------------
LEVEL_DATA = [
    "....................",
    "...S....XXX....S....",
    "...X.........E......",
    "...........XXX......",
    "..XXX.............S.",
    "...........S........",
    ".P....XXX...........",
    "XXXX.........XXXX...",
    ".....S....E.........",
    ".................D..",
    "XXXXXXXXXXXXXXXXXXXX",
]

ROWS = len(LEVEL_DATA)
COLS = len(LEVEL_DATA[0])

# ----------------------------
# 工具函数:创建简单像素块贴图
# ----------------------------
def make_block_surface(w, h, color, outline=None):
    surf = pygame.Surface((w, h), pygame.SRCALPHA)
    surf.fill(color)
    if outline:
        pygame.draw.rect(surf, outline, surf.get_rect(), 1)
    return surf

def clamp(v, lo, hi):
    return max(lo, min(hi, v))

# ----------------------------
# 精灵类定义
# ----------------------------
class Platform(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = make_block_surface(TILE, TILE, COL_PLATFORM)
        self.rect = self.image.get_rect(topleft=(x, y))


class Star(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        # 星星是小块,加轻微发光边
        self.base = make_block_surface(TILE, TILE, (0, 0, 0, 0))
        pygame.draw.rect(self.base, COL_STAR, pygame.Rect(5, 5, 6, 6))
        pygame.draw.rect(self.base, COL_STAR_GLOW, pygame.Rect(4, 4, 8, 8), 1)
        self.image = self.base.copy()
        self.rect = self.image.get_rect(topleft=(x, y))
        self.t = 0.0

    def update(self, dt):
        # 简单闪烁动画
        self.t += dt
        pulse = (pygame.math.sin(self.t * 6.0) + 1.0) * 0.5  # 0..1
        # 通过Alpha实现闪烁
        self.image = self.base.copy()
        self.image.set_alpha(180 + int(75 * pulse))


class Enemy(pygame.sprite.Sprite):
    def __init__(self, x, y, solids_map):
        super().__init__()
        # 敌人是16x12的小矩形,带描边
        self.image = make_block_surface(TILE, TILE - 4, COL_ENEMY, outline=(80, 20, 20))
        self.rect = self.image.get_rect(midbottom=(x + TILE // 2, y + TILE))
        self.pos = Vector2(self.rect.x, self.rect.y)
        self.vel = Vector2(40.0, 0.0)  # 初速度向右
        self.solids_map = solids_map
        self.gravity = 900.0
        self.speed = 40.0

    def is_solid_at(self, px, py):
        tx = int(px // TILE)
        ty = int(py // TILE)
        if 0 <= tx < COLS and 0 <= ty < ROWS:
            return self.solids_map[ty][tx]
        return False

    def update(self, dt, platforms):
        # 重力
        self.vel.y += self.gravity * dt
        # 水平巡逻:前方阻挡或前方地面缺失则反向
        dir_sign = 1 if self.vel.x >= 0 else -1
        ahead_x = self.rect.centerx + dir_sign * (self.rect.width // 2 + 1)
        foot_y = self.rect.bottom + 1
        # 检查前方是否墙
        if self.is_solid_at(ahead_x, self.rect.centery) or self.is_solid_at(ahead_x, self.rect.top + 1):
            self.vel.x *= -1
        else:
            # 检查前方脚下是否有地面
            if not self.is_solid_at(ahead_x, foot_y):
                self.vel.x *= -1

        # 应用水平位移
        self.pos.x += self.vel.x * dt
        self.rect.x = int(self.pos.x)
        # 水平方向碰撞(防止嵌入墙体)
        hits = pygame.sprite.spritecollide(self, platforms, False)
        for p in hits:
            if self.vel.x > 0:
                self.rect.right = p.rect.left
            elif self.vel.x < 0:
                self.rect.left = p.rect.right
            self.pos.x = self.rect.x
            self.vel.x *= -1  # 撞墙反向

        # 竖直方向
        self.pos.y += self.vel.y * dt
        self.rect.y = int(self.pos.y)
        hits = pygame.sprite.spritecollide(self, platforms, False)
        for p in hits:
            if self.vel.y > 0:
                self.rect.bottom = p.rect.top
            elif self.vel.y < 0:
                self.rect.top = p.rect.bottom
            self.pos.y = self.rect.y
            self.vel.y = 0.0


class Player(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        # 玩家:14x16,有外描边
        self.image = make_block_surface(14, 16, COL_PLAYER, outline=COL_PLAYER_OUTLINE)
        self.rect = self.image.get_rect(midbottom=(x + TILE // 2, y + TILE))
        self.pos = Vector2(self.rect.x, self.rect.y)
        self.vel = Vector2(0.0, 0.0)
        self.move_speed = 110.0
        self.gravity = 900.0
        self.jump_speed = 270.0
        self.on_ground = False
        # 跳跃辅助(容错)
        self.coyote_time = 0.1
        self.coyote_timer = 0.0
        self.jump_buffer_time = 0.12
        self.jump_buffer_timer = 0.0

    def handle_input(self, dt):
        keys = pygame.key.get_pressed()
        left = keys[pygame.K_LEFT]
        right = keys[pygame.K_RIGHT]
        self.vel.x = 0.0
        if left: self.vel.x -= self.move_speed
        if right: self.vel.x += self.move_speed

    def queue_jump(self):
        self.jump_buffer_timer = self.jump_buffer_time

    def update(self, dt, platforms):
        # 输入
        self.handle_input(dt)

        # 更新计时器
        if self.on_ground:
            self.coyote_timer = self.coyote_time
        else:
            self.coyote_timer = max(0.0, self.coyote_timer - dt)
        self.jump_buffer_timer = max(0.0, self.jump_buffer_timer - dt)

        # 跳跃判定(缓冲 + 土狼时间)
        if self.jump_buffer_timer > 0 and self.coyote_timer > 0:
            self.vel.y = -self.jump_speed
            self.on_ground = False
            self.jump_buffer_timer = 0.0

        # 重力
        self.vel.y += self.gravity * dt
        self.vel.y = clamp(self.vel.y, -1000, 1000)

        # X轴移动 + 碰撞
        self.pos.x += self.vel.x * dt
        self.rect.x = int(self.pos.x)
        hits = pygame.sprite.spritecollide(self, platforms, False)
        for p in hits:
            if self.vel.x > 0:
                self.rect.right = p.rect.left
            elif self.vel.x < 0:
                self.rect.left = p.rect.right
            self.pos.x = self.rect.x

        # Y轴移动 + 碰撞
        self.pos.y += self.vel.y * dt
        self.rect.y = int(self.pos.y)
        hits = pygame.sprite.spritecollide(self, platforms, False)
        self.on_ground = False
        for p in hits:
            if self.vel.y > 0:
                self.rect.bottom = p.rect.top
                self.on_ground = True
            elif self.vel.y < 0:
                self.rect.top = p.rect.bottom
            self.pos.y = self.rect.y
            self.vel.y = 0.0

        # 限制在屏幕内(虚拟坐标)
        self.rect.left = max(0, self.rect.left)
        self.rect.right = min(VIRTUAL_W, self.rect.right)
        self.pos.x = self.rect.x

        # 若掉出屏幕底部,交由Game处理复位

class Door(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.closed_img = make_block_surface(TILE, TILE*2, COL_DOOR_LOCK, outline=(50, 50, 70))
        self.open_img = make_block_surface(TILE, TILE*2, COL_DOOR_OPEN, outline=(50, 70, 50))
        self.image = self.closed_img
        self.rect = self.image.get_rect(bottomleft=(x, y + TILE))  # 门两格高,底对齐地面
        self.is_open = False

    def set_open(self, open_state: bool):
        self.is_open = open_state
        self.image = self.open_img if self.is_open else self.closed_img


# ----------------------------
# 游戏主类
# ----------------------------
class Game:
    def __init__(self):
        pygame.display.set_caption("像素平台跳跃 - 收集星星")
        self.screen = pygame.display.set_mode((WIN_W, WIN_H))
        self.canvas = pygame.Surface((VIRTUAL_W, VIRTUAL_H))
        self.clock = pygame.time.Clock()
        self.font = pygame.font.SysFont("arial", 10, bold=False)

        self.running = True
        self.state = "playing"  # "playing" or "finished"

        # 关卡与对象容器
        self.platforms = pygame.sprite.Group()
        self.stars = pygame.sprite.Group()
        self.enemies = pygame.sprite.Group()
        self.all_sprites = pygame.sprite.Group()
        self.door = None
        self.player = None

        # 统计
        self.star_total = 0
        self.star_collected = 0
        self.deaths = 0
        self.time_elapsed = 0.0

        # solids map(用于快速检测瓦片是否为实心)
        self.solids_map = [[False for _ in range(COLS)] for _ in range(ROWS)]

        self.load_level()
        self.reset_runtime_stats()

    def reset_runtime_stats(self):
        self.state = "playing"
        self.star_collected = 0
        self.deaths = 0
        self.time_elapsed = 0.0

    def load_level(self):
        # 清空
        self.platforms.empty()
        self.stars.empty()
        self.enemies.empty()
        self.all_sprites.empty()
        self.door = None
        self.player = None
        # 重建solids map
        for r in range(ROWS):
            for c in range(COLS):
                self.solids_map[r][c] = (LEVEL_DATA[r][c] == 'X')

        # 解析关卡
        spawn_pos = (0, 0)
        door_pos = None

        for r, row in enumerate(LEVEL_DATA):
            for c, ch in enumerate(row):
                x, y = c * TILE, r * TILE
                if ch == 'X':
                    block = Platform(x, y)
                    self.platforms.add(block)
                elif ch == 'S':
                    star = Star(x, y)
                    self.stars.add(star)
                    self.all_sprites.add(star)
                elif ch == 'E':
                    enemy = Enemy(x, y, self.solids_map)
                    self.enemies.add(enemy)
                    self.all_sprites.add(enemy)
                elif ch == 'P':
                    spawn_pos = (x, y)
                elif ch == 'D':
                    door_pos = (x, y)

        self.star_total = len(self.stars)

        # 创建玩家与门
        self.player = Player(*spawn_pos)
        self.all_sprites.add(self.player)
        if door_pos:
            self.door = Door(*door_pos)
            self.all_sprites.add(self.door)

        # 平台需要在独立组中用于碰撞
        # 绘制时两者都画到canvas上

    def respawn_player(self):
        # 找到出生点
        for r, row in enumerate(LEVEL_DATA):
            for c, ch in enumerate(row):
                if ch == 'P':
                    x, y = c * TILE, r * TILE
                    self.player.rect.midbottom = (x + TILE // 2, y + TILE)
                    self.player.pos = Vector2(self.player.rect.x, self.player.rect.y)
                    self.player.vel = Vector2(0, 0)
                    return

    def finish_level(self):
        self.state = "finished"

    def draw_text(self, surf, text, x, y, color=COL_TEXT, align="topleft"):
        img = self.font.render(text, True, color)
        rect = img.get_rect(**{align: (x, y)})
        surf.blit(img, rect)

    def update_play(self, dt):
        self.time_elapsed += dt

        # 处理事件(跳跃按键在事件循环里加缓冲)
        # 移动输入在Player内部处理

        # 更新对象
        for star in self.stars:
            star.update(dt)

        for enemy in self.enemies:
            enemy.update(dt, self.platforms)

        self.player.update(dt, self.platforms)

        # 掉出屏幕底部 -> 复位并计死亡
        if self.player.rect.top > VIRTUAL_H + 50:
            self.deaths += 1
            self.respawn_player()

        # 碰到敌人 -> 复位并计死亡
        if pygame.sprite.spritecollide(self.player, self.enemies, False):
            self.deaths += 1
            self.respawn_player()

        # 收集星星
        hits = pygame.sprite.spritecollide(self.player, self.stars, dokill=True)
        if hits:
            self.star_collected += len(hits)

        # 门状态
        if self.door:
            self.door.set_open(self.star_collected >= self.star_total)
            # 开门后碰撞即通关
            if self.door.is_open and self.player.rect.colliderect(self.door.rect):
                self.finish_level()

    def draw_world(self):
        # 背景
        self.canvas.fill(COL_BG)

        # 绘制平台
        for p in self.platforms:
            self.canvas.blit(p.image, p.rect)

        # 绘制道具与敌人、玩家、门
        # 控制层级:星星 -> 敌人 -> 门 -> 玩家(随意,像素风简单)
        for star in self.stars:
            self.canvas.blit(star.image, star.rect)

        for enemy in self.enemies:
            self.canvas.blit(enemy.image, enemy.rect)

        if self.door:
            self.canvas.blit(self.door.image, self.door.rect)

        self.canvas.blit(self.player.image, self.player.rect)

        # UI
        self.draw_text(self.canvas, f"Stars: {self.star_collected}/{self.star_total}", 4, 4)
        self.draw_text(self.canvas, f"Deaths: {self.deaths}", 4, 16)
        self.draw_text(self.canvas, f"Time: {self.time_elapsed:0.2f}s", 4, 28)

        # 像素放大绘制到窗口
        surf_scaled = pygame.transform.scale(self.canvas, (WIN_W, WIN_H))
        self.screen.blit(surf_scaled, (0, 0))
        pygame.display.flip()

    def draw_finish(self):
        # 背景 + 结束面板
        self.canvas.fill(COL_BG)

        # 简单报告
        total = self.star_total
        got = self.star_collected
        deaths = self.deaths
        time_used = self.time_elapsed

        # 简单评级(仅提示)
        rank = "S"
        if deaths >= 3 or time_used > 60:
            rank = "B"
        elif deaths >= 1 or time_used > 40:
            rank = "A"

        # 面板
        panel = pygame.Surface((VIRTUAL_W - 40, VIRTUAL_H - 60))
        panel.fill((35, 35, 50))
        pygame.draw.rect(panel, (80, 80, 110), panel.get_rect(), 2)

        # 文本内容
        cx = panel.get_width() // 2
        self.draw_text(panel, "关卡完成!", cx, 14, align="midtop")
        self.draw_text(panel, f"用时: {time_used:0.2f} 秒", cx, 38, align="midtop")
        self.draw_text(panel, f"收集: {got}/{total} 颗星星", cx, 54, align="midtop")
        self.draw_text(panel, f"死亡: {deaths} 次", cx, 70, align="midtop")
        self.draw_text(panel, f"评价: {rank}", cx, 86, align="midtop")

        self.draw_text(panel, "按 R 重新开始,Esc 退出", cx, panel.get_height() - 22, color=(200, 220, 255), align="midtop")

        self.canvas.blit(panel, (20, 30))

        surf_scaled = pygame.transform.scale(self.canvas, (WIN_W, WIN_H))
        self.screen.blit(surf_scaled, (0, 0))
        pygame.display.flip()

    def run(self):
        while self.running:
            dt = self.clock.tick(FPS) / 1000.0

            # 事件处理
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False
                elif event.type == pygame.KEYDOWN:
                    if self.state == "playing":
                        if event.key in (pygame.K_UP, pygame.K_SPACE):
                            self.player.queue_jump()
                        if event.key == pygame.K_ESCAPE:
                            self.running = False
                    elif self.state == "finished":
                        if event.key == pygame.K_r:
                            # 重置关卡(重新加载星星与敌人,统计归零)
                            self.load_level()
                            self.reset_runtime_stats()
                        if event.key == pygame.K_ESCAPE:
                            self.running = False

            # 更新与绘制
            if self.state == "playing":
                self.update_play(dt)
                self.draw_world()
            else:
                self.draw_finish()

        pygame.quit()
        sys.exit()


if __name__ == "__main__":
    Game().run()
```

## 代码说明
- 关键函数和类的作用说明
  - Game:游戏主控制器,负责主循环、状态管理(playing/finished)、事件处理、关卡加载、绘制与统计
  - Player:玩家角色,支持左右移动与跳跃,带“土狼时间”和“跳跃缓冲”,进行轴向分离的矩形碰撞
  - Enemy:敌人带重力,沿地面巡逻;遇到墙或前方悬空时反向;与玩家碰撞会触发玩家复位并统计死亡
  - Star:收集物,带简单闪烁动画;玩家碰撞后被移除并加分
  - Platform:静态平台块,用于碰撞
  - Door:出口;在收集完全部星星后开启;玩家接触开启的出口后进入结束界面
  - LEVEL_DATA:关卡字符数组,解析为对象;X为平台,S为星星,E为敌人,P为玩家出生点,D为出口
  - 像素风渲染:先绘制到320x180虚拟画布,再使用整数倍缩放到窗口,保证像素清晰

- 运行要求和依赖说明
  - Python 3.8+(建议)
  - pygame 2.x
  - 运行方式:安装pygame后,直接执行该脚本
    - pip install pygame
    - python game.py

- 可能的扩展和改进建议
  - 多关卡与关卡切换:将LEVEL_DATA列表扩展为多个关卡,完成一关后进入下一关
  - 更完善的敌人交互:例如允许从上方踩敌人让其短暂眩晕,而非直接复位
  - 动画与特效:为玩家、敌人、星星与门添加更丰富的逐帧动画和粒子效果
  - 音效与背景音乐:收集星星、跳跃、通关等加入音效与简单BGM(使用自制或开源音源)
  - 相机与大地图:加入摄像机跟随以支持大尺寸关卡滚动
  - UI完善:添加生命值、暂停菜单、设置选项、可视化按键提示
  - 性能优化:将平台碰撞切分为网格桶(spatial hashing)以支持更大更复杂的关卡

本代码严格遵循矩形碰撞与主循环最佳实践,逻辑清晰、即开即玩,适合学习与原型开发。

示例2

## 游戏代码概述
- 游戏类型说明
  - 俯视角“霓虹能量”射击(非暴力表现):玩家发射光子脉冲去“标记/关闭”几何体无人机(两种类型),更强调技巧和机制对比。
- 核心功能特点
  - WASD移动,鼠标指向瞄准,左键连发光子脉冲
  - 按 T 切换两种射击机制(A/B 对比)
    - A:反弹模式(脉冲可在墙面反弹,击中目标后消失)
    - B:穿透模式(脉冲可穿透多个目标,不在墙面反弹)
  - 两种敌人
    - Seeker:持续追踪玩家
    - Dasher:墙面弹射移动并周期性朝玩家加速突进
  - 波次与计时
    - 显示累计时间、当前波次、击中计分
    - 波次随时间或清场自动推进,数量与速度逐步增加
  - 简约霓虹视觉:黑底+加法混合的发光圆与线条
- 技术实现要点
  - 使用 pygame.sprite 精灵与 collide_circle 实现圆形碰撞
  - 自定义“霓虹光晕”缓存,减少重复绘制开销
  - 子弹内置命中冷却,避免穿透模式多帧重复伤害同一目标
  - 场景管理:主循环、波次管理、对象组更新与渲染分离

## 完整代码
```python
# -*- coding: utf-8 -*-
# 霓虹俯视射击(非暴力) - PyGame 单文件可运行示例
# 控制:
# - 移动:WASD
# - 射击:鼠标左键
# - 切换模式:T(反弹/穿透)
# - 退出:ESC 或 关闭窗口
#
# 依赖:pygame
# 安装:pip install pygame
# 运行:python neon_topdown.py

import math
import random
import sys
import pygame

# -----------------------
# 全局配置
# -----------------------
WIDTH, HEIGHT = 960, 600
FPS = 60

BG_COLOR = (5, 5, 10)  # 深色底
NEON_CYAN = (0, 255, 255)
NEON_MAGENTA = (255, 0, 200)
NEON_YELLOW = (255, 255, 0)
NEON_WHITE = (255, 255, 255)
NEON_GREEN = (0, 255, 160)

PLAYER_COLOR = NEON_CYAN
SEEKER_COLOR = NEON_MAGENTA
DASHER_COLOR = NEON_YELLOW
BULLET_COLOR = NEON_WHITE

from pygame.math import Vector2 as Vec2


# -----------------------
# 霓虹绘制与缓存
# -----------------------
class NeonCache:
    circle_cache = {}  # key: (color, radius, glow), value: Surface

    @staticmethod
    def get_glow_circle(color, radius, glow=10):
        key = (color, radius, glow)
        if key in NeonCache.circle_cache:
            return NeonCache.circle_cache[key]

        size = (radius + glow) * 2 + 2
        surf = pygame.Surface((size, size), pygame.SRCALPHA)
        cx, cy = size // 2, size // 2

        # 多层圆模拟光晕(从外到内,透明度递增,半径递减)
        layers = max(4, glow // 2)
        for i in range(layers, 0, -1):
            # 插值半径与透明度
            ri = radius + int(glow * (i / layers))
            alpha = int(30 * (i / layers))  # 外圈更淡
            col = (color[0], color[1], color[2], alpha)
            pygame.draw.circle(surf, col, (cx, cy), ri)

        # 最内层实体圆
        pygame.draw.circle(surf, (*color, 240), (cx, cy), radius)

        NeonCache.circle_cache[key] = surf
        return surf


def blit_neon_circle(surface, pos, color, radius, glow=10):
    surf = NeonCache.get_glow_circle(color, radius, glow)
    rect = surf.get_rect(center=(int(pos[0]), int(pos[1])))
    # 使用加法混合让光叠加
    surface.blit(surf, rect, special_flags=pygame.BLEND_ADD)


def draw_neon_line(surface, color, start, end, width=2, glow=6):
    # 用一个临时透明层绘制多层线条,再以加法混合叠加
    temp = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
    layers = max(3, glow // 2)
    for i in range(layers, 0, -1):
        w = width + i * 2
        alpha = int(40 * (i / layers))
        col = (color[0], color[1], color[2], alpha)
        pygame.draw.line(temp, col, start, end, w)
    # 实体线
    pygame.draw.line(temp, (*color, 240), start, end, width)
    surface.blit(temp, (0, 0), special_flags=pygame.BLEND_ADD)


# -----------------------
# 工具函数
# -----------------------
def clamp(value, a, b):
    return max(a, min(b, value))


def random_pos_away_from(center: Vec2, min_dist=120):
    for _ in range(64):
        x = random.uniform(40, WIDTH - 40)
        y = random.uniform(40, HEIGHT - 40)
        p = Vec2(x, y)
        if p.distance_to(center) >= min_dist:
            return p
    return Vec2(random.uniform(80, WIDTH - 80), random.uniform(80, HEIGHT - 80))


# -----------------------
# 精灵基类与实体
# -----------------------
class GlowSprite(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        # 用于 collide_circle
        self.radius = 10
        self.image = pygame.Surface((2 * self.radius, 2 * self.radius), pygame.SRCALPHA)
        self.rect = self.image.get_rect()

    def draw(self, surface):
        pass


class Player(GlowSprite):
    def __init__(self, pos: Vec2):
        super().__init__()
        self.pos = Vec2(pos)
        self.vel = Vec2(0, 0)
        self.speed = 280
        self.radius = 14
        self.image = pygame.Surface((2 * self.radius, 2 * self.radius), pygame.SRCALPHA)
        self.rect = self.image.get_rect(center=self.pos)
        self.color = PLAYER_COLOR

        self.shoot_cooldown = 0.12  # 连发频率
        self.shoot_timer = 0.0
        self.bullet_mode = "bounce"  # "bounce" or "pierce"

    def toggle_mode(self):
        self.bullet_mode = "pierce" if self.bullet_mode == "bounce" else "bounce"

    def handle_input(self, dt):
        keys = pygame.key.get_pressed()
        move = Vec2(0, 0)
        if keys[pygame.K_w]:
            move.y -= 1
        if keys[pygame.K_s]:
            move.y += 1
        if keys[pygame.K_a]:
            move.x -= 1
        if keys[pygame.K_d]:
            move.x += 1
        if move.length_squared() > 0:
            move = move.normalize()
        self.vel = move * self.speed
        self.pos += self.vel * dt
        self.pos.x = clamp(self.pos.x, 20, WIDTH - 20)
        self.pos.y = clamp(self.pos.y, 20, HEIGHT - 20)
        self.rect.center = (int(self.pos.x), int(self.pos.y))

    def try_shoot(self, dt, bullets_group):
        self.shoot_timer -= dt
        if self.shoot_timer > 0:
            return

        mouse_buttons = pygame.mouse.get_pressed(num_buttons=3)
        if mouse_buttons[0]:
            mouse_pos = Vec2(pygame.mouse.get_pos())
            dir_vec = mouse_pos - self.pos
            if dir_vec.length_squared() == 0:
                return
            direction = dir_vec.normalize()
            # 发射位置略微离开玩家,避免与自身碰撞
            spawn_pos = self.pos + direction * (self.radius + 10)
            speed = 520
            if self.bullet_mode == "bounce":
                bullets_group.add(Bullet(spawn_pos, direction * speed, mode="bounce", bounces=3))
            else:
                bullets_group.add(Bullet(spawn_pos, direction * speed, mode="pierce"))
            self.shoot_timer = self.shoot_cooldown

    def update(self, dt, bullets_group):
        self.handle_input(dt)
        self.try_shoot(dt, bullets_group)

    def draw(self, surface):
        # 玩家主体
        blit_neon_circle(surface, self.pos, self.color, self.radius, glow=10)
        # 指向线(短线)
        mouse_pos = Vec2(pygame.mouse.get_pos())
        dir_vec = mouse_pos - self.pos
        if dir_vec.length_squared() > 0:
            direction = dir_vec.normalize()
            a = self.pos + direction * (self.radius + 4)
            b = self.pos + direction * 28
            draw_neon_line(surface, NEON_CYAN, a, b, width=2, glow=8)
        # 鼠标瞄准目标圈
        blit_neon_circle(surface, mouse_pos, (80, 200, 255), 4, glow=6)


class Bullet(GlowSprite):
    def __init__(self, pos: Vec2, velocity: Vec2, mode="bounce", bounces=3):
        super().__init__()
        self.pos = Vec2(pos)
        self.vel = Vec2(velocity)
        self.mode = mode  # "bounce" or "pierce"
        self.radius = 4
        self.image = pygame.Surface((2 * self.radius, 2 * self.radius), pygame.SRCALPHA)
        self.rect = self.image.get_rect(center=self.pos)
        self.color = BULLET_COLOR

        self.lifetime = 4.0 if mode == "bounce" else 2.6
        self.bounces_left = bounces if mode == "bounce" else 0
        self.hit_cooldowns = {}  # enemy_id -> remaining_time (用于穿透模式的多帧重复命中抑制)

    def can_hit(self, enemy):
        eid = id(enemy)
        return self.hit_cooldowns.get(eid, 0) <= 0

    def register_hit(self, enemy, cooldown=0.12):
        eid = id(enemy)
        self.hit_cooldowns[eid] = cooldown

    def update(self, dt):
        self.lifetime -= dt
        if self.lifetime <= 0:
            self.kill()
            return

        # 冷却计时更新
        if self.hit_cooldowns:
            to_del = []
            for k in list(self.hit_cooldowns.keys()):
                self.hit_cooldowns[k] -= dt
                if self.hit_cooldowns[k] <= 0:
                    to_del.append(k)
            for k in to_del:
                del self.hit_cooldowns[k]

        # 移动
        self.pos += self.vel * dt

        # 边界处理
        if self.mode == "bounce":
            bounced = False
            if self.pos.x <= self.radius or self.pos.x >= WIDTH - self.radius:
                self.vel.x *= -1
                self.pos.x = clamp(self.pos.x, self.radius, WIDTH - self.radius)
                bounced = True
            if self.pos.y <= self.radius or self.pos.y >= HEIGHT - self.radius:
                self.vel.y *= -1
                self.pos.y = clamp(self.pos.y, self.radius, HEIGHT - self.radius)
                bounced = True
            if bounced:
                self.bounces_left -= 1
                if self.bounces_left < 0:
                    self.kill()
                    return
        else:
            # 穿透模式越界直接销毁
            if (self.pos.x < -20 or self.pos.x > WIDTH + 20 or
                self.pos.y < -20 or self.pos.y > HEIGHT + 20):
                self.kill()
                return

        self.rect.center = (int(self.pos.x), int(self.pos.y))

    def draw(self, surface):
        blit_neon_circle(surface, self.pos, self.color, self.radius, glow=6)


class Enemy(GlowSprite):
    def __init__(self, pos: Vec2, color, radius=12, health=2):
        super().__init__()
        self.pos = Vec2(pos)
        self.color = color
        self.radius = radius
        self.health = health
        self.image = pygame.Surface((2 * self.radius, 2 * self.radius), pygame.SRCALPHA)
        self.rect = self.image.get_rect(center=self.pos)
        self.hit_flash = 0.0

    def take_hit(self, dmg=1):
        self.health -= dmg
        self.hit_flash = 0.12
        if self.health <= 0:
            self.kill()
            return True
        return False

    def update(self, dt, player_pos: Vec2):
        pass

    def draw(self, surface):
        # 击中闪烁叠加绿色
        base_color = self.color
        if self.hit_flash > 0:
            self.hit_flash -= 0.016
            flash_color = (
                min(255, base_color[0] + 40),
                min(255, base_color[1] + 80),
                min(255, base_color[2] + 40),
            )
            blit_neon_circle(surface, self.pos, flash_color, self.radius + 1, glow=10)
        blit_neon_circle(surface, self.pos, base_color, self.radius, glow=10)


class Seeker(Enemy):
    def __init__(self, pos: Vec2, wave=1):
        # 追踪者:速度随波次略增,血量随波次每隔几波+1
        base_speed = 70 + wave * 6
        health = 2 + (wave // 4)
        super().__init__(pos, SEEKER_COLOR, radius=12, health=health)
        self.speed = base_speed

    def update(self, dt, player_pos: Vec2):
        dir_vec = player_pos - self.pos
        if dir_vec.length_squared() > 1:
            self.pos += dir_vec.normalize() * self.speed * dt
            self.pos.x = clamp(self.pos.x, self.radius, WIDTH - self.radius)
            self.pos.y = clamp(self.pos.y, self.radius, HEIGHT - self.radius)
            self.rect.center = (int(self.pos.x), int(self.pos.y))


class Dasher(Enemy):
    def __init__(self, pos: Vec2, wave=1):
        # 弹射者:平时匀速移动,撞墙弹射;周期性朝玩家加速
        base_speed = 110 + wave * 6
        dash_speed = 260 + wave * 8
        health = 2 + (wave // 5)
        super().__init__(pos, DASHER_COLOR, radius=12, health=health)
        angle = random.uniform(0, math.tau)
        self.vel = Vec2(math.cos(angle), math.sin(angle)) * base_speed
        self.base_speed = base_speed
        self.dash_speed = dash_speed
        self.dash_cooldown = random.uniform(1.4, 2.1)
        self.dash_timer = self.dash_cooldown

    def update(self, dt, player_pos: Vec2):
        # 定时指向玩家的冲刺
        self.dash_timer -= dt
        if self.dash_timer <= 0:
            dir_vec = player_pos - self.pos
            if dir_vec.length_squared() > 1:
                self.vel = dir_vec.normalize() * self.dash_speed
            self.dash_timer = self.dash_cooldown

        # 逐步回落到基础速度(避免一直超高速)
        if self.vel.length() > self.base_speed:
            self.vel.scale_to_length(max(self.base_speed, self.vel.length() - 120 * dt))

        # 移动与墙面弹射
        self.pos += self.vel * dt
        bounced = False
        if self.pos.x <= self.radius or self.pos.x >= WIDTH - self.radius:
            self.vel.x *= -1
            self.pos.x = clamp(self.pos.x, self.radius, WIDTH - self.radius)
            bounced = True
        if self.pos.y <= self.radius or self.pos.y >= HEIGHT - self.radius:
            self.vel.y *= -1
            self.pos.y = clamp(self.pos.y, self.radius, HEIGHT - self.radius)
            bounced = True
        self.rect.center = (int(self.pos.x), int(self.pos.y))

        # 弹墙微弱加速感
        if bounced and self.vel.length() < self.base_speed * 1.2:
            self.vel *= 1.05


class Particle(GlowSprite):
    def __init__(self, pos: Vec2, color, radius=10, life=0.3):
        super().__init__()
        self.pos = Vec2(pos)
        self.color = color
        self.radius = radius
        self.life = life
        self.image = pygame.Surface((2, 2), pygame.SRCALPHA)
        self.rect = self.image.get_rect(center=self.pos)
        # 随机运动
        ang = random.uniform(0, math.tau)
        self.vel = Vec2(math.cos(ang), math.sin(ang)) * random.uniform(20, 80)

    def update(self, dt):
        self.life -= dt
        if self.life <= 0:
            self.kill()
            return
        self.pos += self.vel * dt
        self.vel *= (1 - 2.2 * dt)  # 快速阻尼
        self.rect.center = (int(self.pos.x), int(self.pos.y))

    def draw(self, surface):
        alpha = max(0, min(255, int(255 * (self.life / 0.3))))
        col = (self.color[0], self.color[1], self.color[2], alpha)
        # 临时绘制,粒子较小可直接简单画
        temp = pygame.Surface((16, 16), pygame.SRCALPHA)
        pygame.draw.circle(temp, col, (8, 8), max(1, int(self.radius * (self.life / 0.3))))
        surface.blit(temp, (self.pos.x - 8, self.pos.y - 8), special_flags=pygame.BLEND_ADD)


# -----------------------
# 游戏主类(循环、波次、碰撞、渲染)
# -----------------------
class Game:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption("Neon Top-Down (A/B 模式对比)")
        self.clock = pygame.time.Clock()

        self.font = pygame.font.SysFont("Consolas", 20)
        if self.font is None:
            self.font = pygame.font.SysFont(None, 20)

        # 精灵组
        self.all_sprites = pygame.sprite.Group()
        self.enemies = pygame.sprite.Group()
        self.bullets = pygame.sprite.Group()
        self.particles = pygame.sprite.Group()

        # 玩家
        self.player = Player(Vec2(WIDTH / 2, HEIGHT / 2))
        self.all_sprites.add(self.player)

        # 计分与时间
        self.score = 0
        self.elapsed = 0.0

        # 波次管理
        self.wave = 0
        self.wave_timer = 0.0
        self.wave_duration = 18.0  # 每波基准时长(也可清场提前进入下一波)
        self.start_next_wave()

    def start_next_wave(self):
        self.wave += 1
        self.wave_timer = 0.0
        # 根据波次生成敌人
        # 数量与难度递增
        num_seekers = 3 + self.wave // 1
        num_dashers = max(1, self.wave // 2)

        # 稍微随机
        num_seekers += random.randint(0, 1)
        num_dashers += random.randint(0, 1)

        for _ in range(num_seekers):
            pos = random_pos_away_from(self.player.pos, min_dist=180)
            e = Seeker(pos, wave=self.wave)
            self.enemies.add(e)
            self.all_sprites.add(e)
        for _ in range(num_dashers):
            pos = random_pos_away_from(self.player.pos, min_dist=200)
            e = Dasher(pos, wave=self.wave)
            self.enemies.add(e)
            self.all_sprites.add(e)

    def spawn_hit_particles(self, pos, base_color):
        for _ in range(6):
            self.particles.add(Particle(pos, base_color, radius=6, life=0.25))

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.quit()
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    self.quit()
                elif event.key == pygame.K_t:
                    self.player.toggle_mode()

    def update(self, dt):
        self.elapsed += dt
        self.wave_timer += dt

        # 更新玩家和子弹
        self.player.update(dt, self.bullets)
        for b in list(self.bullets):
            b.update(dt)

        # 更新敌人
        for e in list(self.enemies):
            e.update(dt, self.player.pos)

        # 更新粒子
        for p in list(self.particles):
            p.update(dt)

        # 碰撞:子弹 vs 敌人(圆碰撞)
        collisions = pygame.sprite.groupcollide(self.bullets, self.enemies, False, False, collided=pygame.sprite.collide_circle)
        for bullet, enemy_list in collisions.items():
            for enemy in enemy_list:
                if bullet.mode == "pierce":
                    if not bullet.can_hit(enemy):
                        continue
                    destroyed = enemy.take_hit(1)
                    bullet.register_hit(enemy)
                    self.spawn_hit_particles(enemy.pos, NEON_GREEN)
                    if destroyed:
                        self.score += 1
                else:
                    # 反弹模式:命中即销毁子弹
                    destroyed = enemy.take_hit(1)
                    self.spawn_hit_particles(enemy.pos, NEON_GREEN)
                    bullet.kill()
                    if destroyed:
                        self.score += 1

        # 波次推进:清场或时间到
        if (len(self.enemies) == 0 and self.wave_timer > 1.0) or self.wave_timer >= self.wave_duration:
            self.start_next_wave()

    def draw_grid(self, surface):
        # 轻微网格,增强科技感
        step = 40
        grid_color = (20, 20, 30, 40)
        temp = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
        for x in range(0, WIDTH, step):
            pygame.draw.line(temp, grid_color, (x, 0), (x, HEIGHT), 1)
        for y in range(0, HEIGHT, step):
            pygame.draw.line(temp, grid_color, (0, y), (WIDTH, y), 1)
        surface.blit(temp, (0, 0))

    def draw_ui(self, surface):
        mode_text = f"模式: {'反弹(A)' if self.player.bullet_mode=='bounce' else '穿透(B)'}  [T 切换]"
        time_text = f"时间: {self.elapsed:.1f}s"
        wave_text = f"波次: {self.wave}"
        score_text = f"得分: {self.score}"

        def draw_label(txt, x, y, color=NEON_CYAN):
            surf = self.font.render(txt, True, color)
            # 轻微光晕
            glow = self.font.render(txt, True, (min(255, color[0]//2+60), min(255, color[1]//2+60), min(255, color[2]//2+60)))
            surface.blit(glow, (x-1, y-1))
            surface.blit(glow, (x+1, y+1))
            surface.blit(surf, (x, y))

        draw_label(time_text, 14, 10, NEON_CYAN)
        draw_label(wave_text, WIDTH - 160, 10, NEON_YELLOW)
        draw_label(score_text, 14, 36, NEON_GREEN)
        draw_label(mode_text, 14, HEIGHT - 32, NEON_WHITE)

    def draw(self):
        self.screen.fill(BG_COLOR)
        self.draw_grid(self.screen)

        # 绘制顺序:粒子(底层)-> 敌人 -> 子弹 -> 玩家
        for p in self.particles:
            p.draw(self.screen)
        for e in self.enemies:
            e.draw(self.screen)
        for b in self.bullets:
            b.draw(self.screen)
        self.player.draw(self.screen)

        self.draw_ui(self.screen)

        pygame.display.flip()

    def quit(self):
        pygame.quit()
        sys.exit(0)

    def run(self):
        while True:
            dt = self.clock.tick(FPS) / 1000.0
            self.handle_events()
            self.update(dt)
            self.draw()


def main():
    Game().run()


if __name__ == "__main__":
    main()
```

## 代码说明
- 关键函数和类的作用说明
  - NeonCache / blit_neon_circle / draw_neon_line:构建霓虹风格的发光绘制,使用加法混合模拟光晕,并做了圆形的缓存避免重复生成。
  - Player:玩家实体,WASD移动、鼠标瞄准,左键发射光子脉冲;用 T 切换反弹/穿透两种发射机制。
  - Bullet:子弹(光子脉冲),支持反弹和穿透模式;穿透模式带命中冷却,防止同一敌人多帧重复伤害。
  - Seeker / Dasher:两种敌人,分别为追踪型和弹射突进型;参数随波次提升(速度、生命),用于快速测试迭代。
  - Particle:被命中后的轻量粒子效果,增强反馈。
  - Game:主循环(事件-更新-渲染)、波次管理、碰撞检测、UI绘制(时间、波次、模式、得分)。

- 运行要求和依赖说明
  - Python 3.8+(建议)
  - 第三方库:pygame
    - 安装:pip install pygame
  - 运行:python neon_topdown.py

- 可能的扩展和改进建议
  - 增加敌人种类(例如护盾型、分裂型),或加入无碰撞幽灵型增强多样性。
  - 扩展更多 A/B 测试机制,如不同的射速、子弹速度/尺寸、命中反馈强度等,以便教学或玩法验证。
  - 增加玩家护盾与生命系统、敌人生成的“安全区提示圈”等,便于更长时间的生存挑战。
  - 增加音效(射击、命中、波次开始),可用纯代码合成短音或加载免版权音效。
  - 对霓虹绘制进行进一步优化:将线条也做局部缓存(或使用更小的局部临时 Surface),在移动端/低功耗设备上保持更高帧率。

示例3

## 游戏代码概述
- 游戏类型说明
  - 益智解谜(推箱子核心玩法)。玩家在网格地图中推动箱子至目标点,完成关卡。提供简单/中等/困难三档难度与计时显示。

- 核心功能特点
  - 三档难度:内置参数控制地图尺寸、箱子数量与默认障碍密度。
  - 计时与统计:显示用时、步数、撤销次数。
  - 课堂即改参数(实时调整,无需重启):
    - 步数上限([ 与 ] 调整)
    - 撤销次数上限(; 与 ' 调整)
    - 障碍密度(, 与 . 调整,用于随机生成新关)
  - 关卡加载与自动验证:
    - 支持文本关卡文件拖拽(.txt)即加载
    - 自动验证:结构合法性、基础死锁检测,以及启发式 BFS 推动作求解验证(小规模关卡可验证可解)
  - 随机关卡生成:按难度与障碍密度随机生成,自动多次尝试直至找到可解关卡或耗尽次数
  - 撤销/重做:Z 撤销,Y 重做(受限于撤销上限)
  - 事件队列与优化示范:
    - 使用事件过滤(set_allowed / set_blocked)
    - 自定义定时事件刷新计时
    - 脏矩形更新(Dirty Rects),预渲染静态层(背景/墙/目标),仅重绘变动单元
  - 视觉风格(无版权资源):内置主题可切换(1=Pastel,2=Neon,3=Mono)。若用户输入 #{visual_style} 未指定,将默认 Pastel。

- 技术实现要点
  - 采用网格地图与集合结构(walls/goals/boxes)实现快速查询
  - 使用“宏推”BFS:以“推箱动作”为边,只在玩家可到达相邻位置时尝试推箱,显著缩小状态空间
  - 死锁检测:箱子陷于非目标角落立即剪枝
  - 预渲染与脏矩形更新,降低逐帧渲染成本
  - 事件队列过滤与自定义计时事件,减少无关事件开销

## 完整代码
```python
# 完整的PyGame游戏代码
# 推箱子(Sokoban)教学版:三档难度、计时、参数即改、随机生成、关卡加载与验证、事件队列与渲染优化
# 依赖:pygame
# 运行:python this_file.py

import os
import sys
import time
import random
from collections import deque
import pygame

# ---------------------------
# 全局设置与常量
# ---------------------------
pygame.init()
pygame.display.set_caption("推箱子教学版 - 难度&计时&参数即改&验证&优化示范")

# 事件类型(自定义)
TICK_EVENT = pygame.USEREVENT + 1   # 计时每秒刷新
PERF_EVENT = pygame.USEREVENT + 2   # 性能统计更新

# 允许的事件类型:减少事件队列冗余(优化示范)
pygame.event.set_blocked(None)
pygame.event.set_allowed([pygame.QUIT, pygame.KEYDOWN, pygame.KEYUP,
                          pygame.VIDEORESIZE, pygame.WINDOWFOCUSGAINED, pygame.WINDOWFOCUSLOST,
                          pygame.DROPFILE, TICK_EVENT, PERF_EVENT])

# 屏幕与帧率
BASE_WIDTH, BASE_HEIGHT = 900, 700
FLAGS = pygame.RESIZABLE | pygame.SCALED
screen = pygame.display.set_mode((BASE_WIDTH, BASE_HEIGHT), FLAGS)
clock = pygame.time.Clock()
TARGET_FPS = 60

# 网格绘制相关
MARGIN = 10  # 画布边距像素

# ---------------------------
# 主题与颜色
# ---------------------------
def theme_pastel():
    return {
        "bg": (248, 248, 255),
        "wall": (180, 200, 230),
        "goal": (255, 210, 120),
        "floor": (240, 240, 240),
        "box": (255, 160, 160),
        "box_on_goal": (180, 230, 180),
        "player": (140, 180, 255),
        "grid": (220, 220, 220),
        "text": (40, 40, 40),
        "hud_bg": (255, 255, 255, 180),
    }

def theme_neon():
    return {
        "bg": (10, 10, 15),
        "wall": (35, 50, 65),
        "goal": (255, 85, 0),
        "floor": (25, 25, 35),
        "box": (0, 200, 255),
        "box_on_goal": (0, 255, 120),
        "player": (255, 0, 155),
        "grid": (55, 55, 80),
        "text": (230, 230, 240),
        "hud_bg": (20, 20, 30, 190),
    }

def theme_mono():
    return {
        "bg": (250, 250, 250),
        "wall": (110, 110, 110),
        "goal": (80, 80, 80),
        "floor": (230, 230, 230),
        "box": (150, 150, 150),
        "box_on_goal": (100, 100, 100),
        "player": (50, 50, 50),
        "grid": (200, 200, 200),
        "text": (10, 10, 10),
        "hud_bg": (255, 255, 255, 180),
    }

THEMES = [theme_pastel(), theme_neon(), theme_mono()]
THEME_NAMES = ["Pastel", "Neon", "Mono"]

# ---------------------------
# 配置(可课堂即改)
# ---------------------------
class Config:
    def __init__(self):
        self.difficulty = "easy"  # "easy" | "medium" | "hard"
        # 三档缺省参数(仅用于随机生成时提供建议)
        self.diff_params = {
            "easy":   {"w": 9, "h": 9, "boxes": 2, "density": 0.08},
            "medium": {"w": 11, "h": 11, "boxes": 3, "density": 0.11},
            "hard":   {"w": 13, "h": 13, "boxes": 4, "density": 0.14},
        }
        self.step_limit = 300
        self.undo_limit = 20
        self.obstacle_density = 0.10  # 用于随机生成
        self.theme_index = 0  # 0 Pastel, 1 Neon, 2 Mono
        self.show_help = True
        self.validate_timeout_sec = 1.2  # 求解器最大时长
        self.validate_node_limit = 50000 # 求解器最大节点展开

    def apply_difficulty(self, name):
        name = name.lower()
        if name in self.diff_params:
            self.difficulty = name
            p = self.diff_params[name]
            self.obstacle_density = p["density"]

    def adjust_step_limit(self, delta):
        self.step_limit = max(10, min(9999, self.step_limit + delta))

    def adjust_undo_limit(self, delta):
        self.undo_limit = max(0, min(999, self.undo_limit + delta))

    def adjust_density(self, delta):
        self.obstacle_density = max(0.00, min(0.40, round(self.obstacle_density + delta, 3)))

    def cycle_theme(self, idx=None):
        if idx is None:
            self.theme_index = (self.theme_index + 1) % len(THEMES)
        else:
            self.theme_index = idx % len(THEMES)

CONFIG = Config()

# ---------------------------
# 工具:方向与网格帮助
# ---------------------------
DIRS = {
    pygame.K_UP: (0, -1), pygame.K_w: (0, -1),
    pygame.K_DOWN: (0, 1), pygame.K_s: (0, 1),
    pygame.K_LEFT: (-1, 0), pygame.K_a: (-1, 0),
    pygame.K_RIGHT: (1, 0), pygame.K_d: (1, 0),
}
VEC4 = [(0,-1), (0,1), (-1,0), (1,0)]

def add(p, d): return (p[0]+d[0], p[1]+d[1])

# ---------------------------
# 关卡对象
# ---------------------------
class Level:
    def __init__(self, name=""):
        self.name = name
        self.w = 0
        self.h = 0
        self.walls = set()
        self.goals = set()
        self.boxes = set()
        self.player = None
        self.raw_lines = []
        self._bg_cached_surface = None  # 预渲染静态层缓存
        self._tile_size = 32
        self._origin = (0, 0)  # 网格起点在窗口坐标

        # 游戏运行态
        self.steps = 0
        self.start_ticks = pygame.time.get_ticks()
        self.undos_used = 0
        self.undo_stack = []    # [(player, frozenset(boxes))]
        self.redo_stack = []

    # 解析文本关卡
    @staticmethod
    def from_text(text, name="FromText"):
        lvl = Level(name)
        lines = [ln.rstrip("\n") for ln in text.splitlines()]
        if not lines:
            raise ValueError("空关卡")
        width = max(len(ln) for ln in lines)
        height = len(lines)
        lvl.w, lvl.h = width, height
        lvl.raw_lines = lines

        player_count = 0
        for y, ln in enumerate(lines):
            for x in range(width):
                ch = ln[x] if x < len(ln) else " "
                if ch == "#":
                    lvl.walls.add((x, y))
                elif ch in ".G":
                    lvl.goals.add((x, y))
                elif ch == "*":
                    lvl.goals.add((x, y))
                    lvl.boxes.add((x, y))
                elif ch == "B":
                    lvl.boxes.add((x, y))
                elif ch == "+":
                    lvl.goals.add((x, y))
                    lvl.player = (x, y); player_count += 1
                elif ch == "P":
                    lvl.player = (x, y); player_count += 1
                # 其他字符默认地板或空白

        if player_count == 0:
            raise ValueError("未找到玩家 P")
        if player_count > 1:
            raise ValueError("玩家数量超过1")
        if not lvl.boxes:
            raise ValueError("未找到箱子 B 或 *")
        if len(lvl.goals) != len(lvl.boxes):
            raise ValueError("目标与箱子数量不一致")
        return lvl

    # 基础结构验证与简单死锁检测
    def validate_structure(self):
        # 检查边界:如果外围没有墙,允许通过,但提示
        warnings = []
        # 角落死锁检查(非目标)
        for bx, by in self.boxes:
            if (bx, by) not in self.goals:
                # 是否在两面夹墙
                up = (bx, by-1) in self.walls
                down = (bx, by+1) in self.walls
                left = (bx-1, by) in self.walls
                right = (bx+1, by) in self.walls
                # 角落
                if (up and left) or (up and right) or (down and left) or (down and right):
                    warnings.append(f"箱子({bx},{by})位于角落且非目标,可能无解")
        return warnings

    def is_completed(self):
        return self.boxes == self.goals

    def clone_state(self):
        return (self.player, frozenset(self.boxes))

    def restore_state(self, state):
        self.player = state[0]
        self.boxes = set(state[1])

    def reset_run_state(self):
        self.steps = 0
        self.undos_used = 0
        self.undo_stack.clear()
        self.redo_stack.clear()
        self.start_ticks = pygame.time.get_ticks()

    # 尝试移动/推箱
    def try_move(self, dx, dy, step_limit, undo_limit):
        if self.is_completed():
            return False, "completed"
        if self.steps >= step_limit:
            return False, "step_limit_reached"

        px, py = self.player
        np = (px + dx, py + dy)
        pushed = False

        # 墙阻挡
        if np in self.walls:
            return False, "wall"
        # 如果下一格是箱子,检查再下一格是否可推
        if np in self.boxes:
            b2 = (np[0] + dx, np[1] + dy)
            if b2 in self.walls or b2 in self.boxes:
                return False, "box_blocked"
            # 记录状态以便撤销
            if len(self.undo_stack) < undo_limit:
                self.undo_stack.append(self.clone_state())
            else:
                # 撤销栈到达上限时,仍允许移动,但无法撤销更多
                pass
            self.redo_stack.clear()
            # 推箱
            self.boxes.remove(np)
            self.boxes.add(b2)
            self.player = np
            pushed = True
            self.steps += 1
            return True, "pushed"
        else:
            # 空地移动
            if len(self.undo_stack) < undo_limit:
                self.undo_stack.append(self.clone_state())
            self.redo_stack.clear()
            self.player = np
            self.steps += 1
            return True, "moved"

    def undo(self):
        if not self.undo_stack:
            return False
        prev = self.undo_stack.pop()
        self.redo_stack.append(self.clone_state())
        self.restore_state(prev)
        self.undos_used += 1
        return True

    def redo(self):
        if not self.redo_stack:
            return False
        nxt = self.redo_stack.pop()
        self.undo_stack.append(self.clone_state())
        self.restore_state(nxt)
        return True

    # ---------------------------
    # 渲染相关
    # ---------------------------
    def compute_layout(self, surf_size):
        # 根据窗口尺寸计算tile size与原点,使网格居中
        win_w, win_h = surf_size
        avail_w = max(100, win_w - 2 * MARGIN)
        avail_h = max(100, win_h - 2 * MARGIN)
        tile = min(avail_w // self.w, avail_h // self.h)
        tile = max(16, min(64, tile))
        grid_w = tile * self.w
        grid_h = tile * self.h
        ox = (win_w - grid_w) // 2
        oy = (win_h - grid_h) // 2
        self._tile_size = tile
        self._origin = (ox, oy)

    def grid_to_screen(self, cell):
        x, y = cell
        return (self._origin[0] + x * self._tile_size, self._origin[1] + y * self._tile_size)

    def pre_render_static(self, theme, target_surface):
        # 预渲染静态层(地板、墙、目标、网格线)
        self.compute_layout(target_surface.get_size())
        tile = self._tile_size
        ox, oy = self._origin
        bg = pygame.Surface(target_surface.get_size()).convert()
        bg.fill(theme["bg"])

        # 网格地板
        floor_col = theme["floor"]
        grid_col = theme["grid"]
        for y in range(self.h):
            for x in range(self.w):
                rx, ry = ox + x*tile, oy + y*tile
                pygame.draw.rect(bg, floor_col, (rx, ry, tile, tile))
                # 网格线
                pygame.draw.rect(bg, grid_col, (rx, ry, tile, tile), 1)

        # 目标点
        for (x, y) in self.goals:
            rx, ry = ox + x*tile, oy + y*tile
            gcol = theme["goal"]
            inset = max(2, tile // 6)
            pygame.draw.rect(bg, gcol, (rx+inset, ry+inset, tile-2*inset, tile-2*inset), border_radius=max(2, tile//8))

        # 墙
        for (x, y) in self.walls:
            rx, ry = ox + x*tile, oy + y*tile
            wcol = theme["wall"]
            pygame.draw.rect(bg, wcol, (rx, ry, tile, tile))

        self._bg_cached_surface = bg

    def draw_dynamic(self, target_surface, theme, dirty_rects=None):
        # 绘制动态对象(箱子、玩家)
        tile = self._tile_size
        ox, oy = self._origin
        def rect_of(c): 
            rx, ry = ox + c[0]*tile, oy + c[1]*tile
            return pygame.Rect(rx, ry, tile, tile)
        # 箱子
        for (x, y) in self.boxes:
            rc = rect_of((x, y))
            col = theme["box_on_goal"] if (x, y) in self.goals else theme["box"]
            inset = max(2, tile // 10)
            pygame.draw.rect(target_surface, col, rc.inflate(-2*inset, -2*inset), border_radius=max(2, tile//10))
            if dirty_rects is not None:
                dirty_rects.append(rc)
        # 玩家
        rc = rect_of(self.player)
        pcol = theme["player"]
        inset = max(2, tile // 8)
        pygame.draw.ellipse(target_surface, pcol, rc.inflate(-2*inset, -2*inset))
        if dirty_rects is not None:
            dirty_rects.append(rc)

# ---------------------------
# 求解器(宏推 BFS)
# ---------------------------
def reachable_cells(player, walls, boxes, w, h):
    # 计算玩家可到达区域(不穿墙与箱子)
    q = deque([player])
    seen = {player}
    blocked = walls.union(boxes)
    while q:
        x, y = q.popleft()
        for dx, dy in VEC4:
            nx, ny = x+dx, y+dy
            if 0 <= nx < w and 0 <= ny < h and (nx, ny) not in blocked and (nx, ny) not in seen:
                seen.add((nx, ny))
                q.append((nx, ny))
    return seen

def is_deadlock_box(pos, walls, goals):
    if pos in goals:
        return False
    x, y = pos
    up = (x, y-1) in walls
    down = (x, y+1) in walls
    left = (x-1, y) in walls
    right = (x+1, y) in walls
    if (up and left) or (up and right) or (down and left) or (down and right):
        return True
    return False

def solve_sokoban(level, time_limit_sec=1.2, node_limit=50000):
    # 返回 (solvable: bool, steps: int or None)
    start_time = time.time()
    walls = set(level.walls)
    goals = set(level.goals)
    boxes = frozenset(level.boxes)
    w, h = level.w, level.h

    # 初步死锁检查
    for b in boxes:
        if is_deadlock_box(b, walls, goals):
            return False, None

    # 状态:仅用箱子位置(宏推),玩家位置用于可达性判定
    # 队列元素:(boxes_frozenset, player_pos)
    from_state = {}
    start_state = (boxes, level.player)
    q = deque([start_state])
    seen = set([boxes])  # 仅按箱子集去重
    expansions = 0

    while q:
        if time.time() - start_time > time_limit_sec:
            return False, None
        if expansions > node_limit:
            return False, None
        cur_boxes, cur_player = q.popleft()
        expansions += 1

        # 终止条件
        if set(cur_boxes) == goals:
            return True, None

        # 玩家可达区域
        reach = reachable_cells(cur_player, walls, set(cur_boxes), w, h)
        # 尝试每个箱子的四向推
        for bx, by in cur_boxes:
            for dx, dy in VEC4:
                behind = (bx - dx, by - dy)  # 玩家站位
                ahead = (bx + dx, by + dy)   # 箱子推后位置
                # 先判断 ahead 合法,behind 可达
                if not (0 <= ahead[0] < w and 0 <= ahead[1] < h):
                    continue
                if ahead in walls or ahead in cur_boxes:
                    continue
                if behind not in reach:
                    continue
                # 死锁剪枝(推后)
                if is_deadlock_box(ahead, walls, goals):
                    continue
                new_boxes = set(cur_boxes)
                new_boxes.remove((bx, by))
                new_boxes.add(ahead)
                new_boxes_fs = frozenset(new_boxes)
                if new_boxes_fs in seen:
                    continue
                seen.add(new_boxes_fs)
                # 推后玩家位置在原箱子位置
                q.append((new_boxes_fs, (bx, by)))

    return False, None

# ---------------------------
# 随机关卡生成
# ---------------------------
def generate_random_level(name, width, height, box_count, obstacle_density, max_tries=200):
    # 基本框架:外墙 + 内部障碍(随机),随机放置目标/箱子/玩家,验证可解
    # 为确保可解,尝试多次生成
    for attempt in range(1, max_tries+1):
        lvl = Level(f"{name}#{attempt}")
        lvl.w, lvl.h = width, height
        # 外墙
        for x in range(width):
            lvl.walls.add((x, 0))
            lvl.walls.add((x, height-1))
        for y in range(height):
            lvl.walls.add((0, y))
            lvl.walls.add((width-1, y))
        # 内障碍
        cell_count = (width-2)*(height-2)
        obstacles = int(cell_count * obstacle_density)
        # 为避免过度碎片化,先随机挑格子
        candidates = [(x, y) for x in range(1, width-1) for y in range(1, height-1)]
        random.shuffle(candidates)
        for i in range(obstacles):
            lvl.walls.add(candidates[i])

        # 可放置区域
        free = [(x, y) for x in range(1, width-1) for y in range(1, height-1) if (x, y) not in lvl.walls]
        if len(free) < (2*box_count + 1):
            continue

        random.shuffle(free)
        # 放目标
        goals = free[:box_count]
        lvl.goals = set(goals)
        # 放箱子(与目标不同位置,允许部分重叠也可,但更难)
        p2 = free[box_count:]
        if len(p2) < box_count + 1:
            continue
        random.shuffle(p2)
        boxes = []
        # 尽量避免初始就角落死锁
        for cell in p2:
            if len(boxes) >= box_count:
                break
            if not is_deadlock_box(cell, lvl.walls, lvl.goals):
                boxes.append(cell)
        if len(boxes) < box_count:
            # 容错:允许少量角落,但降低成功率
            boxes = p2[:box_count]
        lvl.boxes = set(boxes)

        # 放玩家
        p3 = [c for c in p2 if c not in boxes][:1]
        if not p3:
            continue
        lvl.player = p3[0]

        # 验证结构与求解
        try:
            warns = lvl.validate_structure()
        except Exception:
            continue

        solvable, _ = solve_sokoban(lvl, time_limit_sec=CONFIG.validate_timeout_sec, node_limit=CONFIG.validate_node_limit)
        if solvable:
            return lvl, warns
    raise RuntimeError("随机生成失败:未能在限定次数内生成可解关卡,请降低障碍密度或减少箱子数")

# ---------------------------
# 内置示例关卡
# ---------------------------
BUILTIN_LEVELS = {
    "easy": [
        """#########
#   .   #
#       #
#   B   #
#   P   #
#   B   #
#       #
#   .   #
#########""",
        """#########
#   .   #
#  B    #
#     B #
#   P   #
#       #
#   .   #
#       #
#########"""
    ],
    "medium": [
        """###########
#   .   . #
#   B     #
#       B #
#  ###    #
#   P     #
#      B  #
#   .     #
###########"""
    ],
    "hard": [
        """#############
# .   #   . #
#   B   B   #
#     ###   #
#  P   B    #
#   #     B #
# .   #   . #
#############"""
    ],
}

def load_builtin_level(diff):
    levels = BUILTIN_LEVELS.get(diff, [])
    if not levels:
        return None
    text = random.choice(levels)
    return Level.from_text(text, name=f"{diff}_builtin")

# ---------------------------
# HUD / 文本绘制
# ---------------------------
def draw_hud(surface, theme, level, width, height, msg=""):
    font = pygame.font.SysFont("arial", 18)
    font_small = pygame.font.SysFont("arial", 14)
    # 背景
    hud = pygame.Surface((width, 100), pygame.SRCALPHA)
    hud.fill(theme["hud_bg"])
    # 时间
    elapsed_ms = pygame.time.get_ticks() - level.start_ticks
    sec = elapsed_ms // 1000
    mm, ss = divmod(sec, 60)
    time_str = f"{mm:02d}:{ss:02d}"
    # 状态文本
    lines = [
        f"难度: {CONFIG.difficulty}  主题: {THEME_NAMES[CONFIG.theme_index]}",
        f"用时: {time_str}  步数: {level.steps}/{CONFIG.step_limit}  撤销: {level.undos_used}/{CONFIG.undo_limit}",
        f"障碍密度: {CONFIG.obstacle_density:.3f}   关卡: {level.name}",
        "操作: 方向/WASD移动  Z撤销  Y重做  R重开  V验证  G随机新关  L拖拽txt加载  M菜单  1/2/3主题",
        "课堂即改: [/]步数上限  ;/'撤销上限  ,/.障碍密度  F1/F2/F3选择难度",
    ]
    if msg:
        lines.append(f"提示: {msg}")
    # 绘制文字
    y = 5
    color = theme["text"]
    for ln in lines:
        surf = font.render(ln, True, color)
        hud.blit(surf, (10, y))
        y += 20
    surface.blit(hud, (0, 0))

def draw_center_text(surface, text, theme, y_offset=0, big=False):
    font = pygame.font.SysFont("arial", 42 if big else 32, bold=big)
    surf = font.render(text, True, theme["text"])
    rect = surf.get_rect(center=(surface.get_width()//2, surface.get_height()//2 + y_offset))
    surface.blit(surf, rect)

# ---------------------------
# 游戏状态管理
# ---------------------------
class Game:
    def __init__(self):
        self.theme = THEMES[CONFIG.theme_index]
        self.mode = "menu"  # menu | playing | complete | failed | paused
        self.level = None
        self.info_msg = ""
        self.performance = {"fps": 0.0, "event_queue": 0}
        self.last_dirty = []  # 上一帧脏矩形,用于增量更新

        # 定时事件
        pygame.time.set_timer(TICK_EVENT, 1000)   # 每秒刷新计时显示
        pygame.time.set_timer(PERF_EVENT, 1000)   # 每秒更新性能指标

    def new_level_builtin(self, diff):
        CONFIG.apply_difficulty(diff)
        self.theme = THEMES[CONFIG.theme_index]
        try:
            lvl = load_builtin_level(CONFIG.difficulty)
            if lvl is None:
                raise RuntimeError("无内置关卡")
            warns = lvl.validate_structure()
            self.level = lvl
            self.level.reset_run_state()
            self.level.pre_render_static(self.theme, screen)
            self.info_msg = "; ".join(warns) if warns else "已载入内置关卡"
            self.mode = "playing"
        except Exception as e:
            self.info_msg = f"载入失败: {e}"

    def new_level_random(self):
        p = CONFIG.diff_params[CONFIG.difficulty]
        try:
            lvl, warns = generate_random_level(
                name=f"{CONFIG.difficulty}_rand",
                width=p["w"], height=p["h"],
                box_count=p["boxes"],
                obstacle_density=CONFIG.obstacle_density,
                max_tries=200
            )
            self.level = lvl
            self.level.reset_run_state()
            self.level.pre_render_static(self.theme, screen)
            self.info_msg = "随机关卡成功" + ((" | " + "; ".join(warns)) if warns else "")
            self.mode = "playing"
        except Exception as e:
            self.info_msg = f"随机生成失败: {e}"

    def load_level_from_file(self, path):
        try:
            with open(path, "r", encoding="utf-8") as f:
                text = f.read()
            lvl = Level.from_text(text, name=os.path.basename(path))
            warns = lvl.validate_structure()
            self.level = lvl
            self.level.reset_run_state()
            self.level.pre_render_static(self.theme, screen)
            self.info_msg = f"文件载入成功: {path}" + ((" | " + "; ".join(warns)) if warns else "")
            self.mode = "playing"
        except Exception as e:
            self.info_msg = f"文件载入失败: {e}"

    def validate_current(self):
        if self.level is None:
            self.info_msg = "无关卡"
            return
        st = time.time()
        solvable, _ = solve_sokoban(self.level, time_limit_sec=CONFIG.validate_timeout_sec, node_limit=CONFIG.validate_node_limit)
        dt = time.time() - st
        if solvable:
            self.info_msg = f"验证结果:可解({dt*1000:.0f}ms)"
        else:
            self.info_msg = f"验证结果:未证可解({dt*1000:.0f}ms)"

    def restart_level(self):
        if self.level is None:
            return
        # 重新从原始文本加载,确保初始状态一致
        try:
            base = Level.from_text("\n".join(self.level.raw_lines), name=self.level.name)
            self.level = base
            self.level.reset_run_state()
            self.level.pre_render_static(self.theme, screen)
            self.info_msg = "已重开关卡"
        except Exception:
            # 随机生成或未保存的关卡,简单清空运行态
            self.level.reset_run_state()
            self.level.pre_render_static(self.theme, screen)
            self.info_msg = "已重开(运行态复位)"

    def handle_event(self, e):
        if e.type == pygame.QUIT:
            pygame.quit()
            sys.exit(0)
        elif e.type == pygame.VIDEORESIZE:
            # 重新绘制静态层
            if self.level:
                self.level.pre_render_static(self.theme, screen)
        elif e.type == pygame.DROPFILE:
            self.load_level_from_file(e.file)
        elif e.type == TICK_EVENT:
            # 计时每秒刷新:不必做事,HUD会读取系统计时
            pass
        elif e.type == PERF_EVENT:
            self.performance["fps"] = clock.get_fps()
            self.performance["event_queue"] = pygame.event.peek()  # 简要查看队列
        elif e.type == pygame.KEYDOWN:
            self.on_keydown(e)

    def on_keydown(self, e):
        k = e.key
        mods = pygame.key.get_mods()
        shift = mods & pygame.KMOD_SHIFT

        if self.mode == "menu":
            if k == pygame.K_F1:
                self.new_level_builtin("easy")
            elif k == pygame.K_F2:
                self.new_level_builtin("medium")
            elif k == pygame.K_F3:
                self.new_level_builtin("hard")
            elif k == pygame.K_g:
                self.new_level_random()
            elif k in (pygame.K_1, pygame.K_2, pygame.K_3):
                CONFIG.cycle_theme({pygame.K_1:0, pygame.K_2:1, pygame.K_3:2}[k])
                self.theme = THEMES[CONFIG.theme_index]
            elif k == pygame.K_m:
                # 留在菜单
                pass
            elif k == pygame.K_ESCAPE:
                pygame.quit(); sys.exit(0)
            return

        # playing/complete/failed/paused 公共快捷键
        if k == pygame.K_m:
            self.mode = "menu"
            self.info_msg = "返回菜单"
            return
        if k in (pygame.K_1, pygame.K_2, pygame.K_3):
            CONFIG.cycle_theme({pygame.K_1:0, pygame.K_2:1, pygame.K_3:2}[k])
            self.theme = THEMES[CONFIG.theme_index]
            if self.level:
                self.level.pre_render_static(self.theme, screen)
            return
        if k == pygame.K_F1:
            self.new_level_builtin("easy"); return
        if k == pygame.K_F2:
            self.new_level_builtin("medium"); return
        if k == pygame.K_F3:
            self.new_level_builtin("hard"); return
        if k == pygame.K_g:
            self.new_level_random(); return
        if k == pygame.K_r:
            self.restart_level(); return
        if k == pygame.K_v:
            self.validate_current(); return
        if k == pygame.K_ESCAPE:
            pygame.quit(); sys.exit(0)

        # 参数调整
        if k == pygame.K_LEFTBRACKET:   # [
            CONFIG.adjust_step_limit(-50 if shift else -10); self.info_msg = f"步数上限: {CONFIG.step_limit}"
            return
        if k == pygame.K_RIGHTBRACKET:  # ]
            CONFIG.adjust_step_limit(50 if shift else +10); self.info_msg = f"步数上限: {CONFIG.step_limit}"
            return
        if k == pygame.K_SEMICOLON:     # ;
            CONFIG.adjust_undo_limit(-5 if shift else -1); self.info_msg = f"撤销上限: {CONFIG.undo_limit}"
            return
        if k == pygame.K_QUOTE:         # '
            CONFIG.adjust_undo_limit(+5 if shift else +1); self.info_msg = f"撤销上限: {CONFIG.undo_limit}"
            return
        if k == pygame.K_COMMA:         # ,
            CONFIG.adjust_density(-0.02 if shift else -0.01); self.info_msg = f"障碍密度: {CONFIG.obstacle_density:.3f}"
            return
        if k == pygame.K_PERIOD:        # .
            CONFIG.adjust_density(+0.02 if shift else +0.01); self.info_msg = f"障碍密度: {CONFIG.obstacle_density:.3f}"
            return

        # 游戏动作(playing)
        if self.mode == "playing" and self.level:
            if k in (pygame.K_z,):
                if self.level.undo():
                    self.info_msg = "撤销"
                else:
                    self.info_msg = "无法撤销"
                return
            if k in (pygame.K_y,):
                if self.level.redo():
                    self.info_msg = "重做"
                else:
                    self.info_msg = "无法重做"
                return

            if k in DIRS:
                dx, dy = DIRS[k]
                moved, reason = self.level.try_move(dx, dy, CONFIG.step_limit, CONFIG.undo_limit)
                # 更新绘制脏矩形:上一帧的动态清空,新帧绘制时会加入
                if moved:
                    # 判断胜利/失败
                    if self.level.is_completed():
                        self.mode = "complete"
                        self.info_msg = "成功!按G随机新关,或M回菜单"
                    elif reason == "step_limit_reached" or self.level.steps >= CONFIG.step_limit:
                        self.mode = "failed"
                        self.info_msg = "步数用尽!按R重开,G随机新关,或M回菜单"
                else:
                    if reason == "wall":
                        self.info_msg = "前方是墙"
                    elif reason == "box_blocked":
                        self.info_msg = "箱子后有阻挡"
                return

        # complete/failed 模式下额外键位
        if self.mode in ("complete", "failed"):
            if k == pygame.K_r:
                self.restart_level()
                self.mode = "playing"
            elif k == pygame.K_g:
                self.new_level_random()
            return

    # ---------------------------
    # 渲染
    # ---------------------------
    def render(self):
        self.theme = THEMES[CONFIG.theme_index]
        dirty = []

        # 背景
        if self.level and self.level._bg_cached_surface:
            screen.blit(self.level._bg_cached_surface, (0, 0))
        else:
            screen.fill(self.theme["bg"])

        # 动态层
        if self.level:
            self.level.draw_dynamic(screen, self.theme, dirty_rects=dirty)

        # HUD
        if self.level:
            draw_hud(screen, self.theme, self.level, screen.get_width(), screen.get_height(), self.info_msg)
        else:
            # 菜单页HUD
            font = pygame.font.SysFont("arial", 18)
            hud = pygame.Surface((screen.get_width(), 100), pygame.SRCALPHA)
            hud.fill(self.theme["hud_bg"])
            tx = "F1简  F2中  F3难  G随机  拖拽txt加载  1/2/3主题  ESC退出"
            hud.blit(font.render(tx, True, self.theme["text"]), (10, 10))
            screen.blit(hud, (0, 0))

        # 中央提示
        if self.mode == "menu":
            draw_center_text(screen, "推箱子教学版", self.theme, y_offset=-40, big=True)
            draw_center_text(screen, "选择难度:F1/F2/F3,或按 G 随机生成", self.theme, y_offset=10)
            draw_center_text(screen, "支持拖拽txt关卡文件加载", self.theme, y_offset=50)
        elif self.mode == "complete":
            draw_center_text(screen, "关卡完成!", self.theme, y_offset=-20, big=True)
            draw_center_text(screen, "R重开  G随机新关  M菜单", self.theme, y_offset=20)
        elif self.mode == "failed":
            draw_center_text(screen, "失败:步数用尽", self.theme, y_offset=-20, big=True)
            draw_center_text(screen, "R重开  G随机新关  M菜单", self.theme, y_offset=20)

        # 性能角标
        font = pygame.font.SysFont("consolas", 14)
        perf = f"{self.performance['fps']:.0f} FPS | EQ:{self.performance['event_queue']}"
        ps = font.render(perf, True, self.theme["text"])
        screen.blit(ps, (screen.get_width()-ps.get_width()-8, screen.get_height()-ps.get_height()-6))

        pygame.display.update(dirty if dirty else None)

# ---------------------------
# 主循环
# ---------------------------
def main():
    game = Game()
    # 默认进入菜单;若需要,可直接加载一个内置关卡
    # game.new_level_builtin("easy")

    while True:
        # 事件处理
        for e in pygame.event.get():
            game.handle_event(e)

        # 渲染
        game.render()

        # 帧率
        clock.tick(TARGET_FPS)

if __name__ == "__main__":
    main()
```

## 代码说明
- 关键函数和类的作用说明
  - Config:全局配置,支持课堂即时调整参数(步数上限、撤销上限、障碍密度)、难度与主题。
  - Level:
    - from_text:解析文本关卡(字符:#墙,.或G目标,B箱子,*目标上箱子,P玩家,+目标上玩家)。
    - validate_structure:基础结构验证(玩家/箱子/目标数量、角落死锁警告)。
    - try_move/undo/redo:移动与推箱逻辑、撤销与重做。
    - pre_render_static/draw_dynamic:预渲染静态层与动态渲染(脏矩形优化)。
  - solve_sokoban:宏推BFS求解,进行基础死锁剪枝与玩家可达性判定,适合小规模盒子数量(2-4)验证可解性。
  - generate_random_level:随机生成可解关卡(外墙 + 随机内障碍 + 随机目标/箱子/玩家),多次尝试与自动验证。
  - Game:整体流程控制(菜单/游戏/完成/失败)、事件处理(含事件过滤与自定义计时事件)、渲染。

- 运行要求和依赖说明
  - 需要 Python 3.8+ 与 pygame(pip install pygame)
  - 直接运行脚本即可启动。窗口支持拖拽 txt 文件载入关卡。
  - 关卡字符说明:
    - # 墙
    - . 或 G 目标
    - B 箱子
    - * 目标上箱子
    - P 玩家
    - + 目标上玩家
  - 常用按键:
    - F1/F2/F3:选择难度(简/中/难)
    - G:按当前难度与障碍密度随机生成可解关卡
    - 方向/WASD:移动/推箱
    - Z:撤销(受撤销上限限制)
    - Y:重做
    - R:重开关卡
    - V:验证当前关卡可解性(限时/节点上限)
    - L:拖拽 txt 文件到窗口加载关卡
    - [ / ]:调整步数上限(Shift 加大步幅)
    - ; / ':调整撤销上限(Shift 加大步幅)
    - , / .:调整障碍密度(Shift 加大步幅)
    - 1/2/3:切换主题(Pastel/Neon/Mono)
    - M:返回主菜单
    - ESC:退出

- 可能的扩展和改进建议
  - 更强的求解器:加入更完善的死锁模式库(沿墙死锁、推入死巷死锁),启用双向搜索或IDA*以验证更大规模关卡。
  - 关卡集管理:支持 levels/ 目录批量加载与关卡选择菜单。
  - 计时与排行榜:记录各难度下的最佳用时、步数,用 JSON 存档。
  - 动画与音效:加入推箱/完成音效、平滑移动动画与粒子特效。
  - 触屏与手柄:扩展手柄控制映射与触屏滑动输入。
  - 关卡编辑器:内置简单编辑器,课堂现场制作关卡并一键验证保存。
  - 更多渲染优化:使用pygame.sprite.DirtySprite体系、对大地图使用分块重绘与相机裁剪。

适用用户

编程初学者与学生

借助模板快速完成课程作业或个人项目,理解游戏循环、事件处理与碰撞检测,交付可玩的成品并撰写报告。

独立开发者与原型设计者

把玩法想法在数分钟内变成可玩Demo,AB对比不同机制与视觉风格,用于内测与投资人展示,快速迭代验证。

教育培训讲师

批量生成不同难度的教学案例与作业素材,课堂即跑即改,拆解架构与优化思路,提升授课效率与学习效果。

产品经理与游戏策划

验证核心循环与留存点,构建交互原型做用户测试,收集反馈后迅速调整关卡与道具,降低试错成本。

技术博主与内容创作者

快速产出教程配套代码与讲解大纲,录制教学视频一键生成示例,稳定更新节奏,提升内容质量。

美术与音效设计师

在可运行框架中替换素材,实时查看节奏与风格效果,验证美术方向,缩短与程序协作沟通链路。

学术与研究人员

搭建可控的交互环境做实验,灵活调整规则与反馈,复现实验场景,快速完成原型与论文附录演示。

游戏Jam与校园社团

赛期内迅速组装基础玩法,集中精力打磨世界观与关卡,提升完成度与提交质量,增强团队协作。

解决的问题

把你的游戏创意在极短时间内变成“可玩”的PyGame作品,助你从学习入门到原型验证一路加速。核心目标: - 根据“游戏类型/核心玩法/视觉风格”三要素,自动生成可运行、结构清晰的完整代码 - 给出清楚的主循环、场景管理、对象交互与碰撞处理方案,拒绝拼凑式模板 - 以丰富注释+使用说明,帮助初学者边做边学,帮助进阶者高效迭代原型 - 覆盖射击、平台跳跃、益智解谜等常见2D类型,快速对齐玩法方向 - 提供扩展建议(关卡、道具、音效、难度曲线),让作品更易打磨 - 内置合规与安全规则,规避不当题材与版权风险 适用场景:编程教学课件、个人练习与作品集、Game Jam 快速成型、立项与评审Demo、课程/训练营实操项目。通过试用体验“从描述到可玩”的直观跃迁,进而升级到深度定制与持续迭代版本。

特征总结

一键生成可运行PyGame项目,含主循环、事件响应与碰撞检测,打开即玩。
智能解析玩法设想与视觉风格,匹配合适架构与占位素材,缩短从创意到成品时间。
覆盖射击、平台跳跃、益智解谜等类型,切换模板即得新版本,便于试错和比较手感。
自动添加清晰中文注释与使用说明,像导师陪练,边看边学,快速掌握开发套路。
模块化结构便于扩展关卡、道具与音效,少量改动即可迭代版本,持续提升可玩性。
内置规范与性能建议,规避卡顿与逻辑陷阱,稳定帧率,体验顺滑更省心。
自动生成输入控制、UI与得分系统,课堂演示、用户测试与路演展示均可直接使用。
提供错误边界处理与调试提示,降低排错成本,把精力放在玩法创意与内容打磨。
参数化调用,按需指定类型、机制与风格,一次输入即可生成你的专属版本。
输出扩展清单与下一步建议,指导你从Demo走向完整作品,规划迭代路线。

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

¥20.00元
平台提供免费试用机制,
确保效果符合预期,再付费购买!

您购买后可以获得什么

获得完整提示词模板
- 共 658 tokens
- 3 个可调节参数
{ 游戏类型 } { 核心玩法 } { 视觉风格 }
自动加入"我的提示词库"
- 获得提示词优化器支持
- 版本化管理支持
获得社区共享的应用案例
限时免费

不要错过!

免费获取高级提示词-优惠即将到期

17
:
23
小时
:
59
分钟
:
59