2026 秋软件工程个人作业(第二次)

软件工程个人作业2:一箭又一箭小游戏开发

一、作业基础信息

项目 内容
这个作业属于哪个课程 2026-01 软件工程与软件工程实践班级博客
这个作业要求在哪里 2026秋软件工程个人作业(第二次)
这个作业的目标 使用 Python 和 AIGC 完成“一箭又一箭”小游戏
学号 102401139
GitHub 仓库 game1-arrow

二、项目展示

本次开发的“一箭又一箭”小游戏拥有完整的开始界面、游戏主界面、通关界面、失败界面,支持多关卡切换、失误判定、碰撞反馈、重新开始功能。

演示GIF/视频:
demo

开始界面:
start

选关界面:
select

游戏对局界面:
game_1

旋转棋盘界面:
rotate_demo

通关界面:
win

失败界面:
lose


三、项目介绍

1. 游戏规则

棋盘上分布着朝上(^)、下(v)、左(<)、右(>)四个方向的箭头。玩家点击一个箭头后,程序检测其前进方向上的路径:

  • 若箭头与棋盘边界之间没有其他箭头,箭头飞出棋盘并被消除;

  • 若路径上存在其他箭头,箭头不能被消除,并通过晃动、变红和 “前方有阻挡!” 文字给出碰撞反馈,同时消耗 1 次失误机会

  • 每关有 2~3 次失误机会(前 6 关 3 次、后 4 关 2 次),并且每关限时(60~160 秒);清空本关全部箭头即通关并进入下一关;失误次数耗尽或超时则本关失败,可重新开始。

路径判定采用网格单格箭头:只需判断同一行(左右箭头)或同一列(上下箭头)、箭头与边界之间是否还有其他箭头。

2. 界面与功能特色

  • 完整三级界面:开始页 → 游戏页 → 通关/失败结果页;

  • 实时显示:当前关卡、剩余箭头数、剩余失误次数;

  • 箭头飞出动画、碰撞晃动反馈,交互直观;

  • 内置10个可通关合法关卡,布局合理、无死锁;

  • 支持随时重新开始当前关卡。

  • 后三个关卡新增旋转棋盘功能,增强视觉迷惑性。


四、实现思路与核心原理

4.1 箭头和方向怎么表示

方向用四个字符常量 ^ v < >,每个方向对应一组「行增量、列增量」:

DIRECTIONS = {
    '^': (-1, 0),   # 上
    'v': (1, 0),    # 下
    '<': (0, -1),   # 左
    '>': (0, 1),    # 右
}

这里有个容易搞反的地方:屏幕坐标系里 y 轴是向下的,所以第 0 行在最上面,"向上" 是行号减一。我一开始把 ^ 写成 (1, 0),结果所有上下箭头的路径检测都反了 —— 逻辑测试居然还能过(因为测试用例也跟着错了),直到实际试玩时才发现 "明明朝上的箭头点了说被挡"。

棋盘用二维列表存储:grid[r][c] 存放方向字符或 None(空格)。选二维列表而不是字典,是因为棋盘是稠密矩形网格,按坐标直接索引 O (1),而且后面做棋盘旋转时行列互换非常方便。

4.2 路径检测(核心)

这是整个程序最关键的一段。做法是从箭头的下一个格子开始,沿它指的方向一格一格往外走:

def can_fly(self, r, c):
    ch = self.arrow(r, c)
    if ch is None:
        return False
    dr, dc = DIRECTIONS[ch]
    nr, nc = r + dr, c + dc          # 从下一格开始看,不检测自身
    while self.in_bounds(nr, nc):    # 只要还在棋盘内就继续走
        if self.grid[nr][nc] is not None:
            return False              # 碰到箭头 -> 被挡住
        nr += dr
        nc += dc
    return True                       # 走出棋盘了 -> 可以飞出

几个关键点:

  • 从下一格开始,不检测箭头自身所在的格子
  • 只沿方向向量走(同行或同列),不会跨行误判
  • 遇到边界自动停止in_bounds 兜底,不会数组越界
  • 返回纯布尔值,调用方根据结果决定是消除还是扣失误,逻辑和界面完全解耦

时间复杂度 O (棋盘边长),最大 9×9 的棋盘完全不是性能瓶颈。

4.3 游戏状态管理与扩展机制

通过 Session 类统一管理一局游戏的所有状态,界面层只和 Session 打交道:

class Session:
    def __init__(self, level, max_mistakes=3, time_limit=0, rotate_every=0):
        self.board = Board(level)
        self.mistakes_left = max_mistakes
        self.time_left = time_limit      # 0 表示不限时
        self.rotate_every = rotate_every  # 0 表示不旋转
        self.status = 'playing'           # playing / won / lost
        self.undo_stack = []

状态机只有三种:playing(进行中)、won(通关)、lost(失败)。UI 层根据状态渲染不同界面、响应不同操作,切换逻辑清晰。

4.4 棋盘旋转机制(附加玩法)

为什么要加旋转? 基础玩法玩到后面关卡,箭头多了之后策略性会下降 —— 因为边缘的箭头永远可以先飞,玩家只需要 "从外往里消"。加入棋盘旋转后,每消除几个箭头整个棋盘就转 90°,原来在边缘的箭头可能转到中间,迫使玩家重新规划顺序,增加了策略深度和紧张感。

旋转的核心是两个映射:位置映射和方向映射。

位置映射:顺时针旋转 90° 后,原来在 (r, c) 的箭头会移动到 (c, rows-1-r)。可以这样理解:先把行号变成列号(转置),再把新行号翻转(顺时针旋转后原来的第一行变成最后一列)。

方向映射:箭头本身也要跟着转,朝上的变成朝右,朝右的变成朝下,以此类推:^→>→v→<→^

ROTATE\_CW = {'^': '>', '>': 'v', 'v': '<', '<': '^'}

def rotate\_cw(self):

&#x20;   """顺时针旋转90度:位置和方向同步旋转。"""

&#x20;   new\_rows, new\_cols = self.cols, self.rows

&#x20;   new\_grid = \[\[None] \* new\_cols for \_ in range(new\_rows)]

&#x20;   for r in range(self.rows):

&#x20;       for c in range(self.cols):

&#x20;           ch = self.grid\[r]\[c]

&#x20;           if ch is not None:

&#x20;               nr, nc = c, self.rows - 1 - r   # 位置映射

&#x20;               new\_grid\[nr]\[nc] = ROTATE\_CW\[ch] # 方向同步旋转

&#x20;   self.grid = new\_grid

&#x20;   self.rows, self.cols = new\_rows, new\_cols

触发机制:每关配置 rotate_every(第 8 关 = 4,第 9-10 关 = 3),Session 记录 removed_count,每次成功飞出后 +1,达到阈值就调用 rotate_cw(),并设置 just_rotated 标志让 UI 层播放动画。

def click(self, r, c):

&#x20;   # ... 消除箭头 ...

&#x20;   self.removed\_count += 1

&#x20;   if self.rotate\_every > 0 and self.removed\_count >= self.rotate\_every:

&#x20;       if self.board.count() > 0:  # 最后一个箭头直接通关,不旋转

&#x20;           self.board.rotate\_cw()

&#x20;           self.just\_rotated = True

&#x20;           self.removed\_count = 0

&#x20;           self.undo\_stack = \[]     # 坐标全变了,撤销栈失效

动画实现:UI 层在旋转前先 capture_board() 把当前棋盘画到一个独立 Surface 上,旋转后用 BoardRotate 动画把这个旧截图旋转 90°、缩小、淡出,露出下面已经旋转好的新棋盘。0.6 秒完成,视觉上就是 "旧棋盘转走了,新棋盘露出来"。

几个设计要点和踩坑:

  1. 可解性保持:旋转是对称变换,箭头之间的相对阻挡关系不变,所以原来能通关的关卡旋转后仍然能通关,不需要重新验证关卡数据。

  2. 撤销栈必须清空:旋转后所有箭头的坐标都变了,之前记录的 (r, c) 撤销信息全部失效,不清空会导致撤销到错误位置甚至数组越界。

  3. 最后一个箭头不旋转:如果消除后棋盘空了,直接通关,不需要再旋转(旋转一个空棋盘没有意义,还会多播一次动画)。

  4. 旋转动画期间忽略点击:防止玩家在旋转过程中点到坐标还没更新的箭头,导致误判。

五、AIGC 辅助开发过程(3次真实记录)

本作业使用 豆包(Doubao)AI 编程助手 辅助开发。以下是 7 次具有代表性的协作过程:

子任务 借助何种 AIGC 技术 AI 实现或提供了什么 效果如何 人工修改
路径检测 豆包 生成四方向路径检测代码与方向向量表 初版 '^' 方向向量写错(误配为向下),顶边朝上箭头被误判为阻挡 用 T03 边界用例暴露后,修正方向向量为 (-1, 0)
关卡设计 豆包 生成关卡数组与死锁检测思路 第 2、3、4、6 关初版存在同行 / 同列互堵死锁,无法通关 用求解器逐一验证,调整箭头方向消除死锁并重新试玩
碰撞 / 飞出动画 豆包 补全箭头晃动、变色与淡出动画代码 初版把箭头尺寸写成浮点数导致绘制报错,晃动频率偏高 修复整数类型转换,调整晃动幅度与动画时长
自动化测试 豆包 生成 T01~T06 测试用例与 UI 冒烟测试 逻辑测试全部通过 补充了互堵关卡检测、边缘用例,修正了 1 处测试自身的棋盘设计错误
时间限制 豆包 生成倒计时字段与超时判负逻辑 逻辑正确但初版把超时和失误耗尽混为一个失败原因,界面无法区分 增加 lose_reason 区分两种失败,UI 按原因显示不同文案并接入 HUD 倒计时
可解关卡生成器 豆包 给出“逆向构造 + 求解器验证”生成思路 初版随机方向生成的关卡初始可飞箭头多达 10/14,开头太简单 加入向心方向偏好与“方向翻转优化”,初始可飞降到 2~4 支
代码体检 豆包 复查界面代码,发现 get_font 签名被改坏 bold=True 的 4 处调用全部抛 TypeError,UI 一运行就崩 恢复带候选字体遍历与 bold 参数的实现,UI 冒烟测试恢复通过

记录 1:四方向路径检测(边界条件修复)

提出的要求:实现 “箭头沿前进方向到棋盘边界之间没有其他箭头即可飞出” 的检测,四个方向都要支持,且边缘朝外的箭头不能越界。

AI 完成的工作:给出 DIRECTIONS 方向向量表与基于 while 循环的逐格检测代码,结构清晰。

实际效果与问题:初版代码中 '^'(上)的方向向量被写成了 (1, 0)(向下),导致顶边朝上的箭头会被错误地检查下方路径。我在自动化测试里加入了 “顶边朝上的箭头应能飞出” 的边界用例(T03),测试立即失败暴露了该问题。

人工修改:将 '^' 的向量修正为 (-1, 0) 后测试通过。这让我意识到:AI 生成的代码必须用边界用例验证,尤其方向、索引这类容易 “差一点” 的逻辑。对应提交:fix: 修正向上箭头的方向向量,通过 T03 边界用例

记录 2:关卡设计(不可解布局的发现与调整)

提出的要求:设计若干难度递增、且保证存在通关顺序的关卡。

AI 完成的工作:生成了多组关卡数组,并提示可以用贪心求解器验证可解性。

实际效果与问题:把初版关卡交给求解器测试后,第 2、3、4、6 关均被判定 “不可解”。分析发现是两类互堵死锁:如第 3 关初版同一行 >...< 两箭头互相阻挡;第 4 关同一列 v 在上、^ 在下互相阻挡。这类布局即使先删除其他箭头也无法解除(两箭头互相构成对方的阻挡)。

人工修改:根据求解器输出逐个调整死锁处箭头方向(如将 < 改为空格、将 v 改为空格),每次修改后重新运行求解器测试,直到 6 关全部验证可解,并逐关试玩确认通关顺序合理。这个过程让我真正理解了 “可解性” 的本质,也体会到自动验证比肉眼检查可靠得多。对应提交:fix: 调整第 2/3/4/6 关布局,消除互堵死锁并通过求解器验证

记录 3:碰撞与飞出动画(参数与类型问题)

提出的要求:箭头成功飞出时要有滑出 + 淡出动画,被阻挡时要有晃动 + 变色 + 文字提示。

AI 完成的工作:生成了 FlyOut(沿方向移动到棋盘外一格并逐渐透明)与 Shake(正弦波左右晃动并变红)两个动画类,以及浮动提示文字类。

实际效果与问题:初版运行时出现 TypeError: 'float' object cannot be interpreted as an integer—— 箭头绘制尺寸写成了 CELL * 0.55 这种浮点数,直接传给 pygame.drawborder_radius 报错;另外初版晃动频率偏高、幅度衰减不明显,视觉上过于 “抖”。

人工修改:在绘制函数入口将尺寸转为整数;把晃动频率从 48 调到 42、幅度加上 (1 - k) 衰减因子,让动画先剧烈后平息;同时把飞出动画时长控制在约 0.34s。调整后动画反馈清晰、不喧宾夺主。

记录 4:自动化测试(T01~T06)

提出的要求:按作业要求编写 T01~T06 六项测试,并补充关卡合法性检查。

AI 完成的工作:生成了基于 unittest 的测试文件,覆盖 “无阻挡飞出 / 有阻挡失误 -1 / 边缘不越界 / 清空通关 / 失误耗尽失败 / 进行中重开恢复” 六类场景,以及全部关卡的可解性校验。

实际效果与问题:T01~T05 直接通过;T06 测试自身初版选错了棋盘(选了一个本身互堵、没有任何箭头可飞的棋盘),导致断言失败。

人工修改:重新设计 T06 的棋盘(.<>..>:一个可飞出、一个被阻挡),并额外补充了 “互堵关卡应被判定为不可解” 的反例测试和 UI 冒烟测试(虚拟显示驱动下绘制所有界面、跑通通关与失败流程)。最终 19 项测试全部通过

记录 5:时间限制(失败原因区分)

提出的要求:给每关增加倒计时,时间耗尽直接判负;重新开始要恢复剩余时间。

AI 完成的工作:在 Session 中增加 time_limit / time_left 字段与 tick(dt) 方法,倒计时归零置为 lost;UI 层每帧调用 tick 并更新 HUD。

实际效果与问题:初版把“失误耗尽”和“时间耗尽”都归为 lost,失败界面无法区分原因,用户看到文案永远是“失误次数已用完”。另外时间放在逻辑层后,界面层最初忘记在 update 中推进,倒计时根本不走。

人工修改:增加 lose_reason'mistakes' / 'time')区分两种失败,失败弹窗按原因显示不同文案;在 update 主循环接入 session.tick(dt),超时先弹出“时间到!”浮动提示再切失败界面;HUD 右上角显示倒计时,最后 30 秒变红。新增 T07 共 7 个时间用例(倒计时递减、超时判负、失败原因、重开恢复、通关停表等),并补了一个 UI 冒烟测试验证“超时后进入失败界面”。

记录 6:可解关卡生成器(难度控制)

提出的要求:把 6 关扩充到 10 关、明显加大难度,同时保证每个新关卡都存在通关顺序。

AI 完成的工作:给出“逆向构造”思路——从空棋盘开始,每放置一个箭头就用贪心求解器验证“放置后仍可解”,不满足则回退,从构造上保证可解性。

实际效果与问题:初版生成器随机四向摆放,生成的关卡虽然可解,但初始可直接飞出的箭头多达 10~14 个(一半以上),开头毫无解谜感;且方向分布偏向同一种,视觉单调。

人工修改:加入两个改进——①向心方向偏好:箭头方向按位置偏向棋盘中心(连续权重),边缘箭头大多朝内被锁住;②方向翻转优化:生成后对每个“初始可飞”箭头尝试换向,只要保持可解且减少初始可飞数就接受,迭代直到初始可飞数压到 2~4 支。最终第 7~10 关为 7×79×9、1420 支箭头、失误上限 2 次、限时 120~160 秒,与前三关形成明显难度阶梯。

记录 7:代码体检(get_font 参数错误)

提出的要求:对现有代码做一次全面检查,找出“程序跑不起来”的问题。

AI 完成的工作:运行 UI 冒烟测试,发现 ui.pyget_font 函数被改成了只接受 size 一个参数,而全文件有 4 处调用仍传 bold=True(关卡按钮、HUD 标题、失败弹窗、浮动文字),全部抛 TypeError——游戏一启动就会崩溃;同时发现该版本只尝试 “simhei” 一种字体、找不到时可能退回默认字体丢失中文。

人工修改:恢复 get_font(size, bold=False) 的完整实现(遍历候选中文字体 + 找不到退回默认字体),跑通全部 UI 冒烟测试。这再次印证:AI 修改代码后必须用测试回归,签名变了而调用没跟上是最常见的隐性炸弹

六、项目测试结果

测试环境:Windows + Python 3.14.7 + pygame-ce 2.5.8,运行命令:

python -m unittest discover -s tests -v
编号 测试内容 预期结果 实际结果 是否通过
T01 点击前方无阻挡的箭头 箭头飞出棋盘并消失 箭头消失,剩余数量减少 ✅ 通过
T02 点击前方有阻挡的箭头 箭头不消失,失误次数减 1 箭头保留,失误 3→2 ✅ 通过
T03 点击位于边缘且朝向棋盘外的箭头 箭头正常消失,不发生越界错误 上 / 下 / 左 / 右及角上箭头均正常飞出 ✅ 通过
T04 消除本关全部箭头 显示通关并进入下一关 状态变为 won,自动切换到通关界面,可进入下一关 ✅ 通过
T05 失误次数耗尽 显示失败并允许重新开始 状态变为 lost,重新开始后棋盘与失误恢复 ✅ 通过
T06 游戏进行中重新开始 箭头布局和失误次数恢复 布局、失误次数、撤销栈均恢复初始 ✅ 通过
T07 每关限时倒计时 超时判负、重开恢复时间、通关停表 倒计时递减,超时进入失败界面且原因显示为“时间耗尽” ✅ 通过
附加 全部 10 关可解性验证 每关都存在通关顺序 求解器验证全部可解 ✅ 通过
附加 互堵关卡检测 不可解布局能被识别 死锁关卡被判定为不可解 ✅ 通过
附加 UI 冒烟测试 各界面可绘制、流程正常 全部界面绘制、通关 / 失败 / 超时流程无异常 ✅ 通过

测试小结:共 28 项测试全部通过。测试不仅验证了需求,还真实暴露了开发中的多个 Bug(方向向量错误、关卡死锁、动画类型错误、get_font 参数错误、时间不推进),体现了 “先写用例、再验证” 的价值。除自动化测试外,10 个关卡均已用求解器验证可解,并通过 tools/validate_levels.py 复核。

七、PSP 个人软件开发过程量表

任务 预估耗时(小时) 实际耗时(小时) 差异(小时)
需求分析与游戏设计 1.0 0.5 -0.5
Python 与图形库学习 2.0 1.5 -0.5
游戏界面实现 4.0 3.0 -1.0
路径与碰撞逻辑实现 3.0 2.0 -1.0
关卡设计(含扩充至 10 关与生成器) 2.0 3.5 +1.5
AIGC 辅助开发(含时间限制与代码体检) 2.0 2.5 +0.5
测试与修改(含 T07 与 UI 超时用例) 2.0 3.0 +1.0
README 与博客撰写 3.0 3.5 +0.5
合计 19.0 19.5 +0.5

八、心得体会

AI 带来的帮助:AIGC 把 “从零搭框架” 变成了 “在草稿上修改”。路径检测、动画类、测试用例这些成熟模式,AI 能一次性给出可用版本,让我把精力集中在真正需要判断的地方 —— 边界条件、关卡可解性、交互细节。特别是 “先用测试暴露问题、再让 AI 和人工一起修” 的工作方式,开发效率明显提高。

出现的问题:AI 生成代码最大的风险是 “看起来对、实际错”。本次 AI 的路径检测方向向量写错、关卡出现互堵死锁、动画参数类型错误,都是只靠肉眼检查发现不了的,全部要靠边界用例和求解器这类可验证的手段兜底。这提醒我:AI 是 “高效的草稿生成器”,不是 “可靠的代码评审员”,把 AI 代码直接当正确答案是最危险的用法。

我的收获:一是真正理解了本游戏路径检测与 “互堵死锁” 的本质(同行 ><、同列 v^ 必然无解),并会用贪心求解器证明关卡可解;二是掌握了 “逻辑层与界面层解耦 + 自动化测试” 的工程习惯,使逻辑可以脱离图形界面被反复验证;三是对 AI 协作有了更理性的定位 —— 提问要具体(“请实现四方向路径检测并处理边缘越界”),验证要独立(测试、试玩、求解器),修改要敢下手;四是这次查错让我亲身体会到,AI 修改过的代码必须用测试回归——get_font 签名被改坏这种“一启动就崩”的问题,正是靠 UI 冒烟测试第一时间暴露的;而“逆向构造 + 求解器验证”的关卡生成思路,让我不用再靠肉眼一个个排查死锁,扩关卡从“碰运气”变成了“可复现的工序”。

posted @ 2026-09-18 16:14  platforzation  阅读(0)  评论(0)    收藏  举报