软件工程第二次作业
| 项目内容 | 内容 |
|---|---|
| 这个作业属于哪个课程 | H202601 软件工程与软件工程实践 |
| 这个作业要求在哪里 | 2026 秋软件工程个人作业(第二次) |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏() |
| 学号 | 102401509 |
| GitHub 仓库 | zhewanyiershinicheng/arrow_game |
第一部分:使用AIGC进行游戏开发
| 借助的AIGC | AI 完成的任务 | 效果如何 | 人工修改 |
|---|---|---|---|
| Mimo Desktop | 整体设计 | 实现了基本的功能 | 初版不做修改 |
| Mimo Desktop | 二次设计 | 修改bug | 存在部分逻辑问题,手动修改 |
| Mimo Desktop | 界面设计 | 更加美观 | 去除界面中不必要的元素,优化文本 |
首次对话
-
将任务的所有要求都发给AI,让其自行生成,不做其他任何操作
![image]()
虽然AI此处说有安装python,但是实际我的电脑上并未安装,其使用的是其他软件文件中自带的python,实际运行测试的时候是用后来全局安装的,依赖需重新装(这里使用的是Pygame)。 -
实际运行效果
![image]()
经过游玩测试,核心玩法已经实现,但是有下述问题:
- 失误次数总数会随着关卡的增加而减少。
- 失误次数剩余为1时,若失误就会直接结束游戏,并且没有触发错误时的箭头动效。
- 当前的正确答案,即当前可以移动的箭头边框带有绿色标识,相当于直接告诉答案。
- 存在不明意义关卡试玩验证(即答案),说明关卡并不是随机生成的。
![image]()
- 排版存在错位,文字图像存在相互覆盖的问题
二次修改
- 将之前的问题反馈给AI修复
![image]()
- 有部分问题依旧存在,故再次修改
![image]()
- 成功修复后
![image]()
同关不同图:
![image]()
![image]()
最后计数的问题手动修改,就不浪费token了。
界面加强
这里使用harness中自带的前端设计skill,看看效果:

有部分提示词相关的文本也直接写进去了,这里最后由人工统一修改。
到目前为止的版本: 点此跳转
扩展功能
作业中还有如下的要求:
- 更多关卡或关卡选择;
- 得分、计时或星级评价;
- 提示功能;
- 撤销上一步;
- 保存游戏进度;
- 随机生成可通关的关卡;
- AI 自动求解当前关卡;
- 更流畅的动画和音效;
- 将程序打包为可执行文件。
这里我们就不全部都实现了,这种游戏不用提示也能玩明白(应该吧)
目前关卡随机,动画效果基本实现了,撤销上一步的意义也不大,所以就着重在1,2,9
但是为了方便起见,我们先将这个基础版本的打包,使用pyinstaller:
pyinstaller -w -F arrow_game.py
//游戏文件只有arrow_game.py,其他文件都只是AI coding时用于测试的脚本和图片,你可以在v1.0中下载到它们。
默认仅支持windows
接下来我们增加音效和得分、计时和星级评价功能
这个过程有和agent对话博弈多轮,这里不做展出。
界面展示和测试结果详见文章结尾链接视频。
最终版本: 点此跳转
第二部分:核心代码
这里我们不关注动画是如何实现的,而在意游戏本身的内容。
Q1: 怎么实现点击
1.1 事件入口:on_mouse_down
主循环收到 MOUSEBUTTONDOWN 后调用:
# arrow_game.py (主循环)
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
on_mouse_down(game, event.pos)
def on_mouse_down(game: Game, pos: Tuple[int, int]) -> None:
# ① 先判按钮(开始 / 重开 / 菜单等)
for btn in game.buttons:
if btn.rect.collidepoint(pos):
btn.press = 1.0
play_sound("click")
request_action(game, btn.action)
return
# ② 仅在游玩态、非结算中、非转场中,才处理棋盘点击
if game.state == State.PLAYING and not game.pending_fail and not game.pending_win and game.trans_dir == 0:
for a in game.arrows:
if not a.alive:
continue
if cell_rect(game, a.row, a.col).collidepoint(pos):
click_arrow(game, a.row, a.col)
break
要点:
- 先按钮后棋盘,避免点按钮误触箭头
- 只处理
alive的箭头 pending_win/pending_fail/ 转场中忽略点击,防止连点打乱结算
1.2 坐标映射:cell_rect
鼠标是像素坐标 (x, y),箭头逻辑坐标是 (row, col):
def cell_rect(game: Game, row: int, col: int) -> pygame.Rect:
ox, oy = game.board_origin
return pygame.Rect(ox + col * CELL, oy + row * CELL, CELL, CELL)
- 棋盘原点
(ox, oy)由layout_board计算 col * CELL对应 x,row * CELL对应 yrect.collidepoint(pos)判断是否点在该格内
1.3 路径判定:is_blocked
点击后判断「前方到边界之间是否还有其他箭头」:
def is_blocked(arrows: List[Arrow], target: Arrow, rows: int, cols: int) -> bool:
dr, dc = DIR_DELTA[target.direction]
r, c = target.row + dr, target.col + dc
alive = {(a.row, a.col) for a in arrows if a.alive and a is not target}
while 0 <= r < rows and 0 <= c < cols:
if (r, c) in alive:
return True
r += dr
c += dc
return False
方向增量:
DIR_DELTA = {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)}
# 上 / 下 / 左 / 右
从目标前一格沿方向扫描到边界;命中其他存活箭头 → 阻挡。
1.4 逻辑结算:click_arrow
def click_arrow(game: Game, row: int, col: int) -> None:
if game.state != State.PLAYING or game.pending_fail or game.pending_win:
return
target = None
for a in game.arrows:
if a.alive and a.row == row and a.col == col:
target = a
break
if target is None or target.anim == "bump":
return # 碰撞动画期间忽略重复点击
if not is_blocked(game.arrows, target, game.rows, game.cols):
# —— 无阻挡:飞出并消除 ——
target.alive = False
target.anim = "flying"
target.anim_t = 0.0
# …飘字 / 粒子 / 音效…
if not game.alive_arrows():
game.pending_win = True # 动画播完后再切通关界面
return
# —— 有阻挡:碰撞反馈 + 失误 ——
target.anim = "bump"
target.flash = 1.0
game.shake = SHAKE_AMPLITUDE
# …
if game.misses_left <= 0:
# 次数已是 0 时再错 → 失败
game.pending_fail = True
game.run_total_misses += 1
game.run_had_fail = True
else:
# 还有次数:扣 1,继续玩(减到 0 不立刻失败)
game.misses_left -= 1
game.run_total_misses += 1
流程示意:
鼠标点击
│
├─ 按钮? ──是──► request_action(菜单/重开等)
│
└─ 棋盘格子? ──► click_arrow
│
├─ is_blocked == False
│ alive=False + flying
│ 清空? → pending_win
│
└─ is_blocked == True
bump 动画
misses>0: misses -= 1
misses==0: pending_fail
1.5 为什么用 pending_win / pending_fail
不在点击瞬间切 WIN/FAIL,而是:
- 先改状态、播
flying/bump update_animations里等动画结束- 再
rebuild_buttons并切结果界面
这样碰撞动效不会被结果弹层立刻盖住。
Q2:怎么实现随机生成地图
目标:每次进关随机布局,难度随箭头数量上升,且保证有解。
思路:逆序构造 放箭头 + 贪心求解器 校验。
2.1 难度:箭头数量
def arrow_count_for_level(index: int, hard: bool = False) -> int:
if hard:
return GRID_N * GRID_N # 高难:5×5 = 25,铺满
return 5 + index * 2 # 剧情:5, 7, 9, 11
2.2 求解器:solve_level
反复消除「当前前方无阻挡」的箭头,直到清空或卡住:
def solve_level(grid: List[str]) -> Optional[List[Tuple[int, int, int]]]:
rows, cols, arrows = parse_level(grid)
objs = [_A(a.row, a.col, a.direction) for a in arrows]
order = []
progress = True
while any(o.alive for o in objs):
if not progress:
return None # 卡住 → 无解
progress = False
for o in objs:
if not o.alive:
continue
if not is_blocked(objs, o, rows, cols):
o.alive = False
order.append((o.row, o.col, o.direction))
progress = True
break
return order # 一条可行消除顺序
说明:消除只会减少阻挡,不会新增,因此「能消就消」在本规则下是完备的。
2.3 逆序构造:_try_reverse_construct
按消除顺序的逆放箭头:
| 构造顺序 | 对应正向消除 | 约束 |
|---|---|---|
| 先放 | 最后消 | 空盘上可任意放 |
| 后放 | 先消 | 不得被已放(更晚消)的箭头挡住 |
def _try_reverse_construct(rows, cols, n):
occupied = set()
placed = [] # 放置顺序 = 消除顺序的逆
all_cells = [(r, c) for r in range(rows) for c in range(cols)]
for _ in range(n):
free_cells = [(r, c) for r, c in all_cells if (r, c) not in occupied]
random.shuffle(free_cells)
free_cells = free_cells[: min(20, len(free_cells))]
dirs = [0, 1, 2, 3]
random.shuffle(dirs)
best = []
for r, c in free_cells:
for d in dirs:
trial = Arrow(row=r, col=c, direction=d)
# 关键:不能被「已放置、更晚消除」的箭头挡住
if is_blocked(placed, trial, rows, cols):
continue
score = 0
for p in placed:
if _path_contains(trial, p, rows, cols):
score += 2 # 本箭头会挡住已放的 → 正向更难
if _path_contains(p, trial, rows, cols):
score += 1
best.append((score, r, c, d))
if not best:
return None # 当前候选都不合法
best.sort(key=lambda x: x[0], reverse=True)
top = best[0][0]
pool = [item for item in best if item[0] >= max(0, top - 1)][:8]
_, r, c, d = random.choice(pool)
placed.append(Arrow(row=r, col=c, direction=d))
occupied.add((r, c))
return grid_from_arrows(placed, rows, cols)
_path_contains 判断「blocker 是否在 target 的前进路径上」,用来给能制造阻挡的落点加分,让开局更有解谜感,而不是全图都能点。
2.4 入口:generate_solvable_grid
def generate_solvable_grid(level_index, rows=GRID_N, cols=GRID_N, hard=False):
target = min(arrow_count_for_level(level_index, hard=hard), rows * cols)
if hard:
tries_list = [(target, 30), (target - 1, 12), (20, 10), (16, 8)]
else:
tries_list = [(target, 24), (target - 1, 12), (max(4, target - 2), 8)]
for n, tries in tries_list:
if n < 4:
continue
for _ in range(tries):
grid = _try_reverse_construct(rows, cols, n)
if not grid:
continue
order = solve_level(grid)
total = sum(ch in CHAR_DIR for row in grid for ch in row)
if order and len(order) == total:
return grid, total # 构造成功且求解器认可
fallback = ["R.R..", "..U..", ".....", "L...D", "....."]
return fallback, sum(...)
流程:
确定目标箭头数 n
│
▼
逆序构造随机图(可重试多次)
│
▼
solve_level 校验
│
├─ 步数 == n → 返回该图(保证可通关)
│
└─ 无解/失败 → 降低 n 或换尝试次数
│
▼
仍失败 → 兜底固定可解图
2.5 关卡载入:load_level
def load_level(game: Game, index: int) -> None:
hard = bool(getattr(game, "hard_mode", False))
grid, arrow_n = generate_solvable_grid(index, hard=hard)
rows, cols, arrows = parse_level(grid)
game.arrows = arrows
game.level_grid = grid
game.arrow_count = arrow_n
# 重置失误、计时相关字段、入场动画等
...
每次进入关卡、点「重新开始」都会重新调用 generate_solvable_grid,因此每局地图都可能不同。
2.6 为何这样能「随机且有解」
| 手段 | 作用 |
|---|---|
| 逆序放置 | 正向按放置逆序点击时,每一步前方都没有「还没消掉的箭头」 |
is_blocked(placed, trial) |
保证新放的箭头不被更晚消除的箭头挡住 |
| 评分偏爱阻挡 | 让初始局面更有连锁,而不是全部一次点掉 |
solve_level 校验 |
构造异常时丢弃重试,避免互锁死局 |
箭头数 n |
控制难度;高难 n=25 铺满棋盘 |
第三部分:其它要求
测试表格
| 编号 | 测试内容 | 实际结果 |
|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 |
PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 1 | 1 | 0 |
| Python 与图形库学习 | 1 | 1 | 0 |
| 游戏界面实现 | 2 | 4 | 2 |
| 路径与碰撞逻辑实现 | 1 | 0.2 | 0.8 |
| 关卡设计 | 1 | 1 | 0 |
| AIGC 辅助开发 | 10 | 15 | 5 |
| 测试与修改 | 15 | 12 | 3 |
| README 与博客撰写 | 2 | 2 | 0 |
| 合计 | 33 | 36.2 | 10.8 |
仓库地址
https://github.com/zhewanyiershinicheng/arrow_game
实机演示
推荐自行下载游玩,保证没有后门,请放心运行
演示视频下载:https://pan.cyld.top/@s/U2FSGbxv









浙公网安备 33010602011771号