20254107实验四Python综合实践

一.综合实验报告(防止源代码占用过大篇幅,将其放在实验报告后面展示)
运行视频:【Python实验运行视频-哔哩哔哩】 https://b23.tv/tK1A98O
(一)实验分析

  1. 选题
    本作业要求开发一个Python综合应用。我选择制作一款FPS打地鼠游戏,结合瞄准射击与打地鼠玩法,以此综合运用Python知识。可以锻炼手眼协调能力和反应力。

  2. 游戏机制分析
    (1)玩家需在60秒内尽可能多地击中从洞口冒出的敌人。
    (2)敌人分为头部和身体两部分,头部奖励更高但面积更小,考验瞄准精度。
    (3)随着时间推移,敌人出现频率加快、停留时间缩短,迫使玩家加快反应速度。
    (4)命中率、爆头率、击败率的实时反馈能帮助玩家调整瞄准策略。

  3. 技术选型
    使用Python标准库tkinter,无需安装第三方库,确保在任何环境下均可运行。

(二)实验设计
1.功能介绍
(1)鼠标控制白色空心十字准星,实时跟随。
(2)敌人从9个固定洞口随机出现,身体为柱状矩形,头部为圆形。
(3)击中头部一枪毙命,得20分;击中身体需两枪,第一枪5分,第二枪10分。
(4)击中时产生黄色光圈+橙色光晕特效,0.1秒消失。
(5)实时显示命中率、爆头率、击败率(爆头/命中)。
(6)60秒倒计时,时间归零游戏结束。
(7)动态难度:每20秒难度提升,敌人生成间隔缩短、存活时间减少。
(8)最高分保存至文件,游戏结束后更新,下次启动显示。
(9)重新开始按钮,重置所有状态。

  1. 程序结构
    (1)敌人管理:生成、移除、更新血条。

(2)射击判定:先检测头部(圆形),再检测身体(矩形)。

(3)难度调节:根据运行时间动态调整生成间隔和消失帧数。总共三级难度。

(4)游戏循环:每16ms执行,控制敌人生成与消失。

(5)持久化:读写最高分文件。

  1. 数据结构
    (1)用一个字典记录每个敌人:键是敌人身体的图形ID,值里面存了该敌人所在的位置、剩余血量(2滴)、头部的圆心坐标和半径、以及它出生的时刻(帧计数)。

(2)一个整数变量(帧计数),每次循环加1,用来判断敌人活了多久。

(3)三个整数变量分别记录总射击次数、命中次数、爆头次数,用于计算百分比。

  1. 核心算法
    (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. 关键代码截取
    (1)敌人生成
    image
    (2)射击判定标准
    image
    (3)更新血条显示
    屏幕截图 2026-05-26 204154

(4)游戏主循环
image
2. 调试与修正
(1)准星无法击中:初始先判断身体后判断头,因身体覆盖头部导致爆头失效。交换顺序后修复。

(2)敌人出现过密,有时同时出现两个目标,难以瞄准:加入独立生成计数器

(四)实验结果

  1. 功能验证
    (1)准星跟随鼠标:移动鼠标时白色空心十字实时跟随,无延迟。
    (2)敌人随机出现:9个洞口轮换出现敌人,已占洞口不重复生成。
    (3)爆头与身体伤害:击中头部立即消灭,得20分;击中身体第一枪血条减半,得5分,第二枪消灭,再得5分,总共得十分。
    (4)命中特效:每次击中产生黄色光圈和橙色光晕,0.1秒后消失。
    (5)统计显示:命中率、爆头率、击败率随每次射击实时更新,便于玩家观察自身水平。
    (6)倒计时与动态难度:60秒倒计时正常;每20秒难度提升,敌人出现更快、存活更短,难度等级显示正确。
    (7)最高分保存:游戏结束时若超过历史最高分则写入文件,下次启动显示正确。
    (8)重新开始:点击按钮后所有状态重置,敌人清空,游戏可重新开始。

  2. 稳定性测试
    经过多次完整游戏流程(每次60秒)及反复点击重新开始,程序未出现崩溃、卡死或异常退出。重新开始游戏后也无异常。

  3. 运行界面
    窗口上半部控制栏显示各项数据,下半部深色砖墙画布上有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》

posted @ 2026-06-01 23:05  20254107刘政  阅读(35)  评论(0)    收藏  举报