20254107实验四Python综合实践
一.综合实验报告(防止源代码占用过大篇幅,将其放在实验报告后面展示)
运行视频:【Python实验运行视频-哔哩哔哩】 https://b23.tv/tK1A98O
(一)实验分析
-
选题
本作业要求开发一个Python综合应用。我选择制作一款FPS打地鼠游戏,结合瞄准射击与打地鼠玩法,以此综合运用Python知识。可以锻炼手眼协调能力和反应力。 -
游戏机制分析
(1)玩家需在60秒内尽可能多地击中从洞口冒出的敌人。
(2)敌人分为头部和身体两部分,头部奖励更高但面积更小,考验瞄准精度。
(3)随着时间推移,敌人出现频率加快、停留时间缩短,迫使玩家加快反应速度。
(4)命中率、爆头率、击败率的实时反馈能帮助玩家调整瞄准策略。 -
技术选型
使用Python标准库tkinter,无需安装第三方库,确保在任何环境下均可运行。
(二)实验设计
1.功能介绍
(1)鼠标控制白色空心十字准星,实时跟随。
(2)敌人从9个固定洞口随机出现,身体为柱状矩形,头部为圆形。
(3)击中头部一枪毙命,得20分;击中身体需两枪,第一枪5分,第二枪10分。
(4)击中时产生黄色光圈+橙色光晕特效,0.1秒消失。
(5)实时显示命中率、爆头率、击败率(爆头/命中)。
(6)60秒倒计时,时间归零游戏结束。
(7)动态难度:每20秒难度提升,敌人生成间隔缩短、存活时间减少。
(8)最高分保存至文件,游戏结束后更新,下次启动显示。
(9)重新开始按钮,重置所有状态。
- 程序结构
(1)敌人管理:生成、移除、更新血条。
(2)射击判定:先检测头部(圆形),再检测身体(矩形)。
(3)难度调节:根据运行时间动态调整生成间隔和消失帧数。总共三级难度。
(4)游戏循环:每16ms执行,控制敌人生成与消失。
(5)持久化:读写最高分文件。
- 数据结构
(1)用一个字典记录每个敌人:键是敌人身体的图形ID,值里面存了该敌人所在的位置、剩余血量(2滴)、头部的圆心坐标和半径、以及它出生的时刻(帧计数)。
(2)一个整数变量(帧计数),每次循环加1,用来判断敌人活了多久。
(3)三个整数变量分别记录总射击次数、命中次数、爆头次数,用于计算百分比。
- 核心算法
(1)碰撞检测:先判断鼠标到头部中心的距离,代码为if math.hypot(mx - 头心x, my - 头心y) <= 头半径:,若成立则爆头;否则判断是否在身体矩形内if (x-12 <= mx <= x+12) and (y-14 <= my <= y+14):。
(2)生成控制:使用 self.生成等待 计数器,每帧减1,归零时调用 self.生成敌人() 并重置计数器。
(3)动态难度:每20秒难度+1,通过修改 self.生成间隔帧 (从60降至30)和 (self.消失帧数) (从80降至40)实现。
(4)命中特效:调用 (self.命中特效(x, y)) 绘制同心圆,100ms后删除。
(三)实现过程
- 关键代码截取
(1)敌人生成
![image]()
(2)射击判定标准
![image]()
(3)更新血条显示
![屏幕截图 2026-05-26 204154]()
(4)游戏主循环

2. 调试与修正
(1)准星无法击中:初始先判断身体后判断头,因身体覆盖头部导致爆头失效。交换顺序后修复。
(2)敌人出现过密,有时同时出现两个目标,难以瞄准:加入独立生成计数器
(四)实验结果
-
功能验证
(1)准星跟随鼠标:移动鼠标时白色空心十字实时跟随,无延迟。
(2)敌人随机出现:9个洞口轮换出现敌人,已占洞口不重复生成。
(3)爆头与身体伤害:击中头部立即消灭,得20分;击中身体第一枪血条减半,得5分,第二枪消灭,再得5分,总共得十分。
(4)命中特效:每次击中产生黄色光圈和橙色光晕,0.1秒后消失。
(5)统计显示:命中率、爆头率、击败率随每次射击实时更新,便于玩家观察自身水平。
(6)倒计时与动态难度:60秒倒计时正常;每20秒难度提升,敌人出现更快、存活更短,难度等级显示正确。
(7)最高分保存:游戏结束时若超过历史最高分则写入文件,下次启动显示正确。
(8)重新开始:点击按钮后所有状态重置,敌人清空,游戏可重新开始。 -
稳定性测试
经过多次完整游戏流程(每次60秒)及反复点击重新开始,程序未出现崩溃、卡死或异常退出。重新开始游戏后也无异常。 -
运行界面
窗口上半部控制栏显示各项数据,下半部深色砖墙画布上有9个黑色洞口。敌人为绿色矩形身体加金色圆形头部,红色爱心血条。鼠标隐藏,白色空心十字准星跟随移动,命中时短暂闪光。
二.源代码:
import tkinter as tk
import random
import math
import time
import os
W, H = 1000, 650
洞列表 = [
(150, 120), (350, 120), (550, 120), (750, 120), (900, 120),
(200, 280), (500, 280), (800, 280),
(550, 450)
]
洞半径 = 35
class 打地鼠游戏:
def __init__(self, 根窗口):
self.根 = 根窗口
根窗口.title("FPS打地鼠 - 爆头射击")
根窗口.geometry(f"{W}x{H+80}")
根窗口.resizable(False, False)
# 画布
self.画布 = tk.Canvas(根窗口, width=W, height=H, bg='#3a2a1a')
self.画布.pack()
self.画背景()
# 游戏状态
self.游戏中 = True
self.分数 = 0
self.剩余秒数 = 60
self.开始时间 = None
self.帧计数 = 0
self.难度等级 = 0
self.生成等待 = 0 # 生成倒计时(帧)
self.生成间隔帧 = 60 # 初始60帧生成一个敌人(约1秒)
self.消失帧数 = 80 # 敌人存活帧数
self.敌人数据 = {} # {身体id: 数据字典}
# 统计
self.总射击 = 0
self.命中数 = 0
self.爆头数 = 0
# 最高分
self.最高分 = self.载入最高分()
# 准星
self.准星图形 = []
self.画布.bind("<Motion>", self.移动准星)
self.画布.bind("<Button-1>", self.开枪)
根窗口.bind("<Escape>", lambda e: 根窗口.quit())
# UI面板
self.面板 = tk.Frame(根窗口)
self.面板.pack(fill=tk.X)
self.得分标签 = tk.Label(self.面板, text=f"得分: {self.分数}", font=("微软雅黑", 11))
self.得分标签.pack(side=tk.LEFT, padx=6)
self.时间标签 = tk.Label(self.面板, text=f"时间: {self.剩余秒数}", font=("微软雅黑", 11))
self.时间标签.pack(side=tk.LEFT, padx=6)
self.最高标签 = tk.Label(self.面板, text=f"最高分: {self.最高分}", font=("微软雅黑", 11))
self.最高标签.pack(side=tk.LEFT, padx=6)
self.命中率标签 = tk.Label(self.面板, text="命中率: 0%", font=("微软雅黑", 11))
self.命中率标签.pack(side=tk.LEFT, padx=6)
self.爆头率标签 = tk.Label(self.面板, text="爆头率: 0%", font=("微软雅黑", 11))
self.爆头率标签.pack(side=tk.LEFT, padx=6)
self.击败率标签 = tk.Label(self.面板, text="击败率: 0%", font=("微软雅黑", 11))
self.击败率标签.pack(side=tk.LEFT, padx=6)
self.难度标签 = tk.Label(self.面板, text=f"难度: {self.难度等级}", font=("微软雅黑", 10))
self.难度标签.pack(side=tk.LEFT, padx=6)
self.重开按钮 = tk.Button(self.面板, text="重新开始", command=self.重置游戏)
self.重开按钮.pack(side=tk.RIGHT, padx=10)
# 启动主循环
self.倒计时更新()
self.游戏主循环()
# ---------- 画面绘制 ----------
def 画背景(self):
self.画布.create_rectangle(0, 0, W, H, fill='#5a4a3a', outline='')
for x in range(0, W, 60):
self.画布.create_line(x, 0, x, H, fill='#4a3a2a', width=2)
for y in range(0, H, 60):
self.画布.create_line(0, y, W, y, fill='#4a3a2a', width=2)
for (x, y) in 洞列表:
self.画布.create_oval(x-洞半径, y-洞半径, x+洞半径, y+洞半径, fill='#1a1a1a', outline='#888', width=3)
# ---------- 敌人管理 ----------
def 生成敌人(self):
"""在空闲洞口生成一个新敌人(柱体身体 + 圆形头部)"""
已占洞口 = [数据['pos'] for 数据 in self.敌人数据.values()]
可用洞口 = [洞 for 洞 in 洞列表 if 洞 not in 已占洞口]
if not 可用洞口:
return
x, y = random.choice(可用洞口)
# 身体:矩形柱体
身体 = self.画布.create_rectangle(x-12, y-14, x+12, y+14, fill='#6B8E23', outline='#556B2F', width=2)
# 头部:金色圆形
头半径 = 14
头心 = (x, y-22)
hx, hy = 头心
头部 = self.画布.create_oval(hx-头半径, hy-头半径, hx+头半径, hy+头半径, fill='#FFD700', outline='#DAA520', width=2)
# 眼睛
左眼 = self.画布.create_oval(hx-7, hy-5, hx-3, hy-1, fill='white')
右眼 = self.画布.create_oval(hx+3, hy-5, hx+7, hy-1, fill='white')
瞳孔L = self.画布.create_oval(hx-6, hy-4, hx-4, hy-2, fill='black')
瞳孔R = self.画布.create_oval(hx+4, hy-4, hx+6, hy-2, fill='black')
# 血条文字
血条 = self.画布.create_text(x, y+22, text="❤️❤️", fill='red', font=("Arial", 11))
self.敌人数据[身体] = {
'pos': (x, y),
'hp': 2,
'图形列表': [身体, 头部, 左眼, 右眼, 瞳孔L, 瞳孔R, 血条],
'头中心': 头心,
'头半径': 头半径,
'出生帧': self.帧计数
}
def 移除敌人(self, 身体id):
for 图 in self.敌人数据[身体id]['图形列表']:
self.画布.delete(图)
del self.敌人数据[身体id]
def 更新血条(self, 身体id):
hp = self.敌人数据[身体id]['hp']
x, y = self.敌人数据[身体id]['pos']
血条id = self.敌人数据[身体id]['图形列表'][-1]
文字 = "❤️" * hp if hp > 0 else "💀"
self.画布.itemconfig(血条id, text=文字)
# ---------- 射击与命中(先头部后身体)----------
def 命中特效(self, x, y):
r = 10
外圈 = self.画布.create_oval(x-r, y-r, x+r, y+r, outline='yellow', width=3)
内圈 = self.画布.create_oval(x-r//2, y-r//2, x+r//2, y+r//2, fill='orange', outline='')
self.根.after(100, lambda: self.画布.delete(外圈))
self.根.after(100, lambda: self.画布.delete(内圈))
def 开枪(self, event):
if not self.游戏中:
return
mx, my = event.x, event.y
self.总射击 += 1
打中 = False
for 身体id, 数据 in list(self.敌人数据.items()):
x, y = 数据['pos']
# 先判断是否击中头部(圆形区域)
hx, hy = 数据['头中心']
if math.hypot(mx - hx, my - hy) <= 数据['头半径']:
打中 = True
self.分数 += 20
self.爆头数 += 1
self.移除敌人(身体id)
self.命中数 += 1
self.命中特效(mx, my)
self.得分标签.config(text=f"得分: {self.分数}")
break
# 再判断身体矩形区域
if (x-12 <= mx <= x+12) and (y-14 <= my <= y+14):
打中 = True
self.分数 += 5
数据['hp'] -= 1
if 数据['hp'] <= 0:
self.分数 += 5
self.移除敌人(身体id)
else:
self.更新血条(身体id)
self.命中数 += 1
self.命中特效(mx, my)
self.得分标签.config(text=f"得分: {self.分数}")
break
# 更新统计显示
if self.总射击 > 0:
命中率 = self.命中数 / self.总射击 * 100
爆头率 = self.爆头数 / self.总射击 * 100
击败率 = self.爆头数 / self.命中数 * 100 if self.命中数 > 0 else 0
self.命中率标签.config(text=f"命中率: {命中率:.1f}%")
self.爆头率标签.config(text=f"爆头率: {爆头率:.1f}%")
self.击败率标签.config(text=f"击败率: {击败率:.1f}%")
# ---------- 准星 ----------
def 移动准星(self, event):
for 图 in self.准星图形:
self.画布.delete(图)
x, y = event.x, event.y
sz = 14
gap = 5
self.准星图形 = [
self.画布.create_line(x-sz, y, x-gap, y, fill='white', width=2),
self.画布.create_line(x+gap, y, x+sz, y, fill='white', width=2),
self.画布.create_line(x, y-sz, x, y-gap, fill='white', width=2),
self.画布.create_line(x, y+gap, x, y+sz, fill='white', width=2)
]
# ---------- 倒计时 ----------
def 倒计时更新(self):
if not self.游戏中:
return
if self.开始时间 is None:
self.开始时间 = time.time()
已过 = int(time.time() - self.开始时间)
self.剩余秒数 = max(0, 60 - 已过)
self.时间标签.config(text=f"时间: {self.剩余秒数}")
if self.剩余秒数 <= 0:
self.游戏结束()
else:
self.根.after(1000, self.倒计时更新)
# ---------- 游戏结束与重置 ----------
def 游戏结束(self):
self.游戏中 = False
self.保存最高分()
self.画布.create_text(W//2, H//2-50, text="游戏结束", font=("微软雅黑", 30), fill="red", tag="结束")
self.画布.create_text(W//2, H//2, text=f"得分: {self.分数}", font=("微软雅黑", 20), tag="结束")
self.画布.create_text(W//2, H//2+50, text=f"最高分: {self.最高分}", font=("微软雅黑", 20), tag="结束")
def 重置游戏(self):
# 清除所有敌人
for 身体id in list(self.敌人数据.keys()):
self.移除敌人(身体id)
# 重置变量
self.分数 = 0
self.剩余秒数 = 60
self.游戏中 = True
self.难度等级 = 0
self.生成间隔帧 = 60
self.消失帧数 = 80
self.生成等待 = 0
self.开始时间 = None
self.帧计数 = 0
self.总射击 = 0
self.命中数 = 0
self.爆头数 = 0
# 重置UI
self.得分标签.config(text=f"得分: {self.分数}")
self.时间标签.config(text=f"时间: {self.剩余秒数}")
self.难度标签.config(text=f"难度: {self.难度等级}")
self.命中率标签.config(text="命中率: 0%")
self.爆头率标签.config(text="爆头率: 0%")
self.击败率标签.config(text="击败率: 0%")
# 删除结束文字
self.画布.delete("结束")
# 重新启动倒计时(会重新设置开始时间)
self.倒计时更新()
# ---------- 最高分持久化 ----------
def 载入最高分(self):
if os.path.exists("fps_mole_best.txt"):
with open("fps_mole_best.txt", "r") as f:
try:
return int(f.read())
except:
return 0
return 0
def 保存最高分(self):
if self.分数 > self.最高分:
self.最高分 = self.分数
with open("fps_mole_best.txt", "w") as f:
f.write(str(self.最高分))
self.最高标签.config(text=f"最高分: {self.最高分}")
# ---------- 游戏主循环 ----------
def 游戏主循环(self):
if self.游戏中:
self.帧计数 += 1
# 动态难度(每20秒)
if self.开始时间 is not None:
已过秒 = int(time.time() - self.开始时间)
new_level = 已过秒 // 20
if new_level > self.难度等级:
self.难度等级 = new_level
# 难度越高,生成越快,消失越快
self.生成间隔帧 = max(30, 60 - new_level * 8)
self.消失帧数 = max(40, 80 - new_level * 12)
self.难度标签.config(text=f"难度: {self.难度等级}")
# 使用计数器控制敌人出生(避免瞬间生成多个)
if self.生成等待 <= 0:
if len(self.敌人数据) < len(洞列表):
self.生成敌人()
self.生成等待 = self.生成间隔帧
else:
self.生成等待 -= 1
# 移除存活过久的敌人
for 身体id, 数据 in list(self.敌人数据.items()):
if self.帧计数 - 数据['出生帧'] > self.消失帧数:
self.移除敌人(身体id)
self.根.after(16, self.游戏主循环)
if __name__ == "__main__":
主窗口 = tk.Tk()
游戏 = 打地鼠游戏(主窗口)
主窗口.mainloop()
三.课程总结
(一)、Python基础
特点:跨平台、解释型、面向对象、动态类型。
环境:IDE(如PyCharm)调用解释器(如CPython)执行代码。
语法:注释(#、''')、保留字(if/for等)、标识符规则。
数据类型:整型、浮点型、字符串、布尔型。
运算符:算术、逻辑、赋值、比较。
输入输出:input()(返回字符串,需int()转换)、print()。
流程控制:if-elif-else、for/while循环(break/continue)。
(二)、序列
种类:字符串(不可变)、列表(可变)、元组(不可变)、range。
以貌取序列:[]列表、()元组、''字符串、range()。
list vs tuple:list可变,tuple不可变;list可增删改,tuple不能。
set vs dict:set无序不重复,dict键值对;均非序列。
切片:[start:stop:step],如[::-1]反转。
排序:list.sort()(原地)、sorted()(返回新列表)。
列表推导式:[表达式 for 变量 in 可迭代对象 if 条件]。
(三)、字符串与正则
字符串方法:count()、find()、lower()/upper()。
正则表达式:模式匹配,如手机号13[4-9]\d{8}(134~139号段)、15[01289]\d{8}(150/151/152/158/159)。
(四)、文件与数据库
文件操作:open()、read/write/seek/close;模式'r'、'w'、'a'、'b'(二进制,用于非文本)。
SQLite:轻量嵌入式数据库;基本操作(增删改查:INSERT、DELETE、UPDATE、SELECT);事务(commit/rollback/savepoint)。
(五)、爬虫
定义:自动抓取解析网页数据,用于搜索、监控等。
法律风险:可能触犯《刑法》《网络安全法》等。
步骤:爬取(requests等) + 解析(BeautifulSoup、lxml、re等)。
robots.txt:网站爬虫协议,应遵守。
代理:隐藏IP、突破频率限制。
请求头(Headers):伪造浏览器User-Agent等可绕过部分反爬。
四、课程感想和建议
(一)感想
身为文科生,我还是第一次接触Python,对该课程充满了新奇感,老师的讲课方式也很有趣,我之前没有编程基础,也从未接触过代码,Python的学习过程也很坎坷,虽然最后我对Python的掌握程度也很有限,但是深刻理解了其应用场景的广泛和学习Python的重大意义,如果后续有机会,我也愿意继续深入学习Python
(二)建议
对于没有相关基础的文科生来说,该课程难度有些过大,函数部分难以理解,我觉得课程可以更细致一些,多讲一些简单基础的部分
总之,很高兴能在本学期进行Python课程的学习,补充相关知识。
参考资料
《零基础学python》



浙公网安备 33010602011771号