软件工程第二次个人作业

项目 内容
这个作业属于哪个课程 H202601 软件工程与软件工程实践
这个作业要求在哪里 软件工程课程第二次个人作业
这个作业的目标 使用 Python 和 AIGC 完成“一箭又一箭”小游戏
学号 102401633
GitHub 仓库 https://github.com/Clu3y/Arr0w-voyage

一、项目概览

项目包含开始界面、三个普通关卡、自定义配置页、游戏主界面、通关与失败结算页,以及提示、增加失误次数、移出三类道具。开发过程中还加入了箭头飞出动画、碰撞反馈、红心失误显示、背景音乐与音效。

功能模块 实现内容
核心玩法 点击箭头,判断其前方是否有阻挡;无阻挡时飞出,有阻挡时消耗失误次数
普通关卡 三个可解关卡,失误次数和道具状态跨关保留
道具系统 提示、增加失误次数、移出,普通模式下各可使用一次
自定义模式 支持 1~10 的 N×N 棋盘、三类道具各 0~9 次和空格比例配置
随机棋盘 使用反向放置法生成棋盘,保证生成结果一定可解
视觉反馈 飞出动画、碰撞晃动、变色、提示高亮和 Toast 消息
工程实践 模块拆分、pytest 自动化测试、SDL 冒烟测试和完整文档

关卡设计

关卡 棋盘 箭头数量 设计目的
第一关 5×5 8 熟悉点击、方向和阻挡判断
第二关 5×5 20 增加箭头密度和多层依赖
第三关 6×6 36 使用满棋盘检验完整规划能力

二、项目展示

1

开始界面

开始界面

游戏界面

游戏界面

本关通过

本关通过

全部通关

全部通关

本关失败

本关失败

箭头飞出动画

箭头飞出动画

三、玩法与界面设计

1. 游戏规则

棋盘由若干方格组成,每个箭头格包含上、下、左、右四个方向之一。点击箭头后,程序沿着箭头方向检查它到棋盘边缘之间的格子:

  • 如果路径上没有其他箭头,箭头飞出棋盘并消失;
  • 如果路径上存在其他箭头,箭头保留,并消耗一次失误机会;
  • 清空当前关卡全部箭头后进入下一关;
  • 普通模式共有三颗红心,失误次数在三个关卡之间保留;
  • 失误次数耗尽后显示失败结果,可重新开始。

2. 界面与视觉风格

为了避免明显的模板化游戏风格,界面没有采用高饱和渐变、厚重阴影和悬浮卡片,而是使用暖色背景、细线、清晰的箭头素材和少量强调色。按钮、方格、道具框、提示框和鼠标素材保持统一风格,并通过九宫格缩放避免圆角在拉伸后变形。

界面中的箭头飞出、碰撞晃动、红心变化、道具禁用和确认弹窗都提供了明确反馈,让玩家能够理解“为什么这次操作成功或失败”。

3. 图片、音效与背景音乐来源

项目中的图片、音效和背景音乐均使用 CC0 素材,没有使用原商业游戏的美术、代码或关卡资源。

素材类型 来源 项目中的用途 许可证
界面与棋盘图片 Kenney 箭头、道具、方格、鼠标、按钮、提示框和部分界面背景 CC0 / Public Domain
操作音效 Kenney 按钮悬停、箭头飞出、碰撞失误、通关和失败 CC0 / Public Domain
背景音乐 OpenGameArt 开始界面和游戏中循环播放的 bgm.ogg CC0 / Public Domain
音效文件 使用位置
click.ogg 鼠标进入可交互按钮时播放
fly.ogg 箭头成功飞出时播放
miss.ogg 箭头被阻挡并消耗失误次数时播放
win.ogg 本关通过或全部通关时播放
fail.ogg 失误次数耗尽时播放

四、核心实现思路

1. 使用二维数组表示棋盘

棋盘使用二维列表表示:

# 每个格子要么为空(None),要么保存 "UP"、"DOWN"、"LEFT"、"RIGHT"
# 二维列表使路径判断可以脱离 Pygame 单独测试
Board = list[list[str | None]]

其中 None 表示空位置,"UP""DOWN""LEFT""RIGHT" 表示四种箭头方向。路径判断等纯逻辑与 Pygame 绘制分离,因此测试时不需要打开游戏窗口,既降低了调试成本,也方便覆盖边界情况。

2. 路径阻挡判断

每个方向先映射为行列偏移量,例如 UP = (-1, 0)RIGHT = (0, 1)。然后从箭头所在格开始逐格扫描,遇到任意非空单元格即判定为受阻:

def can_fly_out(board: Board, row: int, col: int) -> bool:
    # 1. 读取当前箭头方向,并转换成行、列的移动步长
    direction = board[row][col]
    delta_row, delta_col = DIRECTIONS[direction]

    # 2. 从箭头前方的第一格开始扫描
    row += delta_row
    col += delta_col

    while 0 <= row < len(board) and 0 <= col < len(board[0]):
        # 3. 前方只要出现任意箭头,就说明路径被阻挡
        if board[row][col] is not None:
            return False

        # 4. 没有阻挡则继续沿当前方向前进
        row += delta_row
        col += delta_col

    # 5. 未遇到阻挡并走出棋盘,说明该箭头可以飞出
    return True

3. 状态管理与动画

游戏使用枚举维护 STARTCUSTOM_CONFIGPLAYINGLEVEL_COMPLETEGAME_COMPLETECUSTOM_COMPLETELOSE 等状态。点击事件、界面绘制与结算逻辑根据当前状态分派,避免不同页面的按钮互相误触。

动画状态没有直接混在绘制代码中,而是集中记录飞出箭头、碰撞格、提示格和 Toast 计时。最后一支箭头飞出时,程序会等待动画结束再显示结果页,避免画面突然切换。

4. 生成保证可解的随机棋盘

内置关卡和自定义棋盘最终都使用同一种二维结构。区别在于,内置关卡使用可读的字符布局描述棋盘,自定义模式则先选择箭头位置,再动态计算方向并把箭头真正写入 board[row][col]

4.1 内置关卡:把字符布局转换成箭头

为了便于阅读和维护,关卡文件先使用 表示方向。读取关卡时,再把字符逐个转换成程序内部使用的方向名称:

DIRECTION_SYMBOLS = {
    "↑": "UP",
    "↓": "DOWN",
    "←": "LEFT",
    "→": "RIGHT",
}


def _parse_level(rows: tuple[str, ...]) -> Board:
    # 每一行字符串按字符拆分;"." 不在映射表中,因此会转换为 None
    return [
        [DIRECTION_SYMBOLS.get(symbol) for symbol in row]
        for row in rows
    ]

例如,字符布局 "....↑"".↑..." 会被转换成:

[
    [None, None, None, None, "UP"],
    [None, "UP", None, None, None],
]

4.2 自定义棋盘:随机选择位置并放置箭头

自定义棋盘的核心目标有两个:第一,箭头位置和方向要保持随机;第二,生成结果必须存在通关顺序。项目采用“反向放置”思路,即每次放置箭头时都确保它当前可以飞向棋盘边缘,这样按放置顺序的逆序点击就一定可以清空棋盘。

# 1. 从所有格子中选出需要放置箭头的位置
selected = generator.sample(positions, arrow_count)

# 2. 从中心向外放置,保证箭头写入时到某一边缘的路径仍然为空
selected.sort(
    key=lambda cell: min(
        cell[0],
        cell[1],
        board_size - 1 - cell[0],
        board_size - 1 - cell[1],
    ),
    reverse=True,
)

# 3. 创建空棋盘,再逐个把箭头写入对应位置
board = [[None for _ in range(board_size)] for _ in range(board_size)]
previous_direction = None

for row, col in selected:
    clear_directions = []

    # 临时写入候选方向,复用 can_fly_out 判断该方向当前是否畅通
    for direction in DIRECTIONS:
        board[row][col] = direction
        if can_fly_out(board, row, col):
            clear_directions.append(direction)
        board[row][col] = None

    if not clear_directions:
        continue

    # 只有一个畅通方向时必须选择它,否则该位置可能无法完成放置
    if len(clear_directions) == 1:
        chosen_direction = clear_directions[0]
    else:
        # 重复方向权重为 1,其他畅通方向权重为 4,减少连续同方向
        weights = [
            1.0 if direction == previous_direction else 4.0
            for direction in clear_directions
        ]
        chosen_direction = generator.choices(
            clear_directions,
            weights=weights,
            k=1,
        )[0]

    # 将最终选择的方向真正写入二维棋盘
    board[row][col] = chosen_direction
    previous_direction = chosen_direction

return board

这里的 selected 决定箭头出现在哪些格子,循环中的 board[row][col] = direction 用于临时测试方向,而 board[row][col] = chosen_direction 负责把最终箭头写入棋盘。由于每次放置时都能飞出,最终棋盘按照放置顺序的逆序操作一定可以清空。

方向选择没有简单禁用上一个方向,而是采用 1:4 的权重降低连续重复概率。如果只有两个畅通方向,重复概率约为 1/5;有三个畅通方向时约为 1/9。如果只有一个畅通方向,仍然直接选择它,以保证棋盘可解性不受影响。

五、AIGC 使用过程

子任务 借助何种 AIGC 技术 我提出的要求 AI 实现或提供了什么 实际效果如何 是否进行人工修改
需求分析与项目骨架 Codex 分析第二次作业要求,拆解评分点,给出开发路线,并把路径逻辑设计成可以独立测试的结构 拆解游戏功能;生成项目骨架、路径检测函数和初始测试 项目目录和模块边界清晰,路径逻辑首批 8 项测试通过 是。自行创建虚拟环境并安装 Pygame、pytest;冒烟测试发现字体枚举异常后,改用 Pygame 自带字体回退
开始界面与视觉重构 Codex 实现开始、游戏和结算界面,让鼠标交互有清晰反馈 生成 HUD、棋盘、箭头、按钮、状态切换和动画代码,初始版本偏常见渐变卡片风格 功能可以运行,但视觉模板感较强,部分控件和文案位置不够协调 是。删除蓝色渐变和厚重阴影;重新调整标题、按钮、道具和提示框位置
道具与模块化拆分 Codex 让提示、增加失误次数和移出道具可靠工作,并拆出独立模块方便后续扩展 实现三种道具、动画状态、音频接口以及 tools.pyanimations.pyaudio.py 等模块 道具逻辑和界面模块可以独立导入 是。逐项回归道具消耗、跨关保留、重新开始和失败流程;将移出操作改为先选道具、再点目标箭头,避免依赖悬停状态
自定义模式与可解随机棋盘 Codex 增加自定义配置页,支持棋盘大小、道具次数和难度设置,并要求随机棋盘始终可解 新增 custom_mode.py、选项滑块和反向放置式棋盘生成器,并接入成功、失败和重开流程 自定义模式主体可用;批量验证时发现空格曾被错误传给路径判断函数,滑块右侧也出现数值重叠 是。临时放入候选方向完成判断后再恢复空格;把圆形旋钮改为五行长条并调整布局;验证不同尺寸和难度均能清空
连续同方向优化 Codex 观察到随机棋盘连续同方向偏多,要求分析如何在不破坏可解性的前提下降低重复概率 提供加权随机的初步思路和可解性风险提示 初步思路可行,但具体权重仍需要结合实际棋盘效果调整 是,将重复方向权重设为 1、其他畅通方向设为 4;保留唯一畅通方向回退

六、测试结果

编号 测试内容 预期结果 实际结果 是否通过
T01 点击无阻挡箭头 箭头飞出并消失 箭头被移除,剩余数量减少
T02 点击有阻挡箭头 箭头保留,失误增加 箭头保留,失误次数正确减少
T03 测试四个方向路径 阻挡判断全部正确 四个方向及边缘测试通过
T04 清空本关箭头 显示通关并进入下一关 状态和关卡切换正确
T05 失误次数耗尽 显示失败并允许重开 失败页与恢复流程正常
U01 道具跨关使用 已用道具不跨关恢复 下一关保持已使用状态
U02 随机棋盘可解性 不同尺寸和难度均可清空 批量验证通过
U03 长条滑块交互 最小值与最大值映射正确 左右边界映射正确
U04 方向重复抑制 上一方向权重为 1,其他方向为 4 受控随机测试通过

主要问题与修复

问题 原因 修复方式 回归结果
系统字体枚举异常 运行环境中的字体枚举行为不一致 改用 Pygame 自带字体并增加回退 冒烟测试通过
清空第一关后无法继续 只更新了本关完成标志,没有进入下一关 增加结果状态和关卡切换逻辑 通关流程通过
失败后没有结果页 仅提示失误,没有切换界面状态 增加失败状态、重试和返回首页 失败流程通过
随机棋盘无法生成 空格曾被传入路径判断函数 临时放置候选方向,判断后再恢复空格 批量可解性通过
滑块右侧数值重叠 滑块和值域位置过度靠右 控件整体左移并检查布局边界 交互测试通过

七、PSP 时间记录

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

总耗时比预估多 1.5 小时,超支约 12.5%。主要偏差来自 Python 与 Pygame 环境学习、界面细节调整和路径逻辑的边界验证。核心逻辑的预估相对准确,但环境配置、实际视觉体验和回归测试仍需要额外时间。

八、心得体会

通过本次作业,我深刻感受到,写出可用功能和完成完整软件工程任务,是两件完全不同的事。一开始我认为项目重点仅仅是实现箭头路径判断与游戏界面。可真正动手开发才发现,模糊的需求必须重新梳理。就像随机棋盘生成功能,看似简单,不加限制就会产生无解棋盘。于是我把它拆成随机生成、尺寸校验、空格比例控制、保证可解等多个可检查条件,并编写对应的测试。我也由此明白:只有将抽象想法变成可验证的标准,开发工作才有明确方向。

在使用 Codex 辅助开发的过程中,我最大的感受是它可以显著加快探索速度,但不能代替人的判断。它能够快速生成界面结构、路径逻辑、模块代码和测试,也确实帮我节省了大量重复劳动。不过,AI 不一定了解真实视觉质量和隐藏边界。生成的滑块在功能上没有问题,放到实际页面中却显得拥挤;随机棋盘生成器最初也存在空格被错误传入路径判断函数的问题;方向重复优化如果直接禁用上一个方向,还会破坏原本的可解性。这些问题让我明白,人的任务不是简单接受生成的代码,而是定义标准、运行程序、发现问题,并决定哪些地方应该修改。

总体来说,这次作业让我经历了从需求分析、界面设计、逻辑实现、AIGC 协作到测试和文档整理的完整过程。相比最后完成的小游戏,我更看重的是学会了如何把模糊想法拆成可执行、可检查、可修改的工程任务,也逐渐理解了 AI 在开发中应该扮演的角色:它是一个高效的协作者,而不是最终决定的替代者。


Arr0w Voyage · 一箭又一箭

Made with Python & Pygame · 2026

posted on 2026-09-16 21:13  EULC  阅读(11)  评论(0)    收藏  举报

导航