20244305 2025-2026-2 《Python程序设计》实验四报告

20244305 2025-2026-2 《Python程序设计》实验四报告

课程:《Python程序设计》
班级: 2443
姓名: 苏楠
学号:20244305
实验教师:王志强
实验日期:2026年5月27日
必修/选修: 公选课

1.实验内容

Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
课代表和各小组负责人收集作业(源代码、视频、综合实践报告)

Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
例如:编写从社交网络爬取数据,实现可视化舆情监控或者情感分析。
例如:利用公开数据集,开展图像分类、恶意软件检测等
例如:利用Python库,基于OCR技术实现自动化提取图片中数据,并填入excel中。
例如:爬取天气数据,实现自动化微信提醒
例如:利用爬虫,实现自动化下载网站视频、文件等。
例如:编写小游戏:坦克大战、贪吃蛇、扫雷等等
注:在Windows/Linux系统上使用VIM、PDB、IDLE、Pycharm等工具编程实现。

本次python实践我是做了一款无尽生存射击类小游戏(网络上很火),玩家操控屏幕中心角色,通过移动、冲刺、射击击败随机刷新的各类怪物,获取经验升级,解锁强化属性,挑战无尽波次的怪物围攻,直至角色死亡游戏结束。游戏开发全程采用面向对象 + 模块化设计,代码结构清晰、功能完整、运行稳定,实现了角色操控、怪物 AI、升级系统、粒子特效、音效系统、计分系统等核心功能。

2. 实验过程及结果

(一)安装Pygame库
image

(二)由于刚开始做比较茫然,所以借助了豆包大模型的帮助生成了初版

点击查看代码
import pygame
import random
import math

pygame.init()
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("俯视角割草游戏")
clock = pygame.time.Clock()
FPS = 60

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

class Player:
    def __init__(self):
        self.x = WIDTH // 2
        self.y = HEIGHT // 2
        self.radius = 20
        self.speed = 4
        self.hp = 100
        self.level = 1
        self.exp = 0
        self.exp_need = 20

    def move(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_w] and self.y > self.radius:
            self.y -= self.speed
        if keys[pygame.K_s] and self.y < HEIGHT - self.radius:
            self.y += self.speed
        if keys[pygame.K_a] and self.x > self.radius:
            self.x -= self.speed
        if keys[pygame.K_d] and self.x < WIDTH - self.radius:
            self.x += self.speed

    def draw(self):
        pygame.draw.circle(screen, BLUE, (int(self.x), int(self.y)), self.radius)

class Monster:
    def __init__(self):
        side = random.randint(0, 3)
        if side == 0:
            self.x = random.randint(0, WIDTH)
            self.y = -20
        elif side == 1:
            self.x = WIDTH + 20
            self.y = random.randint(0, HEIGHT)
        elif side == 2:
            self.x = random.randint(0, WIDTH)
            self.y = HEIGHT + 20
        else:
            self.x = -20
            self.y = random.randint(0, HEIGHT)
        self.radius = 15
        self.speed = 1.5
        self.hp = 20

    def update(self, px, py):
        dx = px - self.x
        dy = py - self.y
        dist = math.hypot(dx, dy)
        if dist > 0:
            self.x += dx / dist * self.speed
            self.y += dy / dist * self.speed

    def draw(self):
        pygame.draw.circle(screen, RED, (int(self.x), int(self.y)), self.radius)

class Bullet:
    def __init__(self, x, y, tx, ty):
        self.x = x
        self.y = y
        speed = 8
        dx = tx - x
        dy = ty - y
        dist = math.hypot(dx, dy)
        self.vx = dx / dist * speed
        self.vy = dy / dist * speed
        self.radius = 6

    def update(self):
        self.x += self.vx
        self.y += self.vy

    def draw(self):
        pygame.draw.circle(screen, GREEN, (int(self.x), int(self.y)), self.radius)

# 全局对象
player = Player()
monsters = []
bullets = []
spawn_timer = 0
spawn_interval = 80
attack_timer = 0
attack_cd = 25
running = True

while running:
    clock.tick(FPS)
    screen.fill(BLACK)

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

    # 玩家移动+绘制
    player.move()
    player.draw()

    # 怪物刷新
    spawn_timer += 1
    if spawn_timer >= spawn_interval:
        monsters.append(Monster())
        spawn_timer = 0

    # 自动攻击
    attack_timer += 1
    if attack_timer >= attack_cd and monsters:
        nearest = min(monsters, key=lambda m: math.hypot(m.x - player.x, m.y - player.y))
        bullets.append(Bullet(player.x, player.y, nearest.x, nearest.y))
        attack_timer = 0

    # 子弹更新
    for b in bullets[:]:
        b.update()
        if b.x < 0 or b.x > WIDTH or b.y < 0 or b.y > HEIGHT:
            bullets.remove(b)
        b.draw()

    # 子弹碰撞怪物
    for b in bullets[:]:
        for m in monsters[:]:
            dis = math.hypot(b.x - m.x, b.y - m.y)
            if dis < b.radius + m.radius:
                monsters.remove(m)
                bullets.remove(b)
                player.exp += 5
                break

    # 怪物更新
    for m in monsters:
        m.update(player.x, player.y)
        m.draw()

    # 升级系统
    if player.exp >= player.exp_need:
        player.level += 1
        player.exp -= player.exp_need
        player.exp_need = int(player.exp_need * 1.3)
        player.speed += 0.2
        attack_cd = max(10, attack_cd - 1)

    pygame.display.flip()

pygame.quit()
运行截图如下:

image

但是这样还是很粗糙,几经大模型的调试和自己的创意改善,最后实现了这样的效果。
image

(三)下面我来解释一下各部分代码:
1.库导入与全局初始化

点击查看代码
import pygame
import random
import math
import sys
from enum import Enum
from collections import deque

# 初始化Pygame与音效
pygame.init()
pygame.mixer.init()
pygame.key.set_repeat(10, 10)  # 键盘长按重复响应
导入游戏开发必需的库:pygame负责图形 / 音效 / 事件,random生成随机数,math计算向量与角度,Enum定义升级类型 初始化游戏引擎、音频系统、键盘长按响应,为游戏运行搭建基础环境

2.常量与配置定义

点击查看代码
WIDTH, HEIGHT = 1000, 700
FPS = 60
# 颜色常量
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (220, 20, 60)
...
定义屏幕尺寸、帧率、颜色、字体等固定常量(可以自由改动选择最舒适的配置) 统一管理全局配置,便于后期修改游戏参数(如难度、速度、尺寸)

3.音效与工具函数

点击查看代码
# 生成游戏音效
def generate_sound(frequency, duration, volume=0.5, wave_type='square'):
    ...

# 辅助计算函数
def distance(x1, y1, x2, y2):
    return math.hypot(x2 - x1, y2 - y1)

def normalize(dx, dy):
    dist = math.hypot(dx, dy)
    return dx/dist, dy/dist if dist !=0 else (0,0)
音效生成:使用 Numpy 生成正弦 / 方波 / 噪音音效,无 Numpy 时自动静音,保证兼容性 距离计算:判断物体之间的距离,用于碰撞检测、怪物追踪 向量归一化:保证角色 / 子弹 / 怪物移动速度均匀,不会出现斜向移动更快的问题

4.粒子特效系统

点击查看代码
class Particle:
    def __init__(self, x, y, color, speed, lifetime):
        ...
    def update(self):
        ...
    def draw(self, surface):
        ...

class ParticleSystem:
    def emit(self, x, y, color, count=10):
        ...
粒子类:单个粒子的位置、速度、生命周期、透明度渐变 粒子管理器:批量生成粒子,实现爆炸、受击、拾取、升级等动态特效 应用场景:怪物死亡、角色受击、拾取宝石、升级闪光

5.游戏核心对象类(最重要也是下功夫最多的)
首先是玩家类

点击查看代码
class Player:
    def __init__(self):
        # 定义属性:坐标、血量、速度、等级、攻击冷却等
        self.x = WIDTH//2
        self.max_hp = 100
        self.speed = 5
        ...
    
    def move(self, keys):  # 移动+冲刺
        ...
    def dash(self):       # 空格键冲刺
        ...
    def take_damage(self, damage):  # 受击+无敌
        ...
    def draw(self, surface):        # 绘制角色+特效
        ...
之前玩这一类游戏总觉得移动很慢,一时半会很难逃离敌人的包围,所以拜托AI加入了空格冲刺的功能 AI优化增加了能量环的配置,我就照猫画虎为角色加入了眼睛
点击查看代码
eye = y - 4
# 眼白
pygame.draw.circle(surface, (255, 255, 255), (x - 5, eye), 4)
pygame.draw.circle(surface, (255, 255, 255), (x + 5, eye), 4)
# 瞳孔
pygame.draw.circle(surface, (0, 0, 0), (x - 5, eye), 2)
pygame.draw.circle(surface, (0, 0, 0), (x + 5, eye), 2)
# 高光点
pygame.draw.circle(surface, (255, 255, 255), (x - 6, eye - 1), 1)
pygame.draw.circle(surface, (255, 255, 255), (x + 4, eye - 1), 1)
没想到效果还不错,虽然改了几次参数,看来自己还是有些美术天赋 之后是怪兽类
点击查看代码
class Monster:
    def __init__(self, x, y, monster_type="normal"):
        # 区分四种怪物:normal/fast/tank/shooter
        if monster_type == "fast":
            self.speed = 3.5
            self.hp = 10
        ...
    
    def update(self, player_x, player_y):  # 追踪玩家
        ...
    def take_damage(self, damage):         # 受击死亡
        ...
四种怪物差异化设计:速度、血量、伤害、外观、攻击方式不同 自动追踪玩家,远程怪物可发射子弹 血条渐变显示,死亡后掉落经验宝石 最后是子弹类和经验类
点击查看代码
class Bullet:
    def __init__(self, x, y, tx, ty, damage):
        ...
class Gem:
    def __init__(self, x, y, value=5):
        ...
子弹:自动飞行、碰撞检测、穿透机制 宝石:悬浮动画、自动吸附玩家、提供经验值

6.升级系统

点击查看代码
class UpgradeType(Enum):
    MAX_HP = "Max HP +20"
    SPEED = "Speed +0.5"
    ...

class UpgradeChoice:
    def apply(self, player):
        # 应用升级效果
        if self.type == UpgradeType.MAX_HP:
            player.max_hp +=20
使用枚举类定义 9 种升级选项 升级后永久强化玩家属性:血量、移速、攻速、伤害、子弹数量等 升级时弹出三选一菜单,鼠标点击选择

7.游戏总管理类 Game

点击查看代码
class Game:
    def __init__(self):
        self.player = Player()
        self.monsters = []
        self.bullets = []
        ...
    
    def spawn_monster(self):  # 刷怪
        ...
    def player_attack(self):  # 自动攻击
        ...
    def update(self):         # 帧更新
        ...
    def draw(self):           # 画面绘制
        ...
游戏大脑:统一管理所有对象、逻辑、状态 负责:怪物刷新、子弹碰撞、经验拾取、升级判断、难度提升、UI 绘制 三种游戏状态:playing(游戏中)、levelup(升级)、gameover(结束) 在这一部分我修复了一个小bug,起初的时候AI给了我一行这样的代码:"Kills: (calculating...)",这个状态会一直保持,并不会出现最后结果。

image
我按照这一行的方式f"Score: {self.score}",对其进行了修正,新建一个变量self.kill,分别在reset和update中加入,最后将这一行("Kills: (calculating...)")改为f"Kills: {self.kills}"。
结果就是这样:
image
新加一部分,通过文件读写的学习,我决定增加一个本地最高分记录功能(在AI建议下选择了json文件)
首先在Game的init中加入最高分的初始记录
然后重点加入以下俩个方法

点击查看代码
def load_high_score(self):
    """从文件读取最高分"""
    try:
        with open("highscore.json", "r") as f:
            data = json.load(f)
            self.high_score = data.get("score", 0)
            self.high_wave = data.get("wave", 0)
    except (FileNotFoundError, json.JSONDecodeError):
        self.high_score = 0
        self.high_wave = 0

def save_high_score(self):
    """如果当前分数更高则保存"""
    if self.score > self.high_score:
        self.high_score = self.score
        self.high_wave = self.wave
        try:
            with open("highscore.json", "w") as f:
                json.dump({"score": self.high_score, "wave": self.high_wave}, f)
        except Exception as e:
            print("Could not save high score:", e)
在 Game.update() 中检测到游戏结束时(if not self.player.alive and self.game_state != "gameover": 块内),调用保存。 在 draw_gameover 方法中,在显示 “Press R to restart” 上方添加最高分信息: high_score_text = font_info.render(f"Best: {self.high_score} (Wave {self.high_wave})", True, YELLOW) surface.blit(high_score_text, (WIDTH//2 - high_score_text.get_width()//2, y)) y += 50 这样就完成了。

(四)完整版代码在这里:暗影割草游戏
image

(五)游戏演示视频:

注:玩游戏时记得切换为英文模式,不然键盘可能失效

课程总结

时光匆匆,为期一学期的 Python 课程已悄然结束。这段学习之旅,虽未触及高阶编程知识,却让我收获满满、受益匪浅。
初次接触 Python 时,面对一行行陌生的代码、各式各样的语法规则,我满是迷茫与不知所措,连最简单的程序编写都觉得无从下手。但随着课程的逐步推进,在老师的讲解和一次次练习中,我慢慢拨开迷雾,熟悉了变量、循环、判断、列表、字符串等基础语法,也能独立编写简单的小程序,从毫无头绪到逐渐上手,每一点进步都让我倍感欣喜。
最让我有成就感的是,课程尾声我成功完成了结课作业,实现了较为复杂的游戏。从构思逻辑到调试修改,虽然借助AI的帮助,但我真切感受到了学习的各种知识的应用,也彻底打破了最初对代码的畏惧。
这一过程中,我用python实现了计数统计,绘画制作,也帮助同学完成了socket通信,也用爬虫代码去复现网页,当然,我深知自己掌握的只是 Python 的冰山一角,还有大量进阶知识、实用技巧等待探索,自身的编程能力也还有诸多不足。但这段学习经历,不仅让我入门了 Python 这门编程语言,更让我懂得了学习编程没有捷径,唯有耐心钻研、坚持练习,才能不断突破。

感悟体会和意见建议

这一学期的Python课程结束了,真心觉得王老师的课堂实用性十足。课上学到的内容总能立刻落地实操,在动手实践中掌握知识,这大概就是编程语言学习最好的方式,也正是它真正的魅力所在。
整学期上课氛围轻松又愉快,我不仅收获了满满的知识,还认识了不少志同道合的伙伴。尤其难忘最后一课,老师聊起窗外的风景。身处室内课堂,恰逢春夏时节,我也常常望见窗外盛放的繁花,总能一扫心头的烦闷。
课程虽已落幕,但我对编程的探索不会停止,学习的脚步也会一直向前。由衷感谢王老师的悉心授课,也庆幸自己当初选择了这门课程。

整体而言,这门课体验极佳,也是我在电科院就读以来,觉得收获颇丰的优质课程。如果提一点小小的建议,希望课程能增加更多趣味内容。我一直对游戏开发很感兴趣,也梦想着能独立制作小游戏,目前课堂上相关内容涉及较少。另外爬虫技术我也十分感兴趣,但学习起来有一定难度,课堂讲解时长偏短,希望后续可以适当增加这部分的授课时间与内容深度。

参考资料

pygame使用方法
pygame绘画教程
python读写json文件的方法
豆包
Deepseek

附录(完整代码)

import pygame
import random
import math
import sys
from enum import Enum
from collections import deque
import json
import os

==================== 初始化 ====================
pygame.init()
pygame.key.set_repeat(10, 10)
尝试导入 numpy,用于生成音效
try:
import numpy as np
_HAS_NUMPY = True
except ImportError:
_HAS_NUMPY = False

当没有 numpy 时用来代替真实音效的静音对象
class DummySound:
def play(self, loops=0, maxtime=0, fade_ms=0):
pass
pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512)

屏幕设置
WIDTH, HEIGHT = 1200, 700
screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.DOUBLEBUF)
pygame.display.set_caption("Shadow Slayer: Endless Trial")
clock = pygame.time.Clock()
FPS = 60

==================== 颜色常量 ====================
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (220, 20, 60)
GREEN = (50, 205, 50)
BLUE = (30, 144, 255)
YELLOW = (255, 215, 0)
PURPLE = (138, 43, 226)
ORANGE = (255, 140, 0)
CYAN = (0, 255, 255)
DARK_GRAY = (30, 30, 30)
LIGHT_GRAY = (200, 200, 200)
BLOOD_RED = (139, 0, 0)

==================== 字体处理 ====================
def get_font(size, bold=False):
return pygame.font.Font(None, size)

==================== 音效生成 ====================
def generate_sound(frequency, duration, volume=0.5, wave_type='square'):
if not _HAS_NUMPY:
return DummySound()
sample_rate = 22050
n_samples = int(sample_rate * duration)
buf = np.zeros((n_samples, 2), dtype=np.int16)
for i in range(n_samples):
t = i / sample_rate
if wave_type == 'square':
val = 1.0 if math.sin(2 * math.pi * frequency * t) >= 0 else -1.0
elif wave_type == 'sine':
val = math.sin(2 * math.pi * frequency * t)
elif wave_type == 'noise':
val = random.uniform(-1, 1)
else:
val = math.sin(2 * math.pi * frequency * t)
val = int(val * volume * 32767)
buf[i] = (val, val)
return pygame.sndarray.make_sound(buf)

预生成常用音效
sfx_shoot = generate_sound(800, 0.05, 0.3, 'square') or DummySound()
sfx_hit = generate_sound(200, 0.1, 0.5, 'noise') or DummySound()
sfx_levelup = generate_sound(600, 0.2, 0.7, 'sine') or DummySound()
sfx_die = generate_sound(100, 0.3, 0.8, 'sine') or DummySound()
sfx_pickup = generate_sound(1000, 0.08, 0.4, 'square') or DummySound()

==================== 辅助函数 ====================
def distance(x1, y1, x2, y2):
return math.hypot(x2 - x1, y2 - y1)

def normalize(dx, dy):
"""返回单位向量"""
dist = math.hypot(dx, dy)
if dist == 0:
return 0, 0
return dx / dist, dy / dist

def random_edge_position():
"""在屏幕边缘生成随机位置"""
side = random.randint(0, 3)
if side == 0: # 上边
x = random.randint(0, WIDTH)
y = -20
elif side == 1: # 右边
x = WIDTH + 20
y = random.randint(0, HEIGHT)
elif side == 2: # 下边
x = random.randint(0, WIDTH)
y = HEIGHT + 20
else: # 左边
x = -20
y = random.randint(0, HEIGHT)
return x, y

==================== 粒子系统 ====================
class Particle:
def init(self, x, y, color, speed, lifetime, size=3):
self.x = x
self.y = y
self.color = color
angle = random.uniform(0, 2 * math.pi)
self.vx = math.cos(angle) * speed
self.vy = math.sin(angle) * speed
self.lifetime = lifetime
self.max_lifetime = lifetime
self.size = size

def update(self):
self.x += self.vx
self.y += self.vy
self.lifetime -= 1
self.vx *= 0.98
self.vy *= 0.98
return self.lifetime > 0

def draw(self, surface):
alpha = int(255 * (self.lifetime / self.max_lifetime))
color = (self.color, alpha)
s = pygame.Surface((self.size
2, self.size*2), pygame.SRCALPHA)
pygame.draw.circle(s, color, (self.size, self.size), self.size)
surface.blit(s, (int(self.x - self.size), int(self.y - self.size)))

class ParticleSystem:
def init(self):
self.particles = []

def emit(self, x, y, color, count=10, speed=2, lifetime=20, size=3):
for _ in range(count):
self.particles.append(Particle(x, y, color, speed, lifetime, size))

def update(self):
self.particles = [p for p in self.particles if p.update()]

def draw(self, surface):
for p in self.particles:
p.draw(surface)

==================== 游戏对象 ====================
class Player:
def init(self):
self.x = WIDTH // 2
self.y = HEIGHT // 2
self.radius = 18
self.base_speed = 5
self.speed = self.base_speed
self.max_hp = 100
self.hp = self.max_hp
self.armor = 0
self.level = 1
self.exp = 0
self.exp_to_level = 30
self.attack_cd = 30
self.attack_timer = 0
self.bullet_speed = 10
self.bullet_damage = 25
self.bullet_count = 1
self.bullet_pierce = 0
self.weapon_type = "normal"
self.alive = True
self.invincible_timer = 0
self.dash_cooldown = 0
self.dash_duration = 0
self.dash_direction = (0, 0)

def move(self, keys):
if not self.alive:
return
dx, dy = 0, 0
if keys[pygame.K_w] or keys[pygame.K_UP]:
dy -= 1
if keys[pygame.K_s] or keys[pygame.K_DOWN]:
dy += 1
if keys[pygame.K_a] or keys[pygame.K_LEFT]:
dx -= 1
if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
dx += 1

冲刺状态处理优化:冲刺时仍保留方向输入(可选)
if self.dash_duration > 0:
self.dash_duration -= 1
# 如果有方向输入,更新冲刺方向(让冲刺更可控)
if dx != 0 or dy != 0:
self.dash_direction = normalize(dx, dy)
self.x += self.dash_direction[0] * 15
self.y += self.dash_direction[1] * 15
else:
if dx != 0 or dy != 0:
dx, dy = normalize(dx, dy)
self.x += dx * self.speed
self.y += dy * self.speed

边界检测(确保玩家不出屏)
self.x = max(self.radius, min(WIDTH - self.radius, self.x))
self.y = max(self.radius, min(HEIGHT - self.radius, self.y))

if self.dash_cooldown > 0:
self.dash_cooldown -= 1

def dash(self):
if self.dash_cooldown <= 0 and self.dash_duration <= 0 and self.alive:
keys = pygame.key.get_pressed()
dx, dy = 0, 0
if keys[pygame.K_w] or keys[pygame.K_UP]: dy -= 1
if keys[pygame.K_s] or keys[pygame.K_DOWN]: dy += 1
if keys[pygame.K_a] or keys[pygame.K_LEFT]: dx -= 1
if keys[pygame.K_d] or keys[pygame.K_RIGHT]: dx += 1
if dx != 0 or dy != 0:
dx, dy = normalize(dx, dy)
self.dash_direction = (dx, dy)
self.dash_duration = 8
self.dash_cooldown = 60
else:
self.dash_direction = (0, 1)
self.dash_duration = 8
self.dash_cooldown = 60

def take_damage(self, damage):
if self.invincible_timer > 0 or not self.alive:
return
reduced = damage * (1 - self.armor / 100)
self.hp -= max(1, reduced)
self.invincible_timer = 30
if self.hp <= 0:
self.hp = 0
self.alive = False
sfx_die.play()

def update(self):
if self.invincible_timer > 0:
self.invincible_timer -= 1
if self.attack_timer > 0:
self.attack_timer -= 1
if self.dash_cooldown > 0:
self.dash_cooldown -= 1

def can_attack(self):
return self.attack_timer <= 0 and self.alive

def reset_attack_cooldown(self):
self.attack_timer = self.attack_cd

def draw(self, surface, camera=(0, 0)):
if not self.alive:
return
x, y = int(self.x), int(self.y)
time_ms = pygame.time.get_ticks()

基础光晕(最外层)
glow_radius = self.radius + 6 + int(3 * math.sin(time_ms * 0.01))
glow_surf = pygame.Surface((glow_radius * 2, glow_radius * 2), pygame.SRCALPHA)
for i in range(3):
r = glow_radius - i * 2
alpha = 40 - i * 10
pygame.draw.circle(glow_surf, (0, 255, 255, max(0, alpha)),
(glow_radius, glow_radius), r)
surface.blit(glow_surf, (x - glow_radius, y - glow_radius))

无敌状态额外光环
if self.invincible_timer > 0:
inv_radius = self.radius + 12
inv_surf = pygame.Surface((inv_radius * 2, inv_radius * 2), pygame.SRCALPHA)
flash = abs(math.sin(time_ms * 0.02)) # 闪烁
color = (255, 215, 0, int(150 * flash)) # 金色
pygame.draw.circle(inv_surf, color, (inv_radius, inv_radius), inv_radius, 3)
surface.blit(inv_surf, (x - inv_radius, y - inv_radius))

冲刺拖尾(简单实现:在冲刺方向后方绘制半透明圆)
if self.dash_duration > 0:
trail_surf = pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA)
alpha = int(100 * (self.dash_duration / 8))
color = (0, 191, 255, alpha) # 深天蓝
在玩家后方2个位置绘制拖尾
for offset in [0.5, 1.0]:
tx = x - self.dash_direction[0] * (self.speed * 2) * offset
ty = y - self.dash_direction[1] * (self.speed * 2) * offset
pygame.draw.circle(trail_surf, color,
(int(tx - x + self.radius), int(ty - y + self.radius)),
self.radius - 4)
surface.blit(trail_surf, (x - self.radius, y - self.radius))

主体外层(深色边框)
pygame.draw.circle(surface, (0, 50, 150), (x, y), self.radius)

主体内层(亮蓝渐变效果——使用半透明圆叠加)
pygame.draw.circle(surface, (30, 144, 255), (x, y), self.radius - 2)
pygame.draw.circle(surface, (0, 191, 255), (x, y), self.radius - 5)

中心高光
highlight_surf = pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA)
pygame.draw.circle(highlight_surf, (255, 255, 255, 100),
(self.radius - 4, self.radius - 5), self.radius // 3)
surface.blit(highlight_surf, (x - self.radius, y - self.radius))

能量环(旋转的光点)
ring_radius = self.radius + 2
for i in range(3):
angle = time_ms * 0.003 + i * 2.094 # 120度间隔
px = x + int(ring_radius * math.cos(angle))
py = y + int(ring_radius * math.sin(angle))
pygame.draw.circle(surface, (0, 255, 255), (px, py), 3)
能量环细线
pygame.draw.circle(surface, (0, 255, 255), (x, y), ring_radius, 1)

护盾指示器(头顶小三角)
tri_points = [(x, y - self.radius - 8),
(x - 5, y - self.radius - 2),
(x + 5, y - self.radius - 2)]
pygame.draw.polygon(surface, (0, 255, 255), tri_points)

眼睛
eye = y - 4
眼白
pygame.draw.circle(surface, (255, 255, 255), (x - 5, eye), 4)
pygame.draw.circle(surface, (255, 255, 255), (x + 5, eye), 4)
瞳孔
pygame.draw.circle(surface, (0, 0, 0), (x - 5, eye), 2)
pygame.draw.circle(surface, (0, 0, 0), (x + 5, eye), 2)
高光点
pygame.draw.circle(surface, (255, 255, 255), (x - 6, eye - 1), 1)
pygame.draw.circle(surface, (255, 255, 255), (x + 4, eye - 1), 1)

class Gem:
"""经验宝石"""
def init(self, x, y, value=5):
self.x = x
self.y = y
self.value = value
self.radius = 6
self.collect_speed = 0.5
self.lifetime = 300
self.max_lifetime = 300
self.collected = False
self.float_offset = 0
self.float_speed = random.uniform(0.05, 0.1)

def update(self, player_x, player_y):
self.float_offset += self.float_speed
dist = distance(self.x, self.y, player_x, player_y)
if dist < 100:
dx, dy = normalize(player_x - self.x, player_y - self.y)
speed = 5 + (100 - dist) * 0.1
self.x += dx * speed
self.y += dy * speed
self.lifetime -= 1
return self.lifetime > 0 and not self.collected

def draw(self, surface):
if self.lifetime < 60 and self.lifetime % 10 < 5:
return
y_off = math.sin(self.float_offset) * 3
pos = (int(self.x), int(self.y + y_off))
pygame.draw.circle(surface, YELLOW, pos, self.radius)
pygame.draw.circle(surface, ORANGE, pos, self.radius - 2)

class Monster:
"""敌人基类"""
def init(self, x, y, monster_type="normal"):
self.x = x
self.y = y
self.type = monster_type
self.alive = True
self.speed = 1.8
self.hp = 20
self.max_hp = 20
self.damage = 10
self.radius = 14
self.color = RED
self.exp_value = 5
self.attack_cooldown = 0
self.bullets = []

if monster_type == "fast":
self.speed = 3.5
self.hp = 10
self.max_hp = 10
self.damage = 8
self.radius = 10
self.color = ORANGE
self.exp_value = 8
elif monster_type == "tank":
self.speed = 1.2
self.hp = 60
self.max_hp = 60
self.damage = 20
self.radius = 22
self.color = BLOOD_RED
self.exp_value = 15
elif monster_type == "shooter":
self.speed = 1.2
self.hp = 25
self.max_hp = 25
self.damage = 12
self.radius = 16
self.color = PURPLE
self.exp_value = 12
self.attack_cooldown = 90

def update(self, player_x, player_y, player):
if not self.alive:
return
dx, dy = normalize(player_x - self.x, player_y - self.y)
self.x += dx * self.speed
self.y += dy * self.speed

if self.type == "shooter" and self.attack_cooldown > 0:
self.attack_cooldown -= 1
if self.attack_cooldown <= 0:
self.bullets.append(EnemyBullet(self.x, self.y, player_x, player_y))
self.attack_cooldown = random.randint(60, 120)

for b in self.bullets[:]:
b.update()
if b.x < -50 or b.x > WIDTH+50 or b.y < -50 or b.y > HEIGHT+50:
self.bullets.remove(b)

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

def draw(self, surface):
if not self.alive:
return
x, y = int(self.x), int(self.y)
time_ms = pygame.time.get_ticks()

===== 阴影 =====
shadow_surf = pygame.Surface((self.radius * 3, self.radius // 2), pygame.SRCALPHA)
pygame.draw.ellipse(shadow_surf, (0, 0, 0, 60), shadow_surf.get_rect())
surface.blit(shadow_surf, (x - self.radius * 1.5, y + self.radius // 2))

===== 外发光 =====
glow_radius = self.radius + 4 + int(2 * math.sin(time_ms * 0.01))
glow_surf = pygame.Surface((glow_radius * 2, glow_radius * 2), pygame.SRCALPHA)
for i in range(3):
r = glow_radius - i * 2
alpha = 30 - i * 8
pygame.draw.circle(glow_surf, (*self.color, max(0, alpha)),
(glow_radius, glow_radius), r)
surface.blit(glow_surf, (x - glow_radius, y - glow_radius))

===== 主体绘制 =====
根据类型绘制不同形状
if self.type == "tank":
坦克型:带尖刺的圆角多边形
spike_count = 8
inner_radius = self.radius - 2
outer_radius = self.radius + 4
points = []
for i in range(spike_count * 2):
angle = math.pi * i / spike_count + time_ms * 0.002
r = outer_radius if i % 2 == 0 else inner_radius
px = x + int(r * math.cos(angle))
py = y + int(r * math.sin(angle))
points.append((px, py))
pygame.draw.polygon(surface, self.color, points)
内部暗色区域
pygame.draw.circle(surface,
(max(0, self.color[0] - 50), max(0, self.color[1] - 50), max(0, self.color[2] - 50)),
(x, y), self.radius - 5)
elif self.type == "fast":
快速型:拉长的身体 + 运动线
angle = time_ms * 0.005 # 旋转方向并不重要,只是装饰
dx = int(4 * math.cos(angle))
dy = int(4 * math.sin(angle))
主体椭圆
ellipse_rect = pygame.Rect(x - self.radius - 3, y - self.radius + 2,
self.radius * 2 + 6, self.radius * 2 - 4)
pygame.draw.ellipse(surface, self.color, ellipse_rect)
运动拖尾线
for i in range(1, 4):
alpha = 150 - i * 30
trail_surf = pygame.Surface((6, 6), pygame.SRCALPHA)
pygame.draw.circle(trail_surf, (*self.color, alpha), (3, 3), 3)
surface.blit(trail_surf, (x - dx * i - 3, y - dy * i - 3))
elif self.type == "shooter":
射击型:主体圆 + 头顶炮管
pygame.draw.circle(surface, self.color, (x, y), self.radius)
炮管(随距离指向玩家的方向?这里简单用时间旋转)
cannon_angle = time_ms * 0.003
cannon_length = self.radius + 6
end_x = x + int(cannon_length * math.cos(cannon_angle))
end_y = y - int(cannon_length * math.sin(cannon_angle))
pygame.draw.line(surface, self.color, (x, y - self.radius // 3), (end_x, end_y), 4)
pygame.draw.circle(surface, (255, 255, 255), (end_x, end_y), 3)
触角(两侧)
for side in [-1, 1]:
ant_angle = cannon_angle + side * 0.6 + math.sin(time_ms * 0.01) * 0.3
ant_x = x + int(self.radius * 0.8 * math.cos(ant_angle))
ant_y = y - int(self.radius * 0.8 * math.sin(ant_angle))
pygame.draw.circle(surface, (255, 255, 255), (ant_x, ant_y), 3)
else: # normal
普通型:光滑圆形 + 内圈高光
pygame.draw.circle(surface, self.color, (x, y), self.radius)
inner_color = tuple(min(255, c + 40) for c in self.color)
pygame.draw.circle(surface, inner_color, (x - 1, y - 2), self.radius - 4)
高光点
highlight_surf = pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA)
pygame.draw.circle(highlight_surf, (255, 255, 255, 80),
(self.radius - 4, self.radius - 5), self.radius // 3)
surface.blit(highlight_surf, (x - self.radius, y - self.radius))

===== 眼睛(所有类型共用,但快速型眼睛会拉长)=====
eye_dx = self.radius * 0.3
eye_dy = self.radius * 0.4
if self.type == "fast":
快速型眼睛呈椭圆形
for ex, ey in [(x - eye_dx, y - eye_dy), (x + eye_dx, y - eye_dy)]:
pygame.draw.ellipse(surface, (0, 0, 0), (ex - 3, ey - 2, 6, 5))
pygame.draw.ellipse(surface, (255, 255, 255), (ex - 2, ey - 1, 3, 3))
else:
标准眼睛
pygame.draw.circle(surface, (0, 0, 0), (int(x - eye_dx), int(y - eye_dy)), 3)
pygame.draw.circle(surface, (0, 0, 0), (int(x + eye_dx), int(y - eye_dy)), 3)
高光
pygame.draw.circle(surface, (255, 255, 255), (int(x - eye_dx - 1), int(y - eye_dy - 1)), 1)
pygame.draw.circle(surface, (255, 255, 255), (int(x + eye_dx - 1), int(y - eye_dy - 1)), 1)

===== 血条美化 =====
if self.hp < self.max_hp:
bar_width = self.radius * 2
bar_height = 5
bar_x = x - bar_width // 2
bar_y = y - self.radius - 12
血条背景
bg_rect = pygame.Rect(bar_x - 1, bar_y - 1, bar_width + 2, bar_height + 2)
pygame.draw.rect(surface, (20, 20, 20), bg_rect, border_radius=2)
血量填充(渐变色:红->绿)
fill_ratio = self.hp / self.max_hp
fill_width = int(bar_width * fill_ratio)
if fill_width > 0:
fill_color = (
int(255 * (1 - fill_ratio)),
int(255 * fill_ratio),
0
)
fill_rect = pygame.Rect(bar_x, bar_y, fill_width, bar_height)
pygame.draw.rect(surface, fill_color, fill_rect, border_radius=2)
边框
pygame.draw.rect(surface, (255, 255, 255), (bar_x - 1, bar_y - 1, bar_width + 2, bar_height + 2), 1,
border_radius=2)

class EnemyBullet:
"""敌人发射的子弹"""
def init(self, x, y, tx, ty):
self.x = x
self.y = y
speed = 4
dx, dy = normalize(tx - x, ty - y)
self.vx = dx * speed
self.vy = dy * speed
self.radius = 5
self.color = PURPLE

def update(self):
self.x += self.vx
self.y += self.vy

def draw(self, surface):
pygame.draw.circle(surface, self.color, (int(self.x), int(self.y)), self.radius)

class Bullet:
"""玩家子弹"""
def init(self, x, y, tx, ty, speed=10, damage=25, pierce=0):
self.x = x
self.y = y
dx, dy = normalize(tx - x, ty - y)
self.vx = dx * speed
self.vy = dy * speed
self.radius = 5
self.damage = damage
self.pierce = pierce
self.alive = True
self.trail = deque(maxlen=5)

def update(self):
self.trail.append((self.x, self.y))
self.x += self.vx
self.y += self.vy
if self.x < -20 or self.x > WIDTH+20 or self.y < -20 or self.y > HEIGHT+20:
self.alive = False

def draw(self, surface):
for i, pos in enumerate(self.trail):
alpha = (i+1) / len(self.trail) * 0.5
s = pygame.Surface((6,6), pygame.SRCALPHA)
pygame.draw.circle(s, (0, 255, 0, int(100*alpha)), (3,3), 3)
surface.blit(s, (int(pos[0]-3), int(pos[1]-3)))
pygame.draw.circle(surface, GREEN, (int(self.x), int(self.y)), self.radius)

==================== 升级系统 ====================
class UpgradeType(Enum):
MAX_HP = "Max HP +20"
SPEED = "Speed +0.5"
ATTACK_SPEED = "Attack Speed Up"
BULLET_DAMAGE = "Bullet Damage +5"
BULLET_COUNT = "Bullets +1"
BULLET_PIERCE = "Pierce +1"
BULLET_SPEED = "Bullet Speed +2"
ARMOR = "Armor +5"
HEAL = "Heal 30 HP"

UPGRADE_POOL = list(UpgradeType)

class UpgradeChoice:
"""升级选项"""
def init(self, upgrade_type):
self.type = upgrade_type
self.description = upgrade_type.value

def apply(self, player):
if self.type == UpgradeType.MAX_HP:
player.max_hp += 20
player.hp = min(player.hp + 20, player.max_hp)
elif self.type == UpgradeType.SPEED:
player.speed += 0.5
elif self.type == UpgradeType.ATTACK_SPEED:
player.attack_cd = max(8, player.attack_cd - 3)
elif self.type == UpgradeType.BULLET_DAMAGE:
player.bullet_damage += 5
elif self.type == UpgradeType.BULLET_COUNT:
player.bullet_count += 1
elif self.type == UpgradeType.BULLET_PIERCE:
player.bullet_pierce += 1
elif self.type == UpgradeType.BULLET_SPEED:
player.bullet_speed += 2
elif self.type == UpgradeType.ARMOR:
player.armor = min(70, player.armor + 5)
elif self.type == UpgradeType.HEAL:
player.hp = min(player.max_hp, player.hp + 30)

==================== 游戏管理 ====================
class Game:
def init(self):
self.reset()
self.stars = [(random.randint(0, WIDTH), random.randint(0, HEIGHT), random.random() * 2 + 0.5) for _ in range(80)]
self.ambient_particles = []
for _ in range(30):
self.ambient_particles.append({
'x': random.randint(0, WIDTH),
'y': random.randint(0, HEIGHT),
'vx': random.uniform(-0.2, 0.2),
'vy': random.uniform(-0.2, 0.2),
'size': random.randint(2, 4),
'color': (random.randint(100, 200), random.randint(100, 200), random.randint(100, 200))
})
self.kills = 0
self.high_score = 0
self.high_wave = 0
self.load_high_score()

def reset(self):
self.player = Player()
self.monsters = []
self.bullets = []
self.gems = []
self.particles = ParticleSystem()
self.spawn_timer = 0
self.base_spawn_interval = 50
self.spawn_interval = self.base_spawn_interval
self.wave = 1
self.wave_timer = 0
self.score = 0
self.time_elapsed = 0
self.game_state = "playing" # playing, levelup, gameover
self.upgrade_options = []
self.camera_shake = 0
self.kill_combo = 0
self.combo_timer = 0
self.kills = 0

def spawn_monster(self):
x, y = random_edge_position()
r = random.random()
if self.wave >= 3 and r < 0.15:
m_type = "shooter"
elif self.wave >= 2 and r < 0.25:
m_type = "tank"
elif r < 0.4:
m_type = "fast"
else:
m_type = "normal"
self.monsters.append(Monster(x, y, m_type))

def player_attack(self):
if not self.player.can_attack() or not self.monsters:
return
nearest = min(self.monsters, key=lambda m: distance(self.player.x, self.player.y, m.x, m.y))
tx, ty = nearest.x, nearest.y

count = self.player.bullet_count
base_angle = math.atan2(ty - self.player.y, tx - self.player.x)
spread = 0.2
for i in range(count):
if count == 1:
angle = base_angle
else:
angle = base_angle + spread * (i - (count-1)/2)
dx = math.cos(angle)
dy = math.sin(angle)
target_x = self.player.x + dx * 100
target_y = self.player.y + dy * 100
self.bullets.append(Bullet(self.player.x, self.player.y,
target_x, target_y,
speed=self.player.bullet_speed,
damage=self.player.bullet_damage,
pierce=self.player.bullet_pierce))
self.player.reset_attack_cooldown()
sfx_shoot.play()

def check_level_up(self):
if self.player.exp >= self.player.exp_to_level and self.player.alive:
self.player.level += 1
self.player.exp -= self.player.exp_to_level
self.player.exp_to_level = int(self.player.exp_to_level * 1.15)
self.upgrade_options = random.sample(UPGRADE_POOL, min(3, len(UPGRADE_POOL)))
self.upgrade_options = [UpgradeChoice(opt) for opt in self.upgrade_options]
self.game_state = "levelup"
sfx_levelup.play()
self.particles.emit(self.player.x, self.player.y, YELLOW, 30, 4, 30, 5)

def apply_upgrade(self, choice):
choice.apply(self.player)
self.game_state = "playing"
self.player.invincible_timer = 20

def update(self):
if self.game_state == "gameover":
return

keys = pygame.key.get_pressed()
self.player.move(keys)
self.player.update()

if self.game_state == "playing":
self.player_attack()

self.spawn_timer += 1
dynamic_interval = max(10, self.base_spawn_interval - self.wave * 3)
if self.spawn_timer >= dynamic_interval:
self.spawn_monster()
self.spawn_timer = 0
if random.random() < 0.2 * self.wave:
self.spawn_monster()

self.wave_timer += 1
if self.wave_timer >= 60 * 30:
self.wave += 1
self.wave_timer = 0
self.player.hp = min(self.player.max_hp, self.player.hp + 10)

for b in self.bullets[:]:
b.update()
if not b.alive:
self.bullets.remove(b)
continue
hit = False
for m in self.monsters[:]:
if not m.alive:
continue
if distance(b.x, b.y, m.x, m.y) < b.radius + m.radius:
m.take_damage(b.damage)
self.particles.emit(b.x, b.y, WHITE, 5, 2, 10, 2)
sfx_hit.play()
if b.pierce > 0:
b.pierce -= 1
else:
self.bullets.remove(b)
hit = True
if not m.alive:
self.monsters.remove(m)
self.player.exp += m.exp_value
self.score += m.exp_value * 10
self.kills += 1 # 新增
self.kill_combo += 1
self.combo_timer = 60
if random.random() < 0.7:
self.gems.append(Gem(m.x, m.y, m.exp_value))
self.particles.emit(m.x, m.y, m.color, 15, 3, 20, 4)
self.camera_shake = max(self.camera_shake, 3)
break
if hit:
continue

for m in self.monsters:
m.update(self.player.x, self.player.y, self.player)
if self.player.alive and distance(m.x, m.y, self.player.x, self.player.y) < m.radius + self.player.radius:
self.player.take_damage(m.damage)
dx, dy = normalize(self.player.x - m.x, self.player.y - m.y)
self.player.x += dx * 20
self.player.y += dy * 20
self.particles.emit(self.player.x, self.player.y, RED, 8, 2, 15, 3)
self.camera_shake = max(self.camera_shake, 5)
for eb in m.bullets[:]:
if distance(eb.x, eb.y, self.player.x, self.player.y) < eb.radius + self.player.radius:
self.player.take_damage(10)
m.bullets.remove(eb)
self.particles.emit(self.player.x, self.player.y, PURPLE, 5, 2, 10, 2)
self.camera_shake = max(self.camera_shake, 4)

for gem in self.gems[:]:
if not gem.update(self.player.x, self.player.y):
self.gems.remove(gem)
elif distance(gem.x, gem.y, self.player.x, self.player.y) < gem.radius + self.player.radius:
self.player.exp += gem.value
self.score += gem.value * 5
self.gems.remove(gem)
sfx_pickup.play()
self.particles.emit(gem.x, gem.y, YELLOW, 8, 2, 15, 2)

if self.combo_timer > 0:
self.combo_timer -= 1
if self.combo_timer == 0:
self.kill_combo = 0

self.particles.update()

if self.camera_shake > 0:
self.camera_shake *= 0.9
if self.camera_shake < 0.5:
self.camera_shake = 0

self.check_level_up()

if not self.player.alive and self.game_state != "gameover":
self.game_state = "gameover"
self.save_high_score()
self.particles.emit(self.player.x, self.player.y, RED, 50, 5, 40, 6)
self.camera_shake = 10

self.time_elapsed += 1

for p in self.ambient_particles:
p['x'] += p['vx']
p['y'] += p['vy']
if p['x'] < 0: p['x'] = WIDTH
if p['x'] > WIDTH: p['x'] = 0
if p['y'] < 0: p['y'] = HEIGHT
if p['y'] > HEIGHT: p['y'] = 0

def draw(self, surface):
shake_x = random.randint(-int(self.camera_shake), int(self.camera_shake)) if self.camera_shake else 0
shake_y = random.randint(-int(self.camera_shake), int(self.camera_shake)) if self.camera_shake else 0

game_surf = pygame.Surface((WIDTH, HEIGHT))
game_surf.fill(BLACK)

for x in range(0, WIDTH, 40):
pygame.draw.line(game_surf, DARK_GRAY, (x, 0), (x, HEIGHT), 1)
for y in range(0, HEIGHT, 40):
pygame.draw.line(game_surf, DARK_GRAY, (0, y), (WIDTH, y), 1)
for star in self.stars:
x, y, b = star
brightness = int(128 + 127 * math.sin(pygame.time.get_ticks() * 0.001 * b))
color = (brightness, brightness, brightness)
pygame.draw.circle(game_surf, color, (x, y), 1)
for p in self.ambient_particles:
s = pygame.Surface((p['size'] * 2, p['size'] * 2), pygame.SRCALPHA)
pygame.draw.circle(s, (*p['color'], 60), (p['size'], p['size']), p['size'])
game_surf.blit(s, (int(p['x'] - p['size']), int(p['y'] - p['size'])))

for gem in self.gems:
gem.draw(game_surf)

for b in self.bullets:
b.draw(game_surf)

for m in self.monsters:
m.draw(game_surf)
for eb in m.bullets:
eb.draw(game_surf)

self.player.draw(game_surf)

self.particles.draw(game_surf)

screen.blit(game_surf, (shake_x, shake_y))

self.draw_ui(screen)

if self.game_state == "levelup":
self.draw_upgrade_menu(screen)

if self.game_state == "gameover":
self.draw_gameover(screen)

def draw_ui(self, surface):
bar_width = 200
bar_height = 15
bar_x = 20
bar_y = 20
pygame.draw.rect(surface, DARK_GRAY, (bar_x, bar_y, bar_width, bar_height))
hp_ratio = self.player.hp / self.player.max_hp
hp_color = (int(255(1-hp_ratio)), int(255hp_ratio), 0)
pygame.draw.rect(surface, hp_color, (bar_x, bar_y, bar_width * hp_ratio, bar_height))
pygame.draw.rect(surface, WHITE, (bar_x, bar_y, bar_width, bar_height), 2)

exp_ratio = self.player.exp / self.player.exp_to_level
pygame.draw.rect(surface, DARK_GRAY, (bar_x, bar_y + 22, bar_width, 10))
pygame.draw.rect(surface, YELLOW, (bar_x, bar_y + 22, bar_width * exp_ratio, 10))
pygame.draw.rect(surface, WHITE, (bar_x, bar_y + 22, bar_width, 10), 1)

font = get_font(24)
text_hp = font.render(f"HP: {int(self.player.hp)}/{self.player.max_hp}", True, WHITE)
surface.blit(text_hp, (bar_x + 5, bar_y - 2))

font_small = get_font(20)
text_lv = font_small.render(f"Lv.{self.player.level}", True, WHITE)
surface.blit(text_lv, (bar_x, bar_y + 35))

font_score = get_font(24)
text_score = font_score.render(f"Score: {self.score}", True, WHITE)
surface.blit(text_score, (WIDTH - 160, 20))

text_wave = font_score.render(f"Wave: {self.wave}", True, ORANGE)
surface.blit(text_wave, (WIDTH - 160, 50))

if self.kill_combo >= 3:
combo_text = get_font(30).render(f"COMBO x{self.kill_combo}", True, RED)
surface.blit(combo_text, (WIDTH//2 - combo_text.get_width()//2, 80))

cd_ratio = max(0, self.player.dash_cooldown / 60)
dash_size = 40
dash_x = 20
dash_y = HEIGHT - 70
pygame.draw.rect(surface, DARK_GRAY, (dash_x, dash_y, dash_size, dash_size))
pygame.draw.rect(surface, BLUE, (dash_x, dash_y, dash_size * cd_ratio, dash_size))
pygame.draw.rect(surface, WHITE, (dash_x, dash_y, dash_size, dash_size), 2)
dash_label = get_font(16).render("SPACE", True, WHITE)
surface.blit(dash_label, (dash_x + 2, dash_y + 12))

def draw_upgrade_menu(self, surface):
overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 180))
surface.blit(overlay, (0, 0))

font_title = get_font(48, bold=True)
font_opt = get_font(26)
font_instruction = get_font(18)

title = font_title.render("LEVEL UP!", True, YELLOW)
surface.blit(title, (WIDTH // 2 - title.get_width() // 2, 100))

box_width = 250
box_height = 80
spacing = 20
total_width = len(self.upgrade_options) * box_width + (len(self.upgrade_options) - 1) * spacing
start_x = WIDTH // 2 - total_width // 2
y = HEIGHT // 2 - box_height // 2

mouse_pos = pygame.mouse.get_pos()
mouse_click = pygame.mouse.get_pressed()

for i, opt in enumerate(self.upgrade_options):
x = start_x + i * (box_width + spacing)
rect = pygame.Rect(x, y, box_width, box_height)

if rect.collidepoint(mouse_pos):
pygame.draw.rect(surface, (50, 50, 50), rect, border_radius=10)
pygame.draw.rect(surface, WHITE, rect, 3, border_radius=10)
if mouse_click[0]:
self.apply_upgrade(opt)
return
else:
pygame.draw.rect(surface, DARK_GRAY, rect, border_radius=10)
pygame.draw.rect(surface, LIGHT_GRAY, rect, 2, border_radius=10)

desc = opt.description
shadow_surf = font_opt.render(desc, True, BLACK)
shadow_rect = shadow_surf.get_rect(center=(x + box_width // 2 + 2, y + box_height // 2 + 2))
surface.blit(shadow_surf, shadow_rect)
text_surf = font_opt.render(desc, True, WHITE)
text_rect = text_surf.get_rect(center=(x + box_width // 2, y + box_height // 2))
surface.blit(text_surf, text_rect)

instruction = font_instruction.render("Click to choose upgrade", True, LIGHT_GRAY)
surface.blit(instruction, (WIDTH // 2 - instruction.get_width() // 2, y + box_height + 20))

def draw_gameover(self, surface):
overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
overlay.fill((0,0,0,200))
surface.blit(overlay, (0,0))

font_big = get_font(72, bold=True)
text_go = font_big.render("GAME OVER", True, RED)
surface.blit(text_go, (WIDTH//2 - text_go.get_width()//2, 150))

font_info = get_font(36)
info_lines = [
f"Level: {self.player.level}",
f"Score: {self.score}",
f"Wave: {self.wave}",
f"Kills: {self.kills}" ]
y = 280
for line in info_lines:
text = font_info.render(line, True, WHITE)
surface.blit(text, (WIDTH//2 - text.get_width()//2, y))
y += 50

font_restart = get_font(28)
最高分显示
high_score_text = font_info.render(f"Best: {self.high_score} (Wave {self.high_wave})", True, YELLOW)
surface.blit(high_score_text, (WIDTH // 2 - high_score_text.get_width() // 2, y))
y += 50

text_restart = font_restart.render("Press R to restart ESC to quit", True, LIGHT_GRAY)
surface.blit(text_restart, (WIDTH//2 - text_restart.get_width()//2, y + 30))

def load_high_score(self):
"""从文件读取最高分"""
try:
with open("highscore.json", "r") as f:
data = json.load(f)
self.high_score = data.get("score", 0)
self.high_wave = data.get("wave", 0)
except (FileNotFoundError, json.JSONDecodeError):
self.high_score = 0
self.high_wave = 0

def save_high_score(self):
"""如果当前分数更高则保存"""
if self.score > self.high_score:
self.high_score = self.score
self.high_wave = self.wave
try:
with open("highscore.json", "w") as f:
json.dump({"score": self.high_score, "wave": self.high_wave}, f)
except Exception as e:
print("Could not save high score:", e)

==================== 主循环 ====================
def main():
game = Game()
running = True

while running:
clock.tick(FPS)

始终处理事件(包括升级界面)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE and game.game_state == "playing":
game.player.dash()
if event.key == pygame.K_r and game.game_state == "gameover":
game.reset()
if event.key == pygame.K_ESCAPE:
running = False

if game.game_state == "levelup":
只更新玩家内部计时器,不移动
game.player.update()
仍然更新粒子和环境,保持视觉动态
game.particles.update()
for p in game.ambient_particles:
p['x'] += p['vx']
p['y'] += p['vy']
if p['x'] < 0: p['x'] = WIDTH
if p['x'] > WIDTH: p['x'] = 0
if p['y'] < 0: p['y'] = HEIGHT
if p['y'] > HEIGHT: p['y'] = 0
else:
game.update()

screen.fill(BLACK)
game.draw(screen)

pygame.display.flip()

pygame.quit()
sys.exit()

if name == "main":
main()

posted @ 2026-05-29 10:47  twocats  阅读(45)  评论(0)    收藏  举报