20251231 2025-2026-2《python程序设计》实验四报告

《Python程序设计》实验报告

课程:《Python程序设计》
班级: 2512
姓名: 石有琛
学号:20251231
实验教师:王志强
实验日期:2026年5月25日
必修/选修: 公选课

1.实验内容

  1. 基于Python的Pygame库开发一款2D横版动作冒险游戏,实现玩家操控、敌人交互、场景切换、道具系统、积分记录等核心功能。
  2. 设计多类型游戏场景(草原、荒漠、熔岩、冰雪)、多等级敌人(普通、精英、BOSS)、多样化道具(回血、护盾、速度提升等),并实现游戏状态管理(菜单、游玩、通关、失败)。
  3. 完成游戏物理系统(重力、跳跃、冲刺、滑铲)、碰撞检测、积分与击杀记录存储(JSON文件)、高分榜展示等功能开发与调试。

2. 实验过程及结果

2.1 实验原理

  1. Pygame库核心原理:Pygame封装了SDL库的底层功能,提供图形渲染、事件监听、碰撞检测、定时器等接口,通过“事件监听-逻辑更新-画面渲染”的游戏主循环实现游戏持续运行,是Python开发2D游戏的核心工具。
  2. 面向对象编程原理:通过类封装玩家、敌人、地形、道具等游戏实体,继承pygame.sprite.Sprite实现精灵对象管理,可批量处理碰撞检测和画面渲染,降低代码耦合度。
  3. 物理效果实现原理:通过向量速度叠加重力值模拟角色下落,结合碰撞检测(与平台/墙壁)重置速度和位置,实现跳跃、落地、冲刺等符合物理规律的交互效果。
  4. 状态机管理原理:定义GameState枚举(菜单、游玩、通关、失败),通过状态切换控制不同阶段的逻辑执行和画面渲染,保证游戏流程的有序性。

2.2 程序实现

  1. 开发环境:Python 3.8+、Pygame 2.1.0+,操作系统为Windows 10。
  2. 核心实现步骤
  • 环境搭建:通过pip install pygame安装游戏开发库,配置Python开发环境。
  • 常量与枚举定义:设置屏幕尺寸、帧率、重力值等常量,定义场景、游戏状态、道具类型等枚举类,统一管理核心参数。
  • 类结构设计:
    • Player类:封装玩家属性(血量、护盾、速度等)和行为(移动、跳跃、冲刺、受击、使用道具);
    • Enemy/Boss类:实现敌人的移动、攻击、技能(尖刺)、血量管理等逻辑;
    • Platform/Wall/Item类:定义地形碰撞体和道具实体,实现与玩家的交互逻辑;
    • GameManager类:管理游戏状态、关卡、场景切换和高分数据的读写。
  • 主循环开发:监听退出、按键、定时器等事件,根据游戏状态执行逻辑更新(玩家/敌人行为、碰撞检测)和画面渲染(场景、精灵、UI文本)。
  • 调试优化:修复碰撞重叠、跳跃次数异常等问题,调整物理参数(重力、冲刺速度),增加异常处理(字体加载、文件读写)。

2.3 代码功能

  1. 玩家操控功能:支持A/D键左右移动、空格键二段跳、Shift键冲刺、S键滑铲,操作响应及时,物理效果符合预期。
点击查看代码
# 玩家属性与操控核心类
class Player(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.Surface((40, 60), pygame.SRCALPHA)
        self.rect = self.image.get_rect(center=(x, y))
        self.stats = PlayerStats()
        self.base_speed = self.stats.speed
        self.base_jump = self.stats.jump_power
        self.vel_x = 0
        self.vel_y = 0
        self.on_ground = False
        self.jumps_left = self.stats.jump_count
        self.dashing = False
        self.dash_cooldown = 0
        self.sliding = False
        self.wall_climbing = False
        self.facing_right = True
        self.invulnerable = False
        self.invuln_time = 0
        self.damage_flash_dur = 0
        self.speed_boost_dur = 0
        self.jump_boost_dur = 0
        self.damage_boost_dur = 0
        self.score = 0
        self.kills = 0
        self.jump_key_was_pressed = False
        self.draw_character()

    # 移动/跳跃/滑铲/冲刺输入处理
    def handle_movement(self, keys):
        if not self.dashing and not self.sliding:
            self.vel_x = 0
            if keys[pygame.K_a]:
                self.vel_x = -self.stats.speed
                self.facing_right = False
            if keys[pygame.K_d]:
                self.vel_x = self.stats.speed
                self.facing_right = True
        if keys[pygame.K_LSHIFT] and self.dash_cooldown == 0 and not self.dashing:
            self.dash()
        if keys[pygame.K_s] and self.on_ground and not self.sliding:
            self.slide()

    # 二段跳 + 爬墙逻辑
    def handle_jump_climb(self, keys, platforms, walls):
        if keys[pygame.K_SPACE] and not self.jump_key_was_pressed:
            if self.jumps_left > 0 and not self.wall_climbing:
                self.vel_y = -self.stats.jump_power
                self.jumps_left -= 1
                self.on_ground = False
        self.jump_key_was_pressed = keys[pygame.K_SPACE]
        self.wall_climbing = False
        wall_collide = pygame.sprite.spritecollideany(self, walls)
        if wall_collide and not self.on_ground and self.vel_y > 0:
            self.wall_climbing = True
            self.vel_y = 1

    # 重力+碰撞物理
    def apply_physics(self, platforms, walls):
        if not self.wall_climbing:
            self.vel_y += GRAVITY
        self.rect.x += self.vel_x
        self.rect.y += self.vel_y
        for wall in walls:
            if self.rect.colliderect(wall.rect):
                if self.vel_x > 0:
                    self.rect.right = wall.rect.left
                elif self.vel_x < 0:
                    self.rect.left = wall.rect.right
        self.on_ground = False
        for plat in platforms:
            if self.rect.colliderect(plat.rect):
                if self.vel_y > 0 and self.rect.bottom <= plat.rect.top + 20:
                    self.rect.bottom = plat.rect.top
                    self.vel_y = 0
                    self.on_ground = True
                    self.jumps_left = self.stats.jump_count
                elif self.vel_y < 0 and self.rect.top >= plat.rect.bottom - 20:
                    self.rect.top = plat.rect.bottom
                    self.vel_y = 0
        # 边界限制
        if self.rect.left < 0: self.rect.left = 0
        if self.rect.right > SCREEN_WIDTH: self.rect.right = SCREEN_WIDTH
        if self.rect.top < 0:
            self.rect.top = 0
            self.vel_y = 0
        if self.rect.bottom > SCREEN_HEIGHT:
            self.stats.hp = 0

    # 冲刺技能
    def dash(self):
        self.dashing = True
        self.dash_cooldown = 30
        direction = 1 if self.facing_right else -1
        self.vel_x = direction * self.stats.dash_power
        pygame.time.set_timer(pygame.USEREVENT + 1, 150)

    # 滑铲技能
    def slide(self):
        self.sliding = True
        direction = 1 if self.facing_right else -1
        self.vel_x = direction * (self.stats.speed * 1.5)
        pygame.time.set_timer(pygame.USEREVENT + 2, 300)

    # 技能冷却刷新
    def handle_cooldowns(self):
        if self.dash_cooldown > 0: self.dash_cooldown -= 1
        if self.invuln_time > 0:
            self.invuln_time -= 1
        else:
            self.invulnerable = False
        if self.damage_flash_dur > 0: self.damage_flash_dur -= 1
        # 增益效果冷却
        if self.speed_boost_dur > 0:
            self.speed_boost_dur -= 1
            if self.speed_boost_dur == 0: self.stats.speed = self.base_speed
        if self.jump_boost_dur > 0:
            self.jump_boost_dur -= 1
            if self.jump_boost_dur == 0: self.stats.jump_power = self.base_jump
        if self.damage_boost_dur > 0:
            self.damage_boost_dur -= 1
            if self.damage_boost_dur == 0: self.stats.damage = 1.0

    # 玩家更新总入口
    def update(self, platforms, walls):
        keys = pygame.key.get_pressed()
        self.handle_cooldowns()
        self.handle_movement(keys)
        self.handle_jump_climb(keys, platforms, walls)
        self.apply_physics(platforms, walls)
        self.draw_character()

# 主循环中玩家状态重置事件
if event.type == pygame.USEREVENT + 1:
    player.dashing = False
if event.type == pygame.USEREVENT + 2:
    player.sliding = False
  1. 多场景渲染功能:实现草原、荒漠、熔岩、冰雪4种差异化场景绘制,关卡切换时自动切换场景,提升视觉多样性。
点击查看代码
# 场景类型枚举
class SceneType(Enum):
    GRASS = 0
    DESERT = 1
    LAVA = 2
    SNOW = 3

# 场景颜色常量
WHITE = (255, 255, 255)
SAND = (210, 180, 140)
LAVA = (255, 80, 20)
SNOW = (220, 240, 255)

# 核心场景绘制函数
def draw_scene(scene: SceneType):
    if scene == SceneType.GRASS:
        sky = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
        sky.fill((100, 180, 255))
        SCREEN.blit(sky, (0, 0))
        pygame.draw.ellipse(SCREEN, WHITE, (100, 80, 200, 80))
        pygame.draw.ellipse(SCREEN, WHITE, (400, 120, 250, 90))
        pygame.draw.ellipse(SCREEN, WHITE, (800, 60, 180, 70))
        pygame.draw.polygon(SCREEN, (30, 120, 40), [(0, 550), (200, 400), (400, 550)])
        pygame.draw.polygon(SCREEN, (20, 100, 30), [(300, 550), (600, 380), (900, 550)])
    elif scene == SceneType.DESERT:
        sky = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
        sky.fill((255, 190, 100))
        SCREEN.blit(sky, (0, 0))
        pygame.draw.circle(SCREEN, (255, 120, 0), (1100, 150), 60)
        pygame.draw.ellipse(SCREEN, SAND, (-50, 520, 400, 150))
        pygame.draw.ellipse(SCREEN, (190, 160, 120), (300, 540, 500, 120))
    elif scene == SceneType.LAVA:
        sky = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
        sky.fill((80, 20, 20))
        SCREEN.blit(sky, (0, 0))
        pygame.draw.polygon(SCREEN, (50, 15, 15), [(500, 450), (350, 600), (650, 600)])
        pygame.draw.circle(SCREEN, LAVA, (500, 430), 40)
        for i in range(5):
            pygame.draw.line(SCREEN, LAVA, (i * 300, 650), (i * 300 + 150, 720), 8)
    elif scene == SceneType.SNOW:
        sky = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
        sky.fill((160, 200, 240))
        SCREEN.blit(sky, (0, 0))
        pygame.draw.polygon(SCREEN, SNOW, [(0, 500), (250, 320), (500, 500)])
        pygame.draw.polygon(SCREEN, (180, 210, 230), [(400, 500), (700, 280), (1000, 500)])
        for _ in range(30):
            x = random.randint(0, SCREEN_WIDTH)
            y = random.randint(0, SCREEN_HEIGHT)
            pygame.draw.circle(SCREEN, WHITE, (x, y), 2)

# 关卡切换时自动切换场景
if level % 4 == 1:
    gm.curr_scene = SceneType.GRASS
elif level % 4 == 2:
    gm.curr_scene = SceneType.DESERT
elif level % 4 == 3:
    gm.curr_scene = SceneType.LAVA
else:
    gm.curr_scene = SceneType.SNOW

# 游玩界面渲染场景
draw_scene(gm.curr_scene)
  1. 敌人交互功能:设计普通、精英、BOSS三类敌人,敌人具备追击、跳跃、技能(尖刺)行为;玩家踩踏敌人可击杀(尖刺状态除外),被敌人碰撞会受击掉血。
点击查看代码
# 敌人基类(普通+精英)
class Enemy(pygame.sprite.Sprite):
    def __init__(self, x, y, enemy_type="normal", difficulty=1.0):
        super().__init__()
        self.type = enemy_type
        if enemy_type == "normal":
            base_hp = 30
            base_damage = 15
            base_speed = 2
            self.color = RED
            self.size = 35
        elif enemy_type == "elite":
            base_hp = 70
            base_damage = 35
            base_speed = 3
            self.color = PURPLE
            self.size = 45
        self.max_hp = int(base_hp * difficulty)
        self.hp = self.max_hp
        self.damage = int(base_damage * difficulty)
        self.speed = base_speed * difficulty
        self.image = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
        self.rect = self.image.get_rect(center=(x, y))
        self.vel_x = 0
        self.vel_y = 0
        self.on_ground = False
        self.attack_cooldown = 0
        self.jump_cooldown = 0
        self.spike_active = False
        self.spike_cooldown = 120 if enemy_type=="elite" else 0
        self.spike_duration = 60 if enemy_type=="elite" else 0
        self.spike_timer = self.spike_cooldown

    # 尖刺技能逻辑
    def update_spikes(self):
        if self.type not in ("elite", "boss"): return
        if self.spike_active:
            self.spike_timer -=1
            if self.spike_timer <=0:
                self.spike_active=False
                self.spike_timer=self.spike_cooldown
        else:
            self.spike_timer -=1
            if self.spike_timer <=0:
                self.spike_active=True
                self.spike_timer=self.spike_duration

    # 敌人AI:追击玩家、跳跃、物理
    def update(self, player, platforms):
        if self.attack_cooldown>0: self.attack_cooldown-=1
        if self.jump_cooldown>0: self.jump_cooldown-=1
        self.vel_x = self.speed if player.rect.centerx>self.rect.centerx else -self.speed
        self.rect.x += self.vel_x
        self.vel_y += GRAVITY
        self.rect.y += self.vel_y
        # 平台碰撞
        for plat in platforms:
            if self.rect.colliderect(plat.rect):
                if self.vel_y>0:
                    self.rect.bottom = plat.rect.top
                    self.vel_y=0
                    self.on_ground=True
        # 主动跳跃
        if self.on_ground and self.jump_cooldown==0:
            if player.rect.centery < self.rect.centery-20:
                self.vel_y=-12
                self.jump_cooldown=40
        self.update_spikes()

# BOSS敌人类
class Boss(Enemy):
    def __init__(self, x, y, difficulty=1.0):
        super().__init__(x,y,"boss")
        self.max_hp = int(800 * difficulty)
        self.hp = self.max_hp
        self.damage = int(70 * difficulty)
        self.speed = 2.5 * difficulty
        self.size = 100
        self.color = ZOMBIE_GREEN
        self.spike_cooldown=150
        self.spike_duration=90

# 玩家与敌人碰撞交互逻辑
for enemy in enemies:
    if player.rect.colliderect(enemy.rect):
        # 踩踏击杀(尖刺状态除外)
        if player.rect.bottom >= enemy.rect.top and player.rect.bottom <= enemy.rect.top+15:
            if enemy.type in ("elite","boss") and enemy.spike_active:
                player.take_damage(enemy.damage)
            else:
                enemy.take_damage(999)
                player.score +=100
                player.kills +=1
        # 被敌人碰撞掉血
        elif enemy.attack_cooldown==0:
            player.take_damage(enemy.damage)
            enemy.attack_cooldown=30
  1. 道具系统功能:包含回血、护盾、速度提升、跳跃提升、伤害提升、无敌6种道具,拾取后触发限时增益效果,丰富游戏策略性。
点击查看代码
# 道具类型枚举
class ItemType(Enum):
    HEAL = 0
    DAMAGE = 1
    SHIELD = 2
    SPEED_BOOST = 3
    JUMP_BOOST = 4
    INVINCIBLE = 5

# 道具精灵类
class Item(pygame.sprite.Sprite):
    def __init__(self, x, y, item_type):
        super().__init__()
        self.type = item_type
        self.image = pygame.Surface((20, 20))
        # 道具颜色区分
        if item_type == ItemType.HEAL: self.image.fill(GREEN)
        elif item_type == ItemType.DAMAGE: self.image.fill(RED)
        elif item_type == ItemType.SHIELD: self.image.fill(BLUE)
        elif item_type == ItemType.SPEED_BOOST: self.image.fill(YELLOW)
        elif item_type == ItemType.JUMP_BOOST: self.image.fill(ORANGE)
        elif item_type == ItemType.INVINCIBLE: self.image.fill(WHITE)
        self.rect = self.image.get_rect(center=(x, y))

# 玩家使用道具效果
def use_item(self, item_type):
    if item_type == ItemType.HEAL:
        self.stats.hp = min(self.stats.hp+15, self.stats.max_hp)
    elif item_type == ItemType.DAMAGE:
        self.stats.damage=2.0
        self.damage_boost_dur=300
    elif item_type == ItemType.SHIELD:
        self.stats.shield = min(self.stats.shield+25, self.stats.max_shield)
    elif item_type == ItemType.SPEED_BOOST:
        self.stats.speed = self.base_speed*1.6
        self.speed_boost_dur=600
    elif item_type == ItemType.JUMP_BOOST:
        self.stats.jump_power = self.base_jump*1.6
        self.jump_boost_dur=600
    elif item_type == ItemType.INVINCIBLE:
        self.invulnerable=True
        self.invuln_time=300

# 玩家拾取道具
item_hits = pygame.sprite.spritecollide(player, items, True)
for item in item_hits:
    player.use_item(item.type)
  1. 游戏状态管理功能:菜单界面展示高分榜和操作指引,游玩界面实时显示血量、分数等信息,通关/失败界面支持重新开始或返回菜单。
点击查看代码
# 游戏状态枚举
class GameState(Enum):
    MENU = 0
    PLAYING = 1
    GAME_OVER = 2
    WIN = 3

# 主循环状态切换与渲染
if gm.state == GameState.MENU:
    # 菜单界面:标题、操作指引、高分榜
    SCREEN.fill((20, 30, 50))
    title = BIG_FONT.render("游侠", True, WHITE)
    SCREEN.blit(title, (SCREEN_WIDTH//2 - title.get_width()//2, 80))
    start = FONT.render("按 ENTER 开始游戏", True, GREEN)
    SCREEN.blit(start, (SCREEN_WIDTH//2 - start.get_width()//2, 200))

elif gm.state == GameState.PLAYING:
    # 游玩界面:实时显示血量、护盾、分数、击杀、关卡
    hp_text = FONT.render(f"生命: {max(0, player.stats.hp)}/{player.stats.max_hp}", True, RED)
    shield_text = FONT.render(f"护盾: {player.stats.shield}", True, BLUE)
    score_text = FONT.render(f"分数: {player.score}", True, YELLOW)
    kills_text = FONT.render(f"总击杀: {player.kills}", True, WHITE)
    level_text = FONT.render(f"关卡: {gm.current_level}", True, WHITE)

elif gm.state == GameState.WIN:
    # 通关界面:支持无尽模式/返回菜单
    congrats = BIG_FONT.render("恭喜通关!", True, YELLOW)
    SCREEN.blit(congrats, (SCREEN_WIDTH//2 - congrats.get_width()//2, 200))
    choice = FONT.render("按 E 进入无尽模式 | 按 M 返回菜单", True, GREEN)

elif gm.state == GameState.GAME_OVER:
    # 游戏结束界面:重新开始/返回菜单
    game_over = BIG_FONT.render("游戏结束", True, RED)
    restart = FONT.render("按 R 重新开始 | 按 M 返回菜单", True, GREEN)

# ESC键切换菜单/游戏
if event.type == pygame.KEYDOWN:
    if event.key == pygame.K_ESCAPE:
        if gm.state == GameState.PLAYING:
            gm.state = GameState.MENU
        elif gm.state == GameState.MENU:
            gm.state = GameState.PLAYING
  1. 数据存储功能:自动记录前三名高分(分数+击杀数),菜单界面展示历史高分,提升游戏成就感。
点击查看代码
# 游戏管理器(数据存储核心)
class GameManager:
    def __init__(self):
        self.state = GameState.MENU
        self.high_scores = []
        self.load_save()

    # 加载本地存档
    def load_save(self):
        try:
            if os.path.exists("save.json"):
                with open("save.json", "r", encoding="utf-8") as f:
                    data = json.load(f)
                self.high_scores = data.get("high_scores", [])
        except:
            self.high_scores = []

    # 保存高分(只保留前三名)
    def save_game(self, score=None, kills=None):
        if score is not None and kills is not None:
            self.high_scores.append({"score": score, "kills": kills})
            self.high_scores.sort(key=lambda x: x["score"], reverse=True)
            if len(self.high_scores) > 3:
                self.high_scores = self.high_scores[:3]
        with open("save.json", "w", encoding="utf-8") as f:
            json.dump({"high_scores": self.high_scores}, f, ensure_ascii=False, indent=2)

# 通关/死亡时自动保存分数
if gm.state == GameState.WIN and not saved_death:
    gm.save_game(score=player.score, kills=player.kills)
    saved_death = True
if gm.state == GameState.GAME_OVER and not saved_death:
    gm.save_game(score=player.score, kills=player.kills)
    saved_death = True

# 菜单展示高分榜
for idx, entry in enumerate(gm.high_scores[:3]):
    score_text = FONT.render(f"{idx+1}. 分数: {entry['score']}  击杀: {entry['kills']}", True, WHITE)
  1. 关卡进阶功能:击杀数达标生成BOSS,清空所有敌人后自动进入下一关,关卡难度随击杀数/关卡数递增,提升游戏挑战性。
点击查看代码
# 关卡生成函数
def create_level(level=1):
    # 清空上一关元素
    for sprite in list(all_sprites):
        if sprite != player: sprite.kill()
    # 自动切换场景
    if level %4 ==1: gm.curr_scene=SceneType.GRASS
    # 难度随击杀数递增
    difficulty = 1.0 + player.kills / 100.0
    # 敌人数量随关卡递增
    enemy_count = min(3 + level//2 + int(player.kills//30), 10)
    # 随机生成普通/精英敌人
    for _ in range(enemy_count):
        etype = random.choice(["normal", "elite"])
        enemies.add(Enemy(x, y, etype, difficulty))

# 击杀数达标生成BOSS
if not boss_spawned and player.kills >=50:
    boss = Boss(boss_x, boss_y, difficulty)
    enemies.add(boss)
    boss_spawned = True

# 清空所有敌人 → 自动下一关
if len(enemies) ==0:
    gm.current_level +=1
    create_level(gm.current_level)
    # 通关回血
    player.stats.hp = min(player.stats.hp+20, player.stats.max_hp)

# 通关条件:关卡>10
if not endless_mode and gm.current_level>10:
    gm.state = GameState.WIN

2.4 生成程序优点

  1. 模块化设计:面向对象的类结构清晰,各模块(玩家、敌人、管理器)职责明确,便于后续扩展新功能(如音效、新道具)。
  2. 交互体验佳:玩家操作丰富,敌人AI具备基础追击和攻击行为,道具系统增加游戏策略性,整体玩法完整。
  3. 跨平台兼容:字体加载、文件读写增加异常处理,避免因系统环境差异导致程序崩溃。
  4. UI交互友好:游戏内可通过H键显示/隐藏操作指引,菜单界面展示高分榜和核心操作说明,降低上手成本。
  5. 难度动态调整:敌人数量和难度随击杀数/关卡数递增,BOSS触发机制提升游戏的挑战性和可玩性。

2.5 运行结果

  1. 菜单界面:显示游戏标题、操作指引、历史高分榜,按Enter键可启动游戏,界面布局清晰,交互正常。
    image
  2. 游玩界面:可渲染对应场景(如草原/荒漠)、玩家、敌人、地形和道具,玩家操作流畅,碰撞检测准确,道具拾取触发增益效果,敌人血量条实时更新。
  3. 通关界面:完成10关后触发通关提示,显示总分数和击杀数,支持进入无尽模式或返回菜单。
    image
  4. 失败界面:玩家血量为0时触发游戏结束,自动保存当前分数到高分榜,支持重新开始或返回菜单。
    image
  5. 数据存储效果:游戏结束后查看游戏内分数榜,高分数据正确写入。
    image

2.6 源代码

点击查看代码
import pygame
import random
import sys
import json
import os
from enum import Enum
from dataclasses import dataclass
from typing import Dict, List

# 初始化
pygame.init()
pygame.mixer.init()

# ==================== 常量 ====================
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
FPS = 60
GRAVITY = 0.8
SCREEN = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("游侠")
CLOCK = pygame.time.Clock()

try:
    FONT = pygame.font.SysFont("simhei", 24)
except:
    FONT = pygame.font.Font(None, 24)
try:
    BIG_FONT = pygame.font.SysFont("simhei", 48)
except:
    BIG_FONT = pygame.font.Font(None, 48)
try:
    SMALL_FONT = pygame.font.SysFont("simhei", 18)
except:
    SMALL_FONT = pygame.font.Font(None, 18)

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 100, 255)
YELLOW = (255, 255, 0)
PURPLE = (150, 0, 255)
ORANGE = (255, 165, 0)
GRAY = (100, 100, 100)
BROWN = (139, 69, 19)
SAND = (210, 180, 140)
LAVA = (255, 80, 20)
SNOW = (220, 240, 255)
SKIN = (255, 220, 180)
HAT_COLOR = (100, 60, 30)
BOSS_COLOR = (128, 0, 128)
ZOMBIE_GREEN = (100, 150, 80)
DANGER_RED = (220, 20, 20)


class SceneType(Enum):
    GRASS = 0
    DESERT = 1
    LAVA = 2
    SNOW = 3


class GameState(Enum):
    MENU = 0
    PLAYING = 1
    GAME_OVER = 2
    WIN = 3


class ItemType(Enum):
    HEAL = 0
    DAMAGE = 1
    SHIELD = 2
    SPEED_BOOST = 3
    JUMP_BOOST = 4
    INVINCIBLE = 5


# ==================== 背景绘制 ====================
def draw_scene(scene: SceneType):
    if scene == SceneType.GRASS:
        sky = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
        sky.fill((100, 180, 255))
        SCREEN.blit(sky, (0, 0))
        pygame.draw.ellipse(SCREEN, WHITE, (100, 80, 200, 80))
        pygame.draw.ellipse(SCREEN, WHITE, (400, 120, 250, 90))
        pygame.draw.ellipse(SCREEN, WHITE, (800, 60, 180, 70))
        pygame.draw.polygon(SCREEN, (30, 120, 40), [(0, 550), (200, 400), (400, 550)])
        pygame.draw.polygon(SCREEN, (20, 100, 30), [(300, 550), (600, 380), (900, 550)])
    elif scene == SceneType.DESERT:
        sky = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
        sky.fill((255, 190, 100))
        SCREEN.blit(sky, (0, 0))
        pygame.draw.circle(SCREEN, (255, 120, 0), (1100, 150), 60)
        pygame.draw.ellipse(SCREEN, SAND, (-50, 520, 400, 150))
        pygame.draw.ellipse(SCREEN, (190, 160, 120), (300, 540, 500, 120))
    elif scene == SceneType.LAVA:
        sky = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
        sky.fill((80, 20, 20))
        SCREEN.blit(sky, (0, 0))
        pygame.draw.polygon(SCREEN, (50, 15, 15), [(500, 450), (350, 600), (650, 600)])
        pygame.draw.circle(SCREEN, LAVA, (500, 430), 40)
        for i in range(5):
            pygame.draw.line(SCREEN, LAVA, (i * 300, 650), (i * 300 + 150, 720), 8)
    elif scene == SceneType.SNOW:
        sky = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
        sky.fill((160, 200, 240))
        SCREEN.blit(sky, (0, 0))
        pygame.draw.polygon(SCREEN, SNOW, [(0, 500), (250, 320), (500, 500)])
        pygame.draw.polygon(SCREEN, (180, 210, 230), [(400, 500), (700, 280), (1000, 500)])
        for _ in range(30):
            x = random.randint(0, SCREEN_WIDTH)
            y = random.randint(0, SCREEN_HEIGHT)
            pygame.draw.circle(SCREEN, WHITE, (x, y), 2)


# ==================== 数据类 ====================
@dataclass
class PlayerStats:
    max_hp: int = 70
    hp: int = 70
    max_shield: int = 50
    shield: int = 0
    damage: float = 1.0
    speed: float = 5.0
    jump_power: float = 15
    dash_power: float = 12
    jump_count: int = 2


# ==================== 玩家 ====================
class Player(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.Surface((40, 60), pygame.SRCALPHA)
        self.rect = self.image.get_rect(center=(x, y))
        self.stats = PlayerStats()
        self.base_speed = self.stats.speed
        self.base_jump = self.stats.jump_power
        self.vel_x = 0
        self.vel_y = 0
        self.on_ground = False
        self.jumps_left = self.stats.jump_count
        self.dashing = False
        self.dash_cooldown = 0
        self.sliding = False
        self.wall_climbing = False
        self.facing_right = True
        self.invulnerable = False
        self.invuln_time = 0
        self.damage_flash_dur = 0
        self.speed_boost_dur = 0
        self.jump_boost_dur = 0
        self.damage_boost_dur = 0
        self.score = 0
        self.kills = 0
        self.jump_key_was_pressed = False
        self.draw_character()

    def draw_character(self):
        self.image.fill((0, 0, 0, 0))
        pygame.draw.rect(self.image, BLUE, (10, 30, 20, 25))
        pygame.draw.circle(self.image, SKIN, (20, 18), 12)
        pygame.draw.rect(self.image, HAT_COLOR, (8, 4, 24, 5))
        pygame.draw.rect(self.image, HAT_COLOR, (14, 0, 12, 8))
        pygame.draw.line(self.image, (60, 30, 10), (8, 6), (32, 6), 2)
        if self.damage_flash_dur > 0:
            pygame.draw.line(self.image, BLACK, (13, 15), (17, 15), 2)
            pygame.draw.line(self.image, BLACK, (23, 15), (27, 15), 2)
        else:
            pygame.draw.circle(self.image, WHITE, (15, 15), 3)
            pygame.draw.circle(self.image, BLACK, (15, 15), 1)
            pygame.draw.circle(self.image, WHITE, (25, 15), 3)
            pygame.draw.circle(self.image, BLACK, (25, 15), 1)
        pygame.draw.line(self.image, BLACK, (13, 12), (17, 13), 1)
        pygame.draw.line(self.image, BLACK, (23, 12), (27, 13), 1)
        if self.damage_flash_dur > 0:
            flash = pygame.Surface((40, 60), pygame.SRCALPHA)
            flash.fill((255, 0, 0, 100))
            self.image.blit(flash, (0, 0))

    def update(self, platforms, walls):
        keys = pygame.key.get_pressed()
        self.handle_cooldowns()
        self.handle_movement(keys)
        self.handle_jump_climb(keys, platforms, walls)
        self.apply_physics(platforms, walls)
        self.draw_character()

    def handle_cooldowns(self):
        if self.dash_cooldown > 0:
            self.dash_cooldown -= 1
        if self.invuln_time > 0:
            self.invuln_time -= 1
        else:
            self.invulnerable = False
        if self.damage_flash_dur > 0:
            self.damage_flash_dur -= 1
        if self.speed_boost_dur > 0:
            self.speed_boost_dur -= 1
            if self.speed_boost_dur == 0:
                self.stats.speed = self.base_speed
        if self.jump_boost_dur > 0:
            self.jump_boost_dur -= 1
            if self.jump_boost_dur == 0:
                self.stats.jump_power = self.base_jump
        if self.damage_boost_dur > 0:
            self.damage_boost_dur -= 1
            if self.damage_boost_dur == 0:
                self.stats.damage = 1.0

    def handle_movement(self, keys):
        if not self.dashing and not self.sliding:
            self.vel_x = 0
            if keys[pygame.K_a]:
                self.vel_x = -self.stats.speed
                self.facing_right = False
            if keys[pygame.K_d]:
                self.vel_x = self.stats.speed
                self.facing_right = True
        if keys[pygame.K_LSHIFT] and self.dash_cooldown == 0 and not self.dashing:
            self.dash()
        if keys[pygame.K_s] and self.on_ground and not self.sliding:
            self.slide()

    def handle_jump_climb(self, keys, platforms, walls):
        if keys[pygame.K_SPACE] and not self.jump_key_was_pressed:
            if self.jumps_left > 0 and not self.wall_climbing:
                self.vel_y = -self.stats.jump_power
                self.jumps_left -= 1
                self.on_ground = False
        self.jump_key_was_pressed = keys[pygame.K_SPACE]
        self.wall_climbing = False
        wall_collide = pygame.sprite.spritecollideany(self, walls)
        if wall_collide and not self.on_ground and self.vel_y > 0:
            self.wall_climbing = True
            self.vel_y = 1

    def apply_physics(self, platforms, walls):
        if not self.wall_climbing:
            self.vel_y += GRAVITY
        self.rect.x += self.vel_x
        self.rect.y += self.vel_y
        for wall in walls:
            if self.rect.colliderect(wall.rect):
                if self.vel_x > 0:
                    self.rect.right = wall.rect.left
                elif self.vel_x < 0:
                    self.rect.left = wall.rect.right
        self.on_ground = False
        for plat in platforms:
            if self.rect.colliderect(plat.rect):
                if self.vel_y > 0 and self.rect.bottom <= plat.rect.top + 20:
                    self.rect.bottom = plat.rect.top
                    self.vel_y = 0
                    self.on_ground = True
                    self.jumps_left = self.stats.jump_count
                elif self.vel_y < 0 and self.rect.top >= plat.rect.bottom - 20:
                    self.rect.top = plat.rect.bottom
                    self.vel_y = 0
        if self.rect.left < 0:
            self.rect.left = 0
        if self.rect.right > SCREEN_WIDTH:
            self.rect.right = SCREEN_WIDTH
        if self.rect.top < 0:
            self.rect.top = 0
            self.vel_y = 0
        if self.rect.bottom > SCREEN_HEIGHT:
            self.stats.hp = 0

    def dash(self):
        self.dashing = True
        self.dash_cooldown = 30
        direction = 1 if self.facing_right else -1
        self.vel_x = direction * self.stats.dash_power
        pygame.time.set_timer(pygame.USEREVENT + 1, 150)

    def slide(self):
        self.sliding = True
        direction = 1 if self.facing_right else -1
        self.vel_x = direction * (self.stats.speed * 1.5)
        pygame.time.set_timer(pygame.USEREVENT + 2, 300)

    def take_damage(self, amount):
        if not self.invulnerable:
            if self.stats.shield > 0:
                self.stats.shield -= amount
                if self.stats.shield < 0:
                    self.stats.hp += self.stats.shield
                    self.stats.shield = 0
            else:
                self.stats.hp -= amount
            self.invulnerable = True
            self.invuln_time = 20
            self.damage_flash_dur = 18
            if self.stats.hp <= 0:
                self.stats.hp = 0

    def use_item(self, item_type):
        if item_type == ItemType.HEAL:
            self.stats.hp = min(self.stats.hp + 15, self.stats.max_hp)
        elif item_type == ItemType.DAMAGE:
            self.stats.damage = 2.0
            self.damage_boost_dur = 300
        elif item_type == ItemType.SHIELD:
            self.stats.shield = min(self.stats.shield + 25, self.stats.max_shield)
        elif item_type == ItemType.SPEED_BOOST:
            self.stats.speed = self.base_speed * 1.6
            self.speed_boost_dur = 600
        elif item_type == ItemType.JUMP_BOOST:
            self.stats.jump_power = self.base_jump * 1.6
            self.jump_boost_dur = 600
        elif item_type == ItemType.INVINCIBLE:
            self.invulnerable = True
            self.invuln_time = max(self.invuln_time, 300)

    def reset(self, x, y, reset_kills=False):
        self.rect.center = (x, y)
        self.stats = PlayerStats()
        self.base_speed = self.stats.speed
        self.base_jump = self.stats.jump_power
        self.vel_x = 0
        self.vel_y = 0
        self.on_ground = False
        self.jumps_left = self.stats.jump_count
        self.dashing = False
        self.dash_cooldown = 0
        self.sliding = False
        self.wall_climbing = False
        self.facing_right = True
        self.invulnerable = False
        self.invuln_time = 0
        self.damage_flash_dur = 0
        self.speed_boost_dur = 0
        self.jump_boost_dur = 0
        self.damage_boost_dur = 0
        if reset_kills:
            self.score = 0
            self.kills = 0
        self.jump_key_was_pressed = False
        self.draw_character()


# ==================== 敌人 ====================
class Enemy(pygame.sprite.Sprite):
    def __init__(self, x, y, enemy_type="normal", difficulty=1.0):
        super().__init__()
        self.type = enemy_type
        if enemy_type == "normal":
            base_hp = 30
            base_damage = 15
            base_speed = 2
            self.color = RED
            self.size = 35
        elif enemy_type == "elite":
            base_hp = 70
            base_damage = 35
            base_speed = 3
            self.color = PURPLE
            self.size = 45
        else:
            base_hp = 30
            base_damage = 15
            base_speed = 2
            self.color = RED
            self.size = 35
        self.max_hp = int(base_hp * difficulty)
        self.hp = self.max_hp
        self.damage = int(base_damage * difficulty)
        self.speed = base_speed * difficulty
        self.image = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
        self.rect = self.image.get_rect(center=(x, y))
        self.vel_x = 0
        self.vel_y = 0
        self.on_ground = False
        self.attack_cooldown = 0
        self.jump_cooldown = 0
        self.spike_active = False
        if self.type == "elite":
            self.spike_cooldown = 120
            self.spike_duration = 60
        else:
            self.spike_cooldown = 0
            self.spike_duration = 0
        self.spike_timer = self.spike_cooldown
        self.draw_enemy()

    def draw_enemy(self):
        self.image.fill((0, 0, 0, 0))
        cx = self.size // 2
        cy = self.size // 2
        r = self.size // 2 - 3
        body_color = DANGER_RED if (self.type in ("elite", "boss") and self.spike_active) else self.color
        pygame.draw.circle(self.image, body_color, (cx, cy), r)
        eye_offset = r // 2
        left_eye = (cx - eye_offset, cy - eye_offset // 2)
        right_eye = (cx + eye_offset, cy - eye_offset // 2)
        eye_color = RED if (self.type in ("elite", "boss") and self.spike_active) else WHITE
        pygame.draw.circle(self.image, eye_color, left_eye, 5)
        pygame.draw.circle(self.image, BLACK, left_eye, 2)
        pygame.draw.circle(self.image, eye_color, right_eye, 5)
        pygame.draw.circle(self.image, BLACK, right_eye, 2)
        horn_len = 10
        horn_w = 6
        left_horn = [(cx - eye_offset - 2, cy - r),
                     (cx - eye_offset - 2 - horn_w // 2, cy - r - horn_len),
                     (cx - eye_offset - 2 + horn_w // 2, cy - r - horn_len)]
        right_horn = [(cx + eye_offset + 2, cy - r),
                      (cx + eye_offset + 2 - horn_w // 2, cy - r - horn_len),
                      (cx + eye_offset + 2 + horn_w // 2, cy - r - horn_len)]
        pygame.draw.polygon(self.image, (50, 50, 50), left_horn)
        pygame.draw.polygon(self.image, (50, 50, 50), right_horn)
        if self.type == "elite":
            pygame.draw.line(self.image, (200, 0, 0), (cx - 10, cy + 5), (cx + 10, cy - 5), 3)
        if self.spike_active:
            spike_h = 14
            spike_w = 8
            for offset in [-10, 0, 10]:
                x1 = cx + offset
                y1 = cy - r - 2
                pygame.draw.polygon(self.image, RED,
                                    [(x1 - spike_w // 2, y1), (x1 + spike_w // 2, y1), (x1, y1 - spike_h)])

    def update_spikes(self):
        if self.type not in ("elite", "boss"):
            return
        if self.spike_active:
            self.spike_timer -= 1
            if self.spike_timer <= 0:
                self.spike_active = False
                self.spike_timer = self.spike_cooldown
        else:
            self.spike_timer -= 1
            if self.spike_timer <= 0:
                self.spike_active = True
                self.spike_timer = self.spike_duration
        self.draw_enemy()

    def update(self, player, platforms):
        if self.attack_cooldown > 0:
            self.attack_cooldown -= 1
        if self.jump_cooldown > 0:
            self.jump_cooldown -= 1
        self.vel_x = self.speed if player.rect.centerx > self.rect.centerx else -self.speed
        self.rect.x += self.vel_x
        if self.rect.left < 0:
            self.rect.left = 0
        if self.rect.right > SCREEN_WIDTH:
            self.rect.right = SCREEN_WIDTH
        self.vel_y += GRAVITY
        self.rect.y += self.vel_y
        self.on_ground = False
        for plat in platforms:
            if self.rect.colliderect(plat.rect):
                if self.vel_y > 0 and self.rect.bottom <= plat.rect.top + 20:
                    self.rect.bottom = plat.rect.top
                    self.vel_y = 0
                    self.on_ground = True
                elif self.vel_y < 0 and self.rect.top >= plat.rect.bottom - 20:
                    self.rect.top = plat.rect.bottom
                    self.vel_y = 0
        if self.rect.bottom > SCREEN_HEIGHT:
            self.rect.bottom = SCREEN_HEIGHT
            self.vel_y = 0
            self.on_ground = True
        if self.on_ground and self.jump_cooldown == 0:
            if player.rect.centery < self.rect.centery - 20 and abs(player.rect.centery - self.rect.centery) < 150:
                self.vel_y = -12
                self.jump_cooldown = 40
        self.update_spikes()

    def draw_health_bar(self, screen):
        bar_width = self.size
        bar_height = 6
        x = self.rect.x
        y = self.rect.y - bar_height - 2
        health_percent = self.hp / self.max_hp
        pygame.draw.rect(screen, RED, (x, y, bar_width, bar_height))
        pygame.draw.rect(screen, GREEN, (x, y, bar_width * health_percent, bar_height))

    def take_damage(self, amount):
        self.hp -= amount
        if self.hp <= 0:
            self.kill()
            return True
        return False


class Boss(Enemy):
    def __init__(self, x, y, difficulty=1.0):
        super().__init__(x, y, "boss")
        base_hp = 800
        base_damage = 70
        base_speed = 2.5
        self.max_hp = int(base_hp * difficulty)
        self.hp = self.max_hp
        self.damage = int(base_damage * difficulty)
        self.speed = base_speed * difficulty
        self.size = 100
        self.color = ZOMBIE_GREEN
        self.spike_cooldown = 150
        self.spike_duration = 90
        self.spike_timer = self.spike_cooldown
        self.spike_active = False
        self.image = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
        self.draw_boss()
        self.rect = self.image.get_rect(center=(x, y))

    def draw_boss(self):
        self.image.fill((0, 0, 0, 0))
        cx = self.size // 2
        cy = self.size // 2
        r = self.size // 2 - 5
        body_color = DANGER_RED if self.spike_active else ZOMBIE_GREEN
        pygame.draw.circle(self.image, body_color, (cx, cy), r)
        eye_offset = 25
        left_eye = (cx - eye_offset, cy - 15)
        right_eye = (cx + eye_offset, cy - 15)
        outer_eye_color = RED if self.spike_active else BLACK
        inner_eye_color = (150, 0, 0) if self.spike_active else RED
        pygame.draw.circle(self.image, outer_eye_color, left_eye, 10)
        pygame.draw.circle(self.image, outer_eye_color, right_eye, 10)
        pygame.draw.circle(self.image, inner_eye_color, left_eye, 5)
        pygame.draw.circle(self.image, BLACK, left_eye, 2)
        pygame.draw.circle(self.image, inner_eye_color, right_eye, 5)
        pygame.draw.circle(self.image, BLACK, right_eye, 2)
        pygame.draw.line(self.image, BLACK, (cx - 15, cy - 20), (cx + 10, cy - 30), 3)
        pygame.draw.line(self.image, BLACK, (cx + 15, cy - 20), (cx + 25, cy - 15), 3)
        mouth_y = cy + 20
        for i in range(-3, 4):
            pygame.draw.line(self.image, (200, 0, 0), (cx + i * 8, mouth_y), (cx + i * 8 + 4, mouth_y + 8), 2)
        pygame.draw.rect(self.image, (80, 80, 80), (cx - 30, cy - r - 5, 60, 15))
        pygame.draw.line(self.image, (50, 50, 50), (cx - 25, cy - r - 5), (cx + 25, cy - r - 5), 2)
        if self.spike_active:
            spike_h = 20
            spike_w = 10
            for offset in [-15, 0, 15]:
                x1 = cx + offset
                y1 = cy - r - 8
                pygame.draw.polygon(self.image, RED,
                                    [(x1 - spike_w // 2, y1), (x1 + spike_w // 2, y1), (x1, y1 - spike_h)])

    def update(self, player, platforms):
        super().update(player, platforms)

    def draw_health_bar(self, screen):
        bar_width = self.size
        bar_height = 10
        x = self.rect.x
        y = self.rect.y - bar_height - 5
        health_percent = self.hp / self.max_hp
        pygame.draw.rect(screen, RED, (x, y, bar_width, bar_height))
        pygame.draw.rect(screen, GREEN, (x, y, bar_width * health_percent, bar_height))


# ==================== 地形道具 ====================
class Platform(pygame.sprite.Sprite):
    def __init__(self, x, y, w, h, is_spring=False):
        super().__init__()
        self.image = pygame.Surface((w, h))
        self.image.fill(GRAY if not is_spring else GREEN)
        self.rect = self.image.get_rect(topleft=(x, y))
        self.is_spring = is_spring


class Wall(pygame.sprite.Sprite):
    def __init__(self, x, y, w, h):
        super().__init__()
        self.image = pygame.Surface((w, h))
        self.image.fill(BROWN)
        self.rect = self.image.get_rect(topleft=(x, y))


class Item(pygame.sprite.Sprite):
    def __init__(self, x, y, item_type):
        super().__init__()
        self.type = item_type
        self.image = pygame.Surface((20, 20))
        if item_type == ItemType.HEAL:
            self.image.fill(GREEN)
        elif item_type == ItemType.DAMAGE:
            self.image.fill(RED)
        elif item_type == ItemType.SHIELD:
            self.image.fill(BLUE)
        elif item_type == ItemType.SPEED_BOOST:
            self.image.fill(YELLOW)
        elif item_type == ItemType.JUMP_BOOST:
            self.image.fill(ORANGE)
        elif item_type == ItemType.INVINCIBLE:
            self.image.fill(WHITE)
        self.rect = self.image.get_rect(center=(x, y))


# ==================== 游戏管理器(包含积分记录) ====================
class GameManager:
    def __init__(self):
        self.state = GameState.MENU
        self.current_level = 1
        self.game_mode = 0
        self.curr_scene = SceneType.GRASS
        self.high_scores = []
        self.load_save()

    def load_save(self):
        try:
            if os.path.exists("save.json"):
                with open("save.json", "r", encoding="utf-8") as f:
                    data = json.load(f)
                self.high_scores = data.get("high_scores", [])
        except:
            self.high_scores = []

    def save_game(self, score=None, kills=None):
        if score is not None and kills is not None:
            self.high_scores.append({"score": score, "kills": kills})
            self.high_scores.sort(key=lambda x: x["score"], reverse=True)
            if len(self.high_scores) > 3:
                self.high_scores = self.high_scores[:3]
        try:
            with open("save.json", "w", encoding="utf-8") as f:
                json.dump({"high_scores": self.high_scores}, f, ensure_ascii=False, indent=2)
        except:
            pass


# ==================== 初始化 ====================
gm = GameManager()
all_sprites = pygame.sprite.Group()
enemies = pygame.sprite.Group()
platforms = pygame.sprite.Group()
walls = pygame.sprite.Group()
items = pygame.sprite.Group()

player = Player(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
all_sprites.add(player)

boss_spawned = False
endless_mode = False


def create_level(level=1):
    global boss_spawned
    for sprite in list(all_sprites):
        if sprite != player:
            sprite.kill()
    platforms.empty()
    walls.empty()
    enemies.empty()
    items.empty()
    boss_spawned = False

    if level % 4 == 1:
        gm.curr_scene = SceneType.GRASS
    elif level % 4 == 2:
        gm.curr_scene = SceneType.DESERT
    elif level % 4 == 3:
        gm.curr_scene = SceneType.LAVA
    else:
        gm.curr_scene = SceneType.SNOW

    ground = Platform(0, SCREEN_HEIGHT - 40, SCREEN_WIDTH, 40)
    platforms.add(ground)

    platform_positions = [
        (200, 600, 150, 20, False),
        (400, 500, 150, 20, True),
        (600, 400, 150, 20, False),
        (850, 550, 120, 20, False),
        (100, 450, 100, 20, False),
        (1050, 350, 100, 20, False),
    ]
    for x, y, w, h, spring in platform_positions:
        platforms.add(Platform(x, y, w, h, spring))

    walls.add(Wall(100, 400, 20, 200))
    walls.add(Wall(800, 450, 20, 200))
    walls.add(Wall(1150, 300, 20, 250))

    difficulty = 1.0 + player.kills / 100.0
    enemy_count = min(3 + level // 2 + int(player.kills // 30), 10)
    for _ in range(enemy_count):
        etype = random.choice(["normal", "elite"])
        x = random.randint(200, SCREEN_WIDTH - 200)
        y = SCREEN_HEIGHT - 80
        if abs(x - SCREEN_WIDTH // 2) < 100:
            x += 150
        enemies.add(Enemy(x, y, etype, difficulty))

    item_count = max(1, 3 - player.kills // 20)
    for _ in range(item_count):
        while True:
            ix = random.randint(50, SCREEN_WIDTH - 50)
            iy = random.randint(100, SCREEN_HEIGHT - 100)
            safe = True
            for wall in walls:
                if wall.rect.collidepoint(ix, iy):
                    safe = False
                    break
            if safe:
                break
        itype = random.choice(list(ItemType))
        items.add(Item(ix, iy, itype))


def reset_game():
    global boss_spawned, endless_mode
    endless_mode = False
    player.reset(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2, reset_kills=True)
    enemies.empty()
    items.empty()
    boss_spawned = False
    create_level(gm.current_level)
    gm.state = GameState.PLAYING


create_level(1)
show_controls = True
saved_death = False

# ==================== 主循环 ====================
running = True
while running:
    CLOCK.tick(FPS)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            gm.save_game()
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                if gm.state == GameState.PLAYING:
                    gm.state = GameState.MENU
                elif gm.state == GameState.MENU:
                    gm.state = GameState.PLAYING
            if event.key == pygame.K_h:
                show_controls = not show_controls
        if event.type == pygame.USEREVENT + 1:
            player.dashing = False
        if event.type == pygame.USEREVENT + 2:
            player.sliding = False

    if gm.state == GameState.PLAYING:
        all_sprites.update(platforms, walls)
        for enemy in enemies:
            enemy.update(player, platforms)

        for enemy in enemies:
            if player.rect.colliderect(enemy.rect):
                if player.rect.bottom >= enemy.rect.top and player.rect.bottom <= enemy.rect.top + 15:
                    if enemy.type in ("elite", "boss") and enemy.spike_active:
                        player.take_damage(enemy.damage)
                        player.vel_y = -8
                        enemy.attack_cooldown = 30
                        if isinstance(enemy, Boss):
                            enemy.hp = min(enemy.hp + 20, enemy.max_hp)
                    else:
                        dmg = 200 if isinstance(enemy, Boss) else 999
                        enemy.take_damage(dmg)
                        player.vel_y = -12
                        items.add(Item(enemy.rect.centerx, enemy.rect.top - 10, ItemType.HEAL))
                        player.invulnerable = True
                        player.invuln_time = max(player.invuln_time, 60)
                        player.score += 100
                        player.kills += 1
                        enemy.attack_cooldown = 30
                elif enemy.attack_cooldown == 0:
                    player.take_damage(enemy.damage)
                    enemy.attack_cooldown = 30
                    if isinstance(enemy, Boss):
                        enemy.hp = min(enemy.hp + 20, enemy.max_hp)

        for _ in range(3):
            for i, enemy1 in enumerate(enemies):
                for enemy2 in list(enemies)[i + 1:]:
                    if enemy1.rect.colliderect(enemy2.rect):
                        dx = enemy1.rect.centerx - enemy2.rect.centerx
                        dy = enemy1.rect.centery - enemy2.rect.centery
                        if dx == 0 and dy == 0:
                            dx = 1
                        overlap_x = (enemy1.rect.width + enemy2.rect.width) / 2 - abs(dx)
                        overlap_y = (enemy1.rect.height + enemy2.rect.height) / 2 - abs(dy)
                        if overlap_x < overlap_y:
                            move_x = overlap_x / 2 if dx > 0 else -overlap_x / 2
                            enemy1.rect.x += move_x
                            enemy2.rect.x -= move_x
                        else:
                            move_y = overlap_y / 2 if dy > 0 else -overlap_y / 2
                            enemy1.rect.y += move_y
                            enemy2.rect.y -= move_y
                        enemy1.rect.clamp_ip(SCREEN.get_rect())
                        enemy2.rect.clamp_ip(SCREEN.get_rect())

        item_hits = pygame.sprite.spritecollide(player, items, True)
        for item in item_hits:
            player.use_item(item.type)

        spring_hits = pygame.sprite.spritecollide(player, platforms, False)
        for plat in spring_hits:
            if plat.is_spring and player.vel_y > 0:
                player.vel_y = -25

        if not boss_spawned and player.kills >= 50:
            difficulty = 1.0 + player.kills / 100.0
            boss_x = min(player.rect.x + 400, SCREEN_WIDTH - 100)
            boss_y = SCREEN_HEIGHT - 140
            boss = Boss(boss_x, boss_y, difficulty)
            enemies.add(boss)
            boss_spawned = True

        if len(enemies) == 0:
            gm.current_level += 1
            boss_spawned = False
            create_level(gm.current_level)
            player.stats.hp = min(player.stats.hp + 20, player.stats.max_hp)

        if not endless_mode and gm.current_level > 10:
            gm.state = GameState.WIN

        if player.stats.hp <= 0:
            gm.state = GameState.GAME_OVER
            saved_death = False

    # 渲染
    if gm.state == GameState.MENU:
        SCREEN.fill((20, 30, 50))
        title = BIG_FONT.render("游侠", True, WHITE)
        SCREEN.blit(title, (SCREEN_WIDTH // 2 - title.get_width() // 2, 80))
        start = FONT.render("按 ENTER 开始游戏", True, GREEN)
        SCREEN.blit(start, (SCREEN_WIDTH // 2 - start.get_width() // 2, 200))
        mode = FONT.render("踩踏敌人即可消灭它们!", True, WHITE)
        SCREEN.blit(mode, (SCREEN_WIDTH // 2 - mode.get_width() // 2, 280))
        controls = [
            "操作指引 (游戏内按 H 隐藏/显示):",
            "A/D : 移动 | 空格 : 跳跃",
            "Shift : 冲刺 | S : 滑铲",
            "ESC : 暂停/菜单"
        ]
        for i, line in enumerate(controls):
            txt = SMALL_FONT.render(line, True, (200, 200, 200))
            SCREEN.blit(txt, (50, 380 + i * 25))
        high_title = FONT.render("🏆 历史最高分 (前三名) 🏆", True, YELLOW)
        SCREEN.blit(high_title, (SCREEN_WIDTH // 2 - high_title.get_width() // 2, 480))
        if gm.high_scores:
            for idx, entry in enumerate(gm.high_scores[:3]):
                score_text = FONT.render(f"{idx + 1}. 分数: {entry['score']}  击杀: {entry['kills']}", True, WHITE)
                SCREEN.blit(score_text, (SCREEN_WIDTH // 2 - score_text.get_width() // 2, 520 + idx * 30))
        else:
            no_score = FONT.render("暂无记录", True, GRAY)
            SCREEN.blit(no_score, (SCREEN_WIDTH // 2 - no_score.get_width() // 2, 520))
        if pygame.key.get_pressed()[pygame.K_RETURN]:
            gm.current_level = 1
            reset_game()

    elif gm.state == GameState.PLAYING:
        draw_scene(gm.curr_scene)
        platforms.draw(SCREEN)
        walls.draw(SCREEN)
        items.draw(SCREEN)
        all_sprites.draw(SCREEN)
        enemies.draw(SCREEN)
        for enemy in enemies:
            enemy.draw_health_bar(SCREEN)
        hp_text = FONT.render(f"生命: {max(0, player.stats.hp)}/{player.stats.max_hp}", True, RED)
        shield_text = FONT.render(f"护盾: {player.stats.shield}", True, BLUE)
        score_text = FONT.render(f"分数: {player.score}", True, YELLOW)
        kills_text = FONT.render(f"总击杀: {player.kills}", True, WHITE)
        level_text = FONT.render(f"关卡: {gm.current_level}", True, WHITE)
        if endless_mode:
            mode_text = FONT.render("无尽模式", True, (255, 200, 100))
            SCREEN.blit(mode_text, (SCREEN_WIDTH // 2 - mode_text.get_width() // 2, 10))
        scene_name = ["青草原野", "荒漠戈壁", "熔岩火山", "冰雪雪原"][gm.curr_scene.value]
        scene_txt = FONT.render(f"场景:{scene_name}", True, WHITE)
        boosts = []
        if player.speed_boost_dur > 0: boosts.append("速度↑")
        if player.jump_boost_dur > 0: boosts.append("跳跃↑")
        if player.damage_boost_dur > 0: boosts.append("伤害↑")
        if player.invulnerable: boosts.append("无敌")
        boost_text = FONT.render(f"强化: {', '.join(boosts) if boosts else '无'}", True, (255, 200, 100))
        SCREEN.blit(hp_text, (20, 20))
        SCREEN.blit(shield_text, (20, 50))
        SCREEN.blit(score_text, (20, 80))
        SCREEN.blit(kills_text, (20, 110))
        SCREEN.blit(level_text, (20, 140))
        SCREEN.blit(boost_text, (20, 170))
        SCREEN.blit(scene_txt, (SCREEN_WIDTH - 300, 20))
        if show_controls:
            tip = SMALL_FONT.render("空格跳跃 | A/D移动 | Shift冲刺 | S滑铲 | H隐藏指引", True, (255, 255, 180))
            tip_rect = tip.get_rect(bottomright=(SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10))
            SCREEN.blit(tip, tip_rect)

    elif gm.state == GameState.WIN:
        # ========== 修复点:通关胜利时自动保存分数 ==========
        if not saved_death:
            gm.save_game(score=player.score, kills=player.kills)
            saved_death = True

        SCREEN.fill((20, 30, 50))
        congrats = BIG_FONT.render("恭喜通关!", True, YELLOW)
        SCREEN.blit(congrats, (SCREEN_WIDTH // 2 - congrats.get_width() // 2, 200))
        info = FONT.render(f"总分数: {player.score}  总击杀: {player.kills}", True, WHITE)
        SCREEN.blit(info, (SCREEN_WIDTH // 2 - info.get_width() // 2, 300))
        choice = FONT.render("按 E 进入无尽模式 | 按 M 返回菜单", True, GREEN)
        SCREEN.blit(choice, (SCREEN_WIDTH // 2 - choice.get_width() // 2, 400))
        keys = pygame.key.get_pressed()
        if keys[pygame.K_e]:
            endless_mode = True
            gm.current_level = 11
            create_level(gm.current_level)
            gm.state = GameState.PLAYING
        if keys[pygame.K_m]:
            gm.state = GameState.MENU
            endless_mode = False
            player.reset(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2, reset_kills=True)
            create_level(1)

    elif gm.state == GameState.GAME_OVER:
        if not saved_death:
            gm.save_game(score=player.score, kills=player.kills)
            saved_death = True
        SCREEN.fill((30, 10, 10))
        game_over = BIG_FONT.render("游戏结束", True, RED)
        SCREEN.blit(game_over, (SCREEN_WIDTH // 2 - game_over.get_width() // 2, 200))
        score = FONT.render(f"最终分数: {player.score}  总击杀: {player.kills}", True, WHITE)
        SCREEN.blit(score, (SCREEN_WIDTH // 2 - score.get_width() // 2, 300))
        if endless_mode:
            endless_txt = FONT.render("无尽模式", True, (255, 200, 100))
            SCREEN.blit(endless_txt, (SCREEN_WIDTH // 2 - endless_txt.get_width() // 2, 340))
        restart = FONT.render("按 R 重新开始 | 按 M 返回菜单", True, GREEN)
        SCREEN.blit(restart, (SCREEN_WIDTH // 2 - restart.get_width() // 2, 400))
        keys = pygame.key.get_pressed()
        if keys[pygame.K_r]:
            gm.current_level = 1
            reset_game()
        if keys[pygame.K_m]:
            gm.state = GameState.MENU
            endless_mode = False
            player.reset(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2, reset_kills=True)
            create_level(1)

    pygame.display.flip()

pygame.quit()
sys.exit()

3. 实验过程中遇到的问题和解决过程

  • 问题 1:玩家二段跳逻辑异常,多次按空格键可无限跳跃,无法限制跳跃次数。

  • 问题 1解决方案:新增jump_key_was_pressed变量记录空格键按压状态,仅在“松开→按下”时触发跳跃;维护jumps_left变量(初始值为2),跳跃时减1,落地时重置为2,通过该变量限制二段跳次数。

  • 问题 2:敌人之间发生坐标重叠,视觉混乱且碰撞检测异常。

  • 问题 2解决方案:添加敌人碰撞分离逻辑,遍历所有敌人对,计算x/y轴重叠量,选择重叠较小的轴方向调整坐标,循环执行3次分离逻辑,确保敌人无重叠。

  • 问题 3:部分操作系统缺失“simhei”字体,导致文字渲染失败。

  • 问题 3解决方案:使用try-except包裹字体加载逻辑,当SysFont加载失败时,调用pygame.font.Font(None, 字号)作为兜底方案,保证文字正常显示。

  • 问题 4:玩家冲刺后无法恢复正常移动,冲刺状态持续生效。

  • 问题 4解决方案:冲刺功能绑定USEREVENT + 1定时器(150ms),在事件循环中捕获该事件并将dashing设为False,重置冲刺状态,恢复正常移动逻辑。

其他(感悟、思考等)

课程总结

本学期《Python 程序设计》公选课即将结课,回顾这段学习时光,我收获满满、感触良多。Python 这门编程语言简洁优雅、易于上手的特点让我印象极为深刻,它没有复杂繁琐的语法限制,只要我们有创意、有想法,就能通过代码快速实现,这也是我在学习和做项目过程中最大的体会。
在课程学习中,我基本掌握了完整的 Python 知识体系:首先了解了 Python 的语言性质与特点,夯实了编程基础;接着学习了变量、数据类型、输入输出和注释等入门知识,掌握了代码的基本书写规范;然后深入学习了 for 循环与 while 循环,学会用循环实现重复逻辑;掌握了列表等数组的遍历与增删改查操作,能灵活处理数据;学习了函数的定义与调用,学会封装代码简化逻辑;理解了类和对象的用法,用面向对象思想搭建程序结构;掌握了文件操作技能,实现数据的读取与存储;还学习了异常处理和代码调试技巧,学会了如何排查错误。这些知识层层递进、环环相扣,为我完成实践项目打下了坚实的基础。
本次开发游戏的实验,是我对全课程知识的综合运用。我用类封装玩家、敌人、道具等游戏实体,用循环搭建游戏主循环,用异常处理避免程序崩溃,真正做到了学以致用。在实现功能、修复 bug 的过程中,我体会到了自主探索编程的乐趣,也感受到了将想法变成现实的快乐。
这门课程不仅教会了我编程知识,更培养了我的逻辑思维和动手能力,带给我知识与快乐双重收获。这段学习经历会成为我宝贵的财富,未来我也会继续学习 Python,不断探索编程的乐趣,用代码实现更多有趣的想法。

实验感悟

  1. 模块化设计是项目可维护的核心:初期将所有逻辑写在主循环中,调试困难;改用面向对象封装后,各模块职责清晰,定位问题和扩展功能的效率大幅提升。
  2. 调试能力是编程的核心素养:实验中遇到的跳跃异常、碰撞重叠等问题,需要逐行分析逻辑、打印变量值排查原因,这个过程让我学会了“拆解问题、逐步验证”的调试思路。
  3. 容错设计是程序健壮性的关键:字体缺失、文件读写失败等场景提醒我,编程需考虑不同运行环境的差异,增加异常处理和兜底方案,才能让程序适配更多场景。

意见和建议

  1. 作业布置多多的来。
  2. 可设置小组项目作业,让学生协作完成小型项目,培养团队协作和项目管理能力。
  3. 签到形式还不够折磨人。

后续优化方向

本次游戏仍有可改进的空间:一是添加音效系统(背景音乐、拾取道具/攻击音效),提升沉浸感;二是优化敌人AI,增加巡逻、躲避等行为,提升游戏挑战性;三是开发关卡编辑器,支持自定义地形和敌人分布;四是优化渲染效率,减少重复创建Surface对象,提升高帧率下的流畅度;五是设计成绩存档功能,保存玩家的最好成绩。

参考资料

posted @ 2026-06-07 22:35  eocean  阅读(50)  评论(0)    收藏  举报