zzx0607

导航

软件工程第二次作业

2026秋软件工程个人作业(第二次)—— 一箭又一箭

项目 内容
这个作业属于哪个课程 202601 软件工程(福州大学 计算机与大数据学院)
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16717
这个作业的目标 使用 Python 和 AIGC 完成"一箭又一箭"小游戏
学号 102401121
GitHub 仓库 https://github.com/zzx0607/arrow-and-arrow

一、项目展示

开始界面(正中为游戏标题,下方依次是四条规则说明与"开始游戏 / 随机挑战"两个入口按钮):

开始界面

对局界面(顶部信息栏从左到右显示关卡进度、剩余箭头、剩余失误,底部固定一个"重新开始"按钮):

image

箭头成功飞出时(沿指向平滑滑出棋盘并消失,顶部箭头计数同步减一):

image

点击被挡箭头时(原地晃动并变红,顶部弹出"前方有箭头阻挡!失误 -1"提示):

image

通关画面(半透明遮罩覆盖棋盘,中央显示"第 X 关通过"与下一关按钮):

image

失败画面(失误机会全部消耗后显示"本关失败",可从本关重来):

image

二、项目介绍

2.1 玩法规则

棋盘里散布着若干带指向的箭头(↑↓←→),规则围绕"哪个箭头可以先点"展开:

  • 玩家选定一个箭头后,程序从它紧挨着的下一格出发,顺着它的指向一直检查到棋盘边缘;
  • 这条检查路线上一路为空,则该箭头飞出棋盘并消失
  • 路线中途只要碰到另一个箭头,就判定被阻挡:箭头原地晃动变红提示碰撞,同时扣除一次失误机会;
  • 本关全部箭头清空 → 进入下一关;3 次失误全部耗尽 → 本关失败,可重新开局。

2.2 界面结构

  • 整款游戏共三种界面形态:菜单(menu)对局(playing)结算浮层(LEVEL_CLEAR / GAME_OVER / ALL_CLEAR),彼此之间靠状态机切换;
  • 对局界面顶部实时刷新三组数据:当前关卡 / 总关数、剩余箭头数、剩余失误数;
  • 底部常驻"重新开始"按钮,对局中按键盘 R 也能立即重开本关;
  • 视觉反馈分两类:成功时箭头沿指向滑出棋盘;失败时箭头变红并左右抖动 0.45 秒,顶部同时出现提示文字。

2.3 主要功能与特色

  • 全部由 Python + pygame-ce 实现,不依赖任何外部素材:箭头图形用 pygame 直接绘制,中文用系统自带的微软雅黑字体渲染;
  • 规则与显示彻底分离game.py 只做数据与判定,不 import pygame,因此不开窗口也能在命令行跑单元测试;
  • 内置 4 个手工编排、并由求解器逐一验证可通关的关卡(难度逐关提升);
  • 附加玩法:随机挑战模式——每次随机生成 5×5、含 8 个箭头的棋盘,经求解器确认可通关后才呈现给玩家。

三、实现思路

3.1 箭头、方向与关卡的表示

棋盘底层是一个按行索引的二维列表grid[row][col] 要么是 None(空格),要么是一个 Arrow 对象。方向只用单个字符编码:U/D/L/R 分别对应上/下/左/右,再经 DIRS 字典换算成行列坐标的增量:

DIRS = {
    "U": (0, -1),   # 行号减小
    "D": (0, 1),    # 行号增大
    "L": (-1, 0),   # 列号减小
    "R": (1, 0),    # 列号增大
}

class Arrow:
    def __init__(self, row, col, direction):
        self.row = row          # 行
        self.col = col          # 列
        self.direction = direction  # "U" / "D" / "L" / "R"

每个关卡在 levels.py 里就是一行配置字典,例如第 1 关(4×4):

→  ·  ↑  ·
·  ·  ·  ·
↓  ·  ←  ·
·  ·  ·  ·

3.2 路径检测(核心算法)

整个游戏最关键的判断集中在 Board.can_fly(row, col)。它的思路是沿箭头方向在棋盘内"走一条射线":起点取箭头相邻的下一格(而不是箭头自己),随后一格一格推进,途中碰到任何非空格即立刻返回"被挡";顺利走到棋盘外则返回"可飞":

def can_fly(self, row, col):
    arrow = self.get(row, col)
    if arrow is None:
        return False
    dcol, drow = DIRS[arrow.direction]
    r, c = row + drow, col + dcol      # 从相邻一格开始扫,不看自己
    while 0 <= r < self.rows and 0 <= c < self.cols:
        if self.grid[r][c] is not None:  # 路上还有别的箭头 → 挡住
            return False
        r += drow
        c += dcol
    return True                          # 一路走到边界都空 → 可飞

为什么单方向线性扫描就足够?因为题目只要求考察"同一行/同一列上、箭头与边界之间是否还有别的箭头",不存在需要绕路的场景,所以用不上 BFS/DFS 这类搜索算法。

边界处理细节:循环条件同时约束行、列下标不越界,等于把"走到边界外"统一表达成"循环自然结束"。箭头本来就贴着边、且指向棋盘外时,相邻格直接越界,循环体一次都不执行就返回 True——不会出现下标 -1 之类的数组越界错误。测试 T03 用四方向边缘箭头专门验证了这一点。

3.3 点击与状态流转

Game.click(row, col) 是逻辑层唯一入口,每次点击返回一个事件字典:

  • 点到空格 → {"event": "empty"},不改变任何状态、不扣失误;
  • 点到可飞箭头 → 该格置空,返回 {"event": "flew"},界面层据此播放飞出动画;若这是本关最后一个箭头则附带 cleared 标记并切换状态机;
  • 点到被挡箭头 → 棋盘保持原样,失误次数减 1,返回 {"event": "blocked"},界面层播放晃动动画;失误扣到 0 则进入失败状态。

界面层(ui.py)只负责画图和转发鼠标事件,"能不能飞""是否通关"全部交给 game.py 裁决。此外,动画播放期间界面层会拒绝新的棋盘点击,防止操作与画面状态脱节。

3.4 关卡的可解性验证

关卡定稿前,我没有凭感觉判断"应该能过",而是用贪心求解器 Board.find_solution() 实际解一遍:循环找任意一个当前可飞的箭头消掉,直到棋盘清空或卡死。这里有一个关键的性质支撑贪心的正确性——消除箭头只会减少阻挡,绝不会让原本可飞的箭头变得不可飞,因此"贪心能清空棋盘"与"关卡存在解"是等价的。4 个固定关卡分别能用 4、5、5、6 步清空;随机挑战模式采用"生成 → 求解验证 → 不合格则重试(上限 500 次)"的流程,保证玩家拿到的每一盘都必然可通关。

四、AIGC 使用过程

开发全程借助 Claude Code 辅助编程,下面记录 4 段印象最深的协作。

协作 1:方向向量与边界写法

我最初自己写的路径检测,循环是从箭头所在的格子开始扫的,结果箭头总会把自己误判成障碍,而且箭头指向棋盘外时下标会变成负数报错。AI 看完后给了两个改动建议:起点改为相邻的下一格;用 while 0 <= r < rows and 0 <= c < cols 让越界即结束循环。两个问题一次解决,我随后按它的提示补了 T03 边缘四方向测试用例。

协作 2:让"一次点击"变成"一个事件字典"

刚开始 ui.py 里散布着各种"如果可飞就……否则如果被挡就……"的判断,界面和规则搅在一起。AI 提议把 Game.click 的返回值设计成事件字典(flew / blocked / empty),UI 只根据返回的事件去决定播哪段动画。这样逻辑层变得可以单独断言,测试里直接检查返回字典即可,不用开窗口。

协作 3:为贪心求解器"要一个证明"

设计随机挑战模式时,我担心贪心会不会漏掉某些有解的布局。AI 没有直接说"能行",而是给了一段论证:消掉一个箭头只会让棋盘更宽松,可飞的箭头不会因此被挡住,所以有解的关卡贪心一定走得通,卡死即无解。这个性质也写进了 find_solution 的文档注释,成为整个关卡验证体系的依据。

协作 4:无窗口端到端模拟抓出两个问题

测试全部通过后,AI 又帮我做了一次不开窗口的完整点击流程模拟(菜单 → 开始 → 连续点击 → 通关),结果揪出两个肉眼难以发现的问题:

  • 测试用例的操作顺序有误:T06 中我先消掉了右侧的箭头,再点左侧箭头时它已经不再被阻挡,断言自然失败。按 AI 的提醒调整成"先点被挡箭头制造失误、再消除箭头",测试才真正反映设计意图;
  • 真实游戏里的一处崩溃:点击"开始游戏"后棋盘布局没有重新计算,第一次点击箭头时 board_rect 还是 None。这个 bug 正是模拟到"开始游戏 → 点击箭头"这一步时暴露的,修复后整个流程稳定通过。

说明:AI 生成的代码我没有直接粘贴进项目,而是先弄清每一段在做什么,再自己录入,并补上对应的测试用例。

五、测试结果

运行 python -m unittest test_game -v,共 11 项测试全部通过。作业要求的 T01~T06 对应如下:

编号 测试内容 预期结果 实际结果 是否通过
T01 点击前方无阻挡的箭头 箭头飞出棋盘并消失 该格被置空,事件为 flew,剩余数减 1 ✅ 通过
T02 点击前方有阻挡的箭头 箭头不消失,失误次数减 1 箭头保留,事件为 blocked,失误 3→2 ✅ 通过
T03 点击位于边缘且朝向棋盘外的箭头 正常飞出,不越界 上/下/左/右四方向边缘箭头均可飞,无异常 ✅ 通过
T04 消除本关全部箭头 显示通关并进入下一关 状态变为 level_clear,进入下一关后布局正常 ✅ 通过
T05 失误次数耗尽 显示失败并允许重新开始 连续 3 次被挡后状态变为 game_over,重开后失误恢复 3 ✅ 通过
T06 游戏进行中重新开始 箭头布局和失误次数恢复 布局快照与失误次数完全恢复 ✅ 通过

额外校验:4 个固定关卡分别以 4、5、5、6 步可解,不存在死局;随机生成的 20 个关卡全部可解。各界面(按钮、动画)已通过无窗口截图脚本逐界面实际运行验证。

六、PSP 表格

任务 预估耗时(小时) 实际耗时(小时) 差异(小时)
需求分析与游戏设计 1.0 1.5 +0.5
Python 与图形库学习 1.5 1.5 0
游戏界面实现 2.0 2.5 +0.5
路径与碰撞逻辑实现 1.5 1.5 0
关卡设计 0.5 1.0 +0.5
AIGC 辅助开发 1.0 1.0 0
测试与修改 1.0 1.5 +0.5
README 与博客撰写 1.5 2.0 +0.5
合计 10.0 13.0 +3.0

实际用时超出预估,超出的部分主要耗在"Python 3.14 下原版 pygame 装不上、换用 pygame-ce"以及界面细节调优上。

七、心得体会

  • 带着具体问题去提问,比"帮我写完整个游戏"更高效:这次最有用的提问都是小切口——边界条件怎么写、点击结果怎么表达、贪心为什么正确。问题拆得越细,AI 的回答越容易核对,也越能转化为自己的理解。
  • 先让逻辑可测,再谈界面:把规则层从 pygame 里剥出来之后,改 UI 的时候不再需要每改一版就手动点一遍全流程,命令行一条命令就能回归 T01~T06。端到端模拟还顺手抓出了一个真实崩溃,这是纯靠眼睛试玩发现不了的。
  • 对 AI 给出的结论也要追问依据:贪心求解器那一次,我本可以拿了代码就走,但"为什么贪心不会漏解"这个问题换来的是整个关卡验证体系的基石——现在连随机关卡都敢放心交给玩家。
  • 环境问题要自己兜底:Python 3.14 与 pygame 的兼容问题是 AI 帮忙定位的,但换库的决策、依赖的锁定(requirements.txt)都是自己完成的,作业环境别人替代不了。
  • 不足与改进:目前的动画只有飞出与晃动两种,没有音效;固定关卡仅 4 个,重复游玩价值有限。后续想加入"提示当前哪个箭头能飞"与"撤销上一步"两个功能。

posted on 2026-09-21 08:37  zzx0607  阅读(14)  评论(0)    收藏  举报