2026秋软件工程第二次个人作业
2026 秋软件工程个人作业(第二次)——星箭迷途
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 202601 软件工程 |
| 这个作业要求在哪里 | 2026 秋软件工程个人作业(第二次) |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏,并完成测试、AIGC 使用记录与项目总结 |
| 学号 | 102400412 |
| GitHub 仓库 | illnessemc/star-arrow |
一、项目展示
本次作业完成的项目名为 星箭迷途(Star Arrow),使用 Python 和 Pygame 开发。下面通过基础模式截图和两段 GIF 展示三种游戏模式及主要页面。
1. 基础模式

基础模式只使用单格箭头,保留上、下、左、右四方向的直线路径判断,适合熟悉基本玩法。
2. 进阶模式

进阶模式加入多格直线箭头和折线箭头,阻挡关系更复杂,需要同时观察箭头形状、最终朝向和释放顺序。
3. 调试模式与无尽模式

调试模式通过 python main.py --debug 启动,用于快速切换关卡、强制移除箭头和检查动画,调试过程不会计入正式得分。无尽模式则会在线生成新的可通关关卡,玩家的分数跨关累计,并保存本地最高分。
4. 主要页面
二、项目介绍
1. 游戏规则
棋盘中的箭头朝向上、下、左、右四个方向。玩家点击箭头后,程序从箭头头部开始,沿最终方向检查到棋盘边界。路径畅通时,箭头飞出并消失;路径被占用时,箭头留在原位,第一处阻挡位置给出碰撞反馈,并扣除一次失误机会。对于折线箭头,如果自己的尾部或中段绕到头部正前方,同样会形成阻挡,箭头不能穿过自身离场。
每关有三次失误机会。清空棋盘即可通关,失误次数耗尽或时间结束则失败,重新开始会恢复本关初始布局。与基础作业的单格箭头相比,本项目还支持多格直线箭头和多转角折线箭头,但出界判断仍然只看箭头头部沿最终方向的直线路径。
2. 界面和功能
开始页提供基础模式、进阶模式、无尽模式和退出游戏。进入游戏后,顶部显示当前模式、关卡、生命值、时间和分数,左上角可以打开设置或重新开始。箭头能够从任意一段被点击,成功时播放离场和拖尾动画,失败时用变红和震动提示真正的阻挡位置。通关与失败后,结果页会停止棋盘交互,并提供下一关、重试或返回首页的操作。
进阶模式保留原有的一个教学关和五个主题关卡,从“方向入门”逐步过渡到“星门终阵”,重点考察多格直线箭头、折线箭头和复杂依赖。除此之外,项目还加入了异形棋盘、无尽关卡、计分、星级和最高分保存。无尽关卡不是简单随机摆放箭头,而是在生成后经过可解性和布局质量检查,避免把明显死局交给玩家。
3. 运行方法
项目仓库为 illnessemc/star-arrow。安装依赖后即可运行:
python -m pip install -r requirements.txt
python main.py
Windows 用户也可以在 GitHub Releases 下载 StarArrow-v1.0-win64.exe。
三、实现思路
1. 项目结构
lab2/
├─ main.py # 程序入口与调试参数
├─ arrow_game/
│ ├─ app.py # 状态切换、事件分发和各模块协调
│ ├─ core/
│ │ ├─ arrow.py # 箭头实体与路径合法性校验
│ │ ├─ board.py # 棋盘占用、路径扫描和阻挡查询
│ │ ├─ direction.py # 四方向与坐标增量
│ │ ├─ game.py # 单局游戏规则
│ │ ├─ level.py / layout.py # 关卡数据和棋盘形状
│ │ └─ solver.py # 可通关性验证
│ ├─ data/
│ │ ├─ levels.py # 固定关卡目录
│ │ ├─ manual_levels.py # 手工主题关卡坐标
│ │ ├─ generator.py # 自动关卡生成与质量筛选
│ │ └─ endless.py # 无尽模式关卡工厂
│ └─ ui/
│ ├─ pages.py # 首页、选关、设置和结果页
│ ├─ board_view.py # 棋盘布局、箭头和动画绘制
│ ├─ widgets.py # 按钮、文字、生命值等通用控件
│ ├─ animation.py # 动画数据与播放队列
│ ├─ assets.py # 素材加载与缓存
│ └─ theme.py # 颜色和尺寸主题
└─ test/ # 规则、关卡、流程和渲染测试
重构后,core 只处理游戏规则,data 负责关卡来源,ui 负责具体绘制,app.py 不再直接承载所有页面和棋盘绘制代码,而是负责状态切换、事件分发和模块协调。PageRenderer 绘制普通页面,BoardView 负责棋盘坐标、箭头命中和动画表现,通用按钮与文字则放在 widgets.py。这样修改某个页面时不会碰到路径算法,调整箭头动画时也不需要改游戏状态。
2. 箭头与方向的表示
棋盘坐标统一使用 (row, col)。方向枚举保存每前进一步时行、列的变化量,因此四个方向可以共用同一套扫描算法。
class Direction(Enum):
UP = (-1, 0)
DOWN = (1, 0)
LEFT = (0, -1)
RIGHT = (0, 1)
@dataclass(frozen=True, slots=True)
class Arrow:
arrow_id: str
# 路径严格按照“箭尾 -> 箭头”保存。
# 单格箭头只有一个坐标,折线箭头可以包含多个坐标。
cells: tuple[Cell, ...]
direction: Direction
@property
def head(self) -> Cell:
return self.cells[-1]
长箭头在创建时就检查数据是否合法。下面的校验避免重复占格、斜线连接、跳格,以及箭头方向与最后一段路径不一致。
def __post_init__(self) -> None:
if len(set(self.cells)) != len(self.cells):
raise ValueError("箭头路径存在重复格子")
for previous, current in zip(self.cells, self.cells[1:]):
row_distance = abs(previous[0] - current[0])
col_distance = abs(previous[1] - current[1])
# 曼哈顿距离必须为 1,只允许上下左右相邻。
if row_distance + col_distance != 1:
raise ValueError("箭头路径不连续")
if len(self.cells) > 1:
previous, head = self.cells[-2:]
final_step = (head[0] - previous[0], head[1] - previous[1])
if final_step != self.direction.value:
raise ValueError("箭头朝向必须与路径最后一段一致")
3. 棋盘占用与路径检测
GameBoard 同时保存“箭头 ID 到箭头对象”和“格子到箭头 ID”两个索引。查询某个格子是否被占用时不需要遍历全部箭头,也能让折线箭头的任意一段对应回完整箭头。
self._arrows: dict[str, Arrow] = {}
self._cell_owners: dict[Cell, str] = {}
路径检测从箭头头部的下一格开始,按照方向增量扫描到棋盘边界。遇到的第一个被占用格就是实际阻挡位置;它既可能属于另一支箭,也可能属于当前折线箭头绕到头部前方的尾部或中段。走出边界仍未遇到占用才表示路径畅通。
def cells_to_edge(self, start: Cell, direction: Direction):
row = start[0] + direction.row_step
col = start[1] + direction.col_step
while self.is_inside((row, col)):
yield row, col
row += direction.row_step
col += direction.col_step
def first_blocker(self, arrow: Arrow) -> Arrow | None:
for cell in self.cells_to_edge(arrow.head, arrow.direction):
owner_id = self._cell_owners.get(cell)
# 不排除自身:折线箭头不能穿过自己的身体离场。
if owner_id is not None:
return self._arrows[owner_id]
return None
这里不能简单排除 owner_id == arrow.arrow_id。例如箭头先向左绕行,最后箭头朝右,而自己的中段正好位于头部右侧,它应该被自己的身体挡住。一次扫描只经过当前方向到边界的格子,不会漏掉远处阻挡,额外空间复杂度为 O(1)。
4. 自动生成关卡
无尽模式不是随机选择若干格子摆箭头,而是先构造能够覆盖棋盘的连续路径,再把路径切成多支箭头。切分时使用形状权重控制直线、单转角和多转角箭头的比例,并为不同形状设置长度限制。
@dataclass(frozen=True, slots=True)
class ArrowShapeMix:
# 三类箭头的相对生成权重。
straight: float = 0.3
single_turn: float = 0.4
multi_turn: float = 0.3
@dataclass(frozen=True, slots=True)
class ArrowShapeLimits:
max_straight_length: int = 6
max_single_turn_length: int = 14
max_axis_span: int = 9
def allows(self, length: int, turn_category: int, axis_span: int) -> bool:
if axis_span > self.max_axis_span:
return False
if turn_category == 0: # 直线箭头不能过长
return length <= self.max_straight_length
if turn_category == 1: # 更长的箭头必须包含多个转角
return length <= self.max_single_turn_length
return True
生成器的核心流程如下。代码中省略了部分参数传递,只保留“构图、切分、定向、验证和重试”五个关键步骤。
for attempt in range(1, spec.max_generation_attempts + 1):
# 种子和尝试次数共同决定候选,失败后仍能确定性重试。
randomizer = random.Random(spec.seed * 1000 + attempt)
layout = spec.create_layout()
# 1. 先得到覆盖全部可玩格的长路径。
coverage_paths = self._coverage_paths(layout, spec, randomizer)
# 2. 按长度上限、转角类型和形状权重切成多支箭头。
parts = tuple(
part
for path in coverage_paths
for part in self._split_path(
path,
spec.min_arrow_length,
spec.max_arrow_length,
layout,
spec.boundary_break_chance,
spec.shape_mix,
spec.shape_limits,
True,
randomizer,
)
)
# 3. 为每个片段选择正向或反向,构造至少一条离场顺序。
oriented_parts = self._orient_parts_for_exit(parts, layout, randomizer)
if oriented_parts is None:
continue
# 4. 将片段构造成正式关卡,再用真实规则求解。
builder = ManualLevelBuilder(
spec.name,
layout,
role=LevelRole.FORMAL,
intro="自动生成关卡",
mistake_limit=spec.mistake_limit,
)
for index, cells in enumerate(oriented_parts, start=1):
builder.add_path(f"P-{index:02d}", cells)
level = builder.build()
report = self.solver.solve(level)
# 5. 同时满足“可解”和“布局质量”才交给游戏。
if report.solved and self.quality_policy.accepts(level, report):
return GeneratedLevel(level, report, spec.seed, attempt)
raise ValueError("在最大尝试次数内未生成合格关卡")
仅仅可通关还不够。质量策略还会检查四种方向是否齐全、弯折与短箭头比例、重复形状比例和依赖关系。跨方向依赖太少、同方向连续限制太长,都会拒绝当前候选并重新生成。
dependencies = self.dependency_metrics(level)
if dependencies.cross_direction_ratio < 0.65:
return False # 不同方向之间的制约太少
if dependencies.same_direction_only_ratio > 0.10:
return False # 过多箭头只被同方向箭头限制
if dependencies.longest_same_direction_chain > 3:
return False # 拒绝过长的同方向依赖链
if dependencies.largest_component_ratio < 0.90:
return False # 依赖网络过于零散
5. 保证关卡可通关
当前规则具有单调性:移除箭头只会减少阻挡,不会产生新阻挡。因此验证关卡时不需要枚举所有顺序。求解器每轮找出当前所有能够离场的箭头,移除其中任意一支;如果棋盘未清空却找不到安全箭头,关卡就是死局。
def solve(self, level: Level) -> SolveReport:
board = level.create_board()
solution: list[str] = []
while not board.is_empty:
safe_arrows = [
arrow
for arrow in board.arrows
if board.first_blocker(arrow) is None
]
if not safe_arrows:
# 还有箭头却没有任何出口,说明形成了循环阻挡。
return SolveReport(
solved=False,
solution=tuple(solution),
choice_counts=(),
remaining_arrow_ids=tuple(
arrow.arrow_id for arrow in board.arrows
),
)
chosen = safe_arrows[0]
board.remove_arrow(chosen.arrow_id)
solution.append(chosen.arrow_id)
return SolveReport(
solved=True,
solution=tuple(solution),
choice_counts=(),
)
固定关卡在程序加载时全部执行这项检查;无尽关卡则在每个候选生成后立即验证。这样“随机关卡”只负责变化,“求解器”负责保证不会把死局交给玩家。
四、AIGC 使用过程
本次开发主要使用 Codex / GPT-5.6 Sol 辅助规则分析、数据结构设计、关卡生成和代码实现,并使用 GPT Image 2.0 生成美术素材。AI 能快速给出可以运行的初版,但规则细节、关卡质量和最终视觉风格仍然需要我自己确认、试玩和调整。
1. AIGC 使用概览
| 子任务 | AIGC 工具 | 我提出的要求 | AI 提供的内容 | 实际效果 | 人工修改或验证 |
|---|---|---|---|---|---|
| 游戏规则与箭头类 | Codex / GPT-5.6 Sol | 根据作业要求实现基本规则,并为后续长箭头留出扩展空间 | 生成了可以运行的规则初版,但箭头数据和游戏流程写在一起,没有独立的箭头类和文件 | 基础判断能够运行,但模块之间耦合较高 | 将箭头拆成独立类,按职责拆分文件,统一坐标顺序并补充长箭头校验 |
| 自动关卡生成器 | Codex / GPT-5.6 Sol | 设计能够完整覆盖棋盘、生成后可验证可解性的随机关卡 | 实现路径构造、随机切分、方向选择、固定种子和求解器验收 | 可以自动生成可通关关卡,但早期布局较死板,同类箭头会无意义堆叠 | 增加形状权重、长度与转角限制、重复形状比例和依赖质量指标,强调不同方向互相限制并压低同方向依赖链 |
| 星空主题美术素材 | GPT Image 2.0 | 使用统一的星空休闲游戏风格生成标题、按钮、背景、胜利和失败等素材 | 按固定提示词批量生成多组候选图片 | 很快形成了完整视觉方向,但部分图片过于油腻、炫技,和轻量小游戏不协调 | 人工筛选图片并反复调整提示词,减少过度高光和复杂装饰,使颜色与界面更清爽统一 |
2. 第一次协作:游戏规则与箭头类的编写
AI 完成的内容
我先把核心玩法交给 Codex / GPT-5.6 Sol,让它实现四个方向、路径判断和关卡状态。AI 很快给出了可以运行的初版,但当时没有单独设计 Arrow 类,也没有按职责拆分文件。箭头的坐标、方向、棋盘占用和游戏流程混在同一处,完成基础玩法没有问题,但继续增加长箭头和折线箭头时会越来越难维护。

实际效果与人工修改
我在初版上重新整理结构,把箭头抽成独立的 Arrow 类,并把方向、棋盘、关卡和游戏过程分别放入 direction.py、board.py、level.py 和 game.py。随着界面功能增加,我又把原本集中在 app.py 的绘制代码拆成 PageRenderer、BoardView 和通用控件模块,让 app.py 只负责流程协调。这样修改箭头形状时不需要同时改页面,调整首页或结果页也不会碰到路径规则,模块之间的耦合更低。
整理结构时还必须明确长箭头的数据约定。我规定 cells 始终按照“从箭尾到箭头”的顺序保存,最后一个坐标就是头部;相邻坐标只能上下或左右相接,不能跳格、斜连或重复占格;箭头方向必须与最后一段路径一致。完成这些补充后,单格箭头和折线箭头可以使用同一套接口,后续扩展也不需要推翻原有模型。
这次协作中,AI 主要完成了“能运行”的第一版,而我负责把它整理为更适合继续开发的结构。
3. 第二次协作:自动关卡生成器设计
AI 完成的内容
固定关卡可以手工设计,但无尽模式需要持续生成新棋盘。我要求 Codex / GPT-5.6 Sol 设计自动关卡生成器。AI 初版先生成一条覆盖棋盘的路径,再随机切成多支箭头,并通过固定种子复现关卡;生成完成后再调用求解器,无法清空的候选直接丢弃。

实际效果与人工修改
我手工试玩后发现,“有解”不等于“好玩”。初版切分规则比较死板,容易在同一张图里堆叠大量相同形状、相同方向的箭头,有时还会出现很长的直线箭头。玩家虽然能够通关,却只是在重复相同操作。
为解决这个问题,我进一步加入了 ArrowShapeMix,用权重控制直线、单转角和多转角箭头的比例;同时加入 ArrowShapeLimits,限制直线箭头和单转角箭头的最大长度。过长箭头不能继续保持简单直线,而应包含更多弯折或回环,避免一条箭头无意义地贯穿大半个棋盘。
此外,我增加了关卡质量指标,统计四种方向的数量、弯折比例、重复形状比例和箭头之间的依赖。生成结果应尽量让一支箭头受到不同方向箭头的限制;跨方向依赖太少会被拒绝,同方向单一依赖和连续依赖链也受到上限控制。

修改后,生成器不再只判断“有没有解”,还会判断布局是否包含足够的方向变化、形状变化和交叉制约。最后通过求解器再次验收,保证质量筛选不会破坏可通关性。
4. 第三次协作:星空主题美术素材生成
AI 完成的内容
为了让首页、游戏界面和结算界面保持统一,我使用 GPT Image 2.0 生成星空主题素材。提示词固定蓝紫色星空、轻量休闲游戏、圆润按钮、清晰层级和明亮但不过曝的光效。AI 据此生成了标题 Logo、背景、按钮、星星、生命值、设置、胜利和失败等多组候选素材。

实际效果与人工修改
初次生成的部分图片存在“油腻炫技”的问题:高光太多、立体描边太厚、装饰元素过密,单张图看起来醒目,放进完整界面后却显得拥挤,不符合休闲小游戏应有的清爽感。
因此我没有直接采用第一批结果,而是先筛选构图和配色较合适的图片,再调整提示词,减少金属质感、镜面反光、复杂纹理和无关装饰,强调扁平清晰、留白充足和按钮文字易读。对于仍然不协调的素材,我继续生成变体并进行取舍,最终让标题、按钮、背景和结果页在色彩与光效上保持一致。
这次协作说明,图像生成工具适合提供大量候选和统一风格方向,但最终素材是否适合实际界面,仍然要结合按钮尺寸、文字可读性和整体布局人工筛选。
五、测试过程与结果
1. 测试方法
项目使用 Python 标准库中的 unittest 编写自动化测试。执行命令如下:
python -m unittest discover -s test -v
本次博客整理时的实际运行环境为:
Python 3.14.5
pygame-ce 2.5.8
本次实际运行结果:
Ran 23 tests in 0.592s
OK
2. 测试结果
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 返回 REMOVED,箭头从棋盘数据中移除 |
通过 |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 被点击箭头和阻挡箭头都保留;第一支阻挡箭头触发碰撞动画;失误次数减 1 | 通过 |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 边缘箭头正常移除,关卡状态变为已清空 | 通过 |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 状态切换为通关界面,继续后载入下一关 | 通过 |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 状态切换为失败界面;重开后恢复失误次数和箭头 | 通过 |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 重开后箭头 ID、数量和失误次数恢复为初始值 | 通过 |
| T07 | 折线箭头前方遇到自己的身体 | 自身尾部或中段同样构成阻挡,箭头不能穿过自己 | 返回当前箭头作为阻挡者,播放碰撞反馈并保留箭头 | 通过 |
| T08 | 检查 6 个固定关卡 | 关卡数据固定、覆盖要求正确并且全部可解 | 求解器均能清空关卡,正式关卡没有遗漏可玩格 | 通过 |
| T09 | 检查主题关卡的方向与形状 | 每关包含四种方向、直线箭头、折线箭头和跨方向依赖 | 所有主题关均满足对应质量条件 | 通过 |
| T10 | 倒计时归零后重新开始 | 时间耗尽进入失败,重开后恢复本关时限 | 失败状态和重置后的剩余时间均正确 | 通过 |
| T11 | 比较不同完成速度的计分 | 完成越快,得分与星级越高 | 快、中、慢三种情况分别得到 3、2、1 星 | 通过 |
| T12 | 无尽模式跨关累计 | 分数跨关累加并更新最高分 | 当前累计分与历史最高分均按预期更新 | 通过 |
| T13 | 最高分文件读写 | 正常保存分数;文件损坏时安全回退为 0 | JSON 往返读写正常,损坏文件未导致程序崩溃 | 通过 |
| T14 | 调试模式计分隔离 | 调试时冻结倒计时,不写入正式得分 | 调试过程耗时和累计分均保持不变 | 通过 |
| T15 | 重构后的所有页面渲染 | 首页、选关、游戏、设置、通关和失败状态都能完成绘制 | 使用虚拟显示器逐一渲染,各状态均未抛出异常 | 通过 |
六、PSP 表格
本次开发大量使用 AIGC 完成初版代码与素材生成,实际耗时低于原先预估。差异按照“实际耗时 − 预估耗时”计算。
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 1.0 | 0.7 | -0.3 |
| Python 与图形库学习 | 0.8 | 0.5 | -0.3 |
| 游戏界面实现 | 1.4 | 0.9 | -0.5 |
| 路径与碰撞逻辑实现 | 1.4 | 0.9 | -0.5 |
| 关卡设计 | 1.0 | 0.6 | -0.4 |
| AIGC 辅助开发 | 0.8 | 0.5 | -0.3 |
| 测试与修改 | 1.0 | 0.6 | -0.4 |
| README 与博客撰写 | 0.6 | 0.3 | -0.3 |
| 合计 | 8.0 | 5.0 | -3.0 |
七、心得体会
在这次开发中,我更像是项目的设计者和验收者,而不是把每一行代码都亲手写出来。我先确定整体架构、游戏规则和完成标准,再把箭头模型、路径检测、关卡生成、界面与测试拆成较小的问题交给 Codex 执行。AI 负责快速给出实现和修改版本,我负责运行程序、试玩关卡、判断结果是否符合预期,再把发现的问题继续反馈回去。这个过程让我体会到,使用 Coding Agent 并不是只说一句“帮我做一个游戏”,顶层设计仍然要足够具体:箭头坐标按什么顺序保存、路径检测从哪里开始、哪些状态属于失败、模块之间用什么接口连接,这些约定如果没有先说清楚,AI 即使写出了能运行的代码,后面也很容易因为理解不同而返工。
我花精力最多的部分是自动关卡生成。AI 最初给出的生成器确实能生成可通关的棋盘,自动测试也可以通过,但实际玩起来很机械:相同形状的箭头重复出现,直线箭头过长,同方向箭头一层层堆叠,很多时候只需要连续点击同一类箭头。这说明“算法有解”和“关卡好玩”是两件事。后来我增加了直线、单转角和多转角箭头的生成权重,限制不同类型箭头的长度,又统计跨方向依赖、重复形状和同方向依赖链,让生成器除了检查能否通关,还要检查关卡结构是否足够丰富。这个部分也让我看到 AI 的局限:它很擅长把明确的指标写成代码,却不会自动知道玩家面对一张图时会不会觉得单调,最终的质量标准仍然来自人工试玩和判断。
其他部分也有类似情况。规则初版没有独立的箭头类,长箭头从头到尾还是从尾到头并不明确,我重新拆分文件并补全了连续性和方向校验;界面初版更关注功能是否齐全,信息位置、留白和星空素材放在一起是否协调,还需要实际看画面后调整;动画虽然很快就能“动起来”,但速度、轨迹、拖尾和碰撞反馈是否自然,只看代码很难判断,必须在窗口里反复观察。通过这次作业,我认为 AIGC 最有价值的地方是提高实现和试错速度,让我能把更多时间放在拆分问题、制定约束和判断结果上;但测试、体验和最终是否接受方案仍然是人的工作。以后继续使用 Agent 开发时,我会更早写清验收条件,也会把试玩中发现的问题转化为可以重复执行的测试,避免修好一次后又在后续修改中出现。

浙公网安备 33010602011771号