2026 秋软件工程个人作业(第二次)
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | H202601软件工程与软件工程实践 |
| 这个作业要求在哪里 | 2026 秋软件工程个人作业(第二次) |
| 这个作业的目标 | 使用 Python 和 AIGC 完成“一箭又一箭”小游戏 |
| 学号 | 102402119 |
| GitHub 仓库 | 仓库连接 |
项目展示
1.1游戏开始界面

1.2游戏过程与通关界面

1.3碰撞反馈与失败界面

二、项目介绍
2.1 游戏规则
本项目实现 “一箭又又一箭” 点击解谜小游戏,游戏棋盘为 6×6 网格,棋盘上分布上、下、左、右四个方向的箭头。
玩家点击棋盘上的箭头,程序沿着箭头前进方向做路径检测:
- 若箭头到棋盘边界之间不存在其他箭头阻挡,该箭头执行飞行动画,飞出棋盘后消失移除。
- 若前进路径上存在其他箭头阻挡,箭头无法飞出,触发红色闪烁碰撞反馈,同时消耗一次失误机会。
- 玩家总共有 3 次失误机会,失误次数耗尽则本关游戏失败,可以点击按钮重新开始当前关卡。
- 将本关卡棋盘上所有箭头全部清除后,即可通关进入下一关。
- 项目内置 3 个设计完成的可通关关卡,全部关卡通关后展示全部通关结束界面。
2.2 界面设计
程序一共包含三类界面:开始界面、游戏游玩界面、结算界面(通关界面、失败界面)。
- 开始界面:展示游戏标题 “一箭又一箭”,提供蓝色【开始游戏】按钮,点击后进入第一关。
- 游戏游玩界面:绘制 6×6 网格棋盘,渲染棋盘内各个方向箭头;界面左上角显示当前关卡编号、剩余箭头数量、剩余失误次数信息。箭头被阻挡时会闪烁红色作为碰撞反馈,可飞出的箭头会播放飞出屏幕的动画。
- 通关界面:区分单关通关与全部通关。单关通关显示 “本关通关!”,提供【下一关】按钮;全部关卡通关显示 “全部通关!”。
- 失败界面:显示 “游戏失败” 提示,提供黄色【重新开始】按钮,点击重置当前关卡回到初始状态。
2.3 主要功能
- 图形化交互界面,支持鼠标点击选择棋盘上的箭头。
- 路径检测算法:沿箭头行进方向遍历行 / 列,检测路径上是否存在其他箭头障碍物。
- 箭头飞行动画与碰撞反馈动画:可飞出箭头播放位移飞出动画;被阻挡箭头红色闪烁提示碰撞。
三、 实现思路(迷宫类游戏,箭头 / 方向 / 关卡 + 路径检测)
3.1 箭头、方向表示
用枚举 / 常量代表 4 个移动方向:上、下、左、右。
//方向常量
const int UP = 0;
const int DOWN = 1;
const int LEFT = 2;
const int RIGHT = 3;
//方向偏移:{dx,dy},对应行、列的变化
int dirs[4][2]={{-1,0},{1,0},{0,-1},{0,1}};
箭头就是方向的可视化:向上箭头对应UP,移动坐标按dirs[UP]做坐标更新。
3.2 关卡如何表示
使用二维数组存储迷宫地图:
0代表通路可以走;1代表墙壁障碍物;
起点、终点记录一对坐标(start_x,start_y)、(end_x,end_y);
每一关就是一套不同的二维地图数组。
//示例关卡地图
int map[5][5]={
{1,1,1,1,1},
{1,0,0,0,1},
{1,1,1,0,1},
{1,0,0,0,1},
{1,1,1,1,1}
};
3.3 路径检测方法
路径检测作用:判断朝某个方向移动后,下一个格子是否越界、是否撞墙,能不能通行。
检测逻辑:
- 根据当前位置
(x,y)+ 方向偏移,算出下一位置nx = x+dx , ny = y+dy - 边界检测:
nx、ny不能小于 0,不能超过地图行列下标 - 障碍物检测:
map[nx][ny]不等于墙壁(不为 1) - 全部满足,说明该方向路径可行,可以移动;否则不能走。
关键代码片段
// 判断从(x,y)向direction方向是否可以走
bool canMove(int x,int y,int direction,int map[][5]){
int dx = dirs[direction][0];
int dy = dirs[direction][1];
int nx = x + dx;
int ny = y + dy;
//边界判断
if(nx<0||nx>=5||ny<0||ny>=5)
return false;
//墙壁判断
if(map[nx][ny]==1)
return false;
return true;
}
完整代码:
点击查看代码
import pygame
import sys
# 常量定义
WIDTH = 800
HEIGHT = 600
GRID_SIZE = 80
GRID_OFFSET_X = 80
GRID_OFFSET_Y = 120
MAX_MISTAKE = 3
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (120, 120, 120)
BLUE = (30, 80, 220)
RED = (220, 30, 30)
YELLOW = (255, 200, 0)
# 游戏状态
STATE_START = 0
STATE_PLAY = 1
STATE_WIN = 2
STATE_LOSE = 3
# 方向常量
DIR_RIGHT = 0
DIR_UP = 1
DIR_DOWN = 2
DIR_LEFT = 3
# 方向偏移:(行变化, 列变化)
DIR_DELTA = [
(0, 1), # 右
(-1, 0), # 上
(1, 0), # 下
(0, -1) # 左
]
# 方向像素增量(飞行动画每帧移动像素)
DIR_PIXEL_DELTA = [
(6, 0), # 右
(0, -6), # 上
(0, 6), # 下
(-6, 0) # 左
]
# 方向符号
DIR_SYMBOL = {
DIR_RIGHT: "→",
DIR_UP: "↑",
DIR_DOWN: "↓",
DIR_LEFT: "←"
}
class Arrow:
def __init__(self, row, col, dire):
self.row = row
self.col = col
self.dir = dire
self.flash_timer = 0
self.flying = False
# 动画坐标,静止时等于格子中心
self.anim_x, self.anim_y = self.get_center()
def get_center(self):
x = GRID_OFFSET_X + self.col * GRID_SIZE + GRID_SIZE // 2
y = GRID_OFFSET_Y + self.row * GRID_SIZE + GRID_SIZE // 2
return x, y
def get_rect(self):
return pygame.Rect(
GRID_OFFSET_X + self.col * GRID_SIZE,
GRID_OFFSET_Y + self.row * GRID_SIZE,
GRID_SIZE,
GRID_SIZE
)
def get_color(self):
if self.flash_timer > 0:
return RED
return BLUE
def start_fly(self):
self.flying = True
self.anim_x, self.anim_y = self.get_center()
def update_anim(self):
if not self.flying:
return
dx, dy = DIR_PIXEL_DELTA[self.dir]
self.anim_x += dx
self.anim_y += dy
def is_out_of_screen(self):
# 判断飞出可视区域
return not (0 < self.anim_x < WIDTH and 0 < self.anim_y < HEIGHT)
class Game:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("一箭又一箭")
self.clock = pygame.time.Clock()
self.state = STATE_START
self.cur_level = 0
self.arrows = []
self.mistake_left = MAX_MISTAKE
self.arrow_remain = 0
self.font_big = pygame.font.SysFont("simhei", 36)
self.font_mid = pygame.font.SysFont("simhei", 28)
self.font_arrow = pygame.font.SysFont("simhei", 42)
# 【修改】游玩界面重新开始按钮放在右上角,避开棋盘,不遮挡方格
self.btn_restart = pygame.Rect(WIDTH - 140, 10, 130, 45)
self.start_btn = pygame.Rect(WIDTH // 2 - 100, HEIGHT // 2 - 30, 200, 60)
self.next_level_btn = pygame.Rect(WIDTH // 2 - 100, HEIGHT // 2 + 20, 200, 60)
# 三个关卡:(行, 列, 方向)
self.levels = [
[
Arrow(1, 1, DIR_RIGHT),
Arrow(1, 4, DIR_UP),
Arrow(3, 2, DIR_DOWN),
Arrow(4, 5, DIR_LEFT),
],
[
Arrow(0, 2, DIR_RIGHT),
Arrow(2, 0, DIR_DOWN),
Arrow(3, 4, DIR_LEFT),
Arrow(5, 1, DIR_UP),
Arrow(1, 3, DIR_RIGHT),
],
[
Arrow(0, 0, DIR_DOWN),
Arrow(1, 5, DIR_LEFT),
Arrow(2, 2, DIR_UP),
Arrow(4, 1, DIR_RIGHT),
Arrow(5, 4, DIR_DOWN),
Arrow(3, 3, DIR_LEFT),
]
]
def load_level(self, idx):
self.arrows = []
for a in self.levels[idx]:
self.arrows.append(Arrow(a.row, a.col, a.dir))
self.arrow_remain = len(self.arrows)
self.mistake_left = MAX_MISTAKE
self.state = STATE_PLAY
def draw_start(self):
self.screen.fill(WHITE)
title = self.font_big.render("一箭又一箭", True, BLACK)
self.screen.blit(title, (WIDTH // 2 - title.get_width() // 2, 160))
pygame.draw.rect(self.screen, BLUE, self.start_btn)
btn_text = self.font_mid.render("开始游戏", True, WHITE)
self.screen.blit(
btn_text,
(
self.start_btn.x + self.start_btn.width // 2 - btn_text.get_width() // 2,
self.start_btn.y + self.start_btn.height // 2 - btn_text.get_height() // 2,
)
)
pygame.display.flip()
def draw_game(self):
self.screen.fill(WHITE)
text_lv = self.font_big.render(f"关卡:{self.cur_level + 1}", True, BLACK)
text_arrow = self.font_big.render(f"剩余箭头:{self.arrow_remain}", True, BLACK)
text_mistake = self.font_big.render(f"剩余失误:{self.mistake_left}", True, RED)
self.screen.blit(text_lv, (20, 10))
self.screen.blit(text_arrow, (20, 45))
self.screen.blit(text_mistake, (20, 80))
# 【新增】游戏进行界面绘制右上角重新开始按钮
pygame.draw.rect(self.screen, YELLOW, self.btn_restart)
rt = self.font_mid.render("重新开始", True, BLACK)
self.screen.blit(
rt,
(
self.btn_restart.x + self.btn_restart.width // 2 - rt.get_width() // 2,
self.btn_restart.y + self.btn_restart.height // 2 - rt.get_height() // 2,
)
)
# 绘制网格
for r in range(6):
for c in range(6):
rect = pygame.Rect(
GRID_OFFSET_X + c * GRID_SIZE,
GRID_OFFSET_Y + r * GRID_SIZE,
GRID_SIZE,
GRID_SIZE
)
pygame.draw.rect(self.screen, BLACK, rect, 2)
# 绘制箭头
for ar in self.arrows:
color = ar.get_color()
symbol = DIR_SYMBOL[ar.dir]
text = self.font_arrow.render(symbol, True, color)
# 飞行状态使用动画坐标;静止使用格子中心
if ar.flying:
x, y = ar.anim_x, ar.anim_y
else:
x, y = ar.get_center()
self.screen.blit(
text,
(
x - text.get_width() // 2,
y - text.get_height() // 2
)
)
pygame.display.flip()
def draw_win(self):
self.screen.fill(WHITE)
if self.cur_level + 1 >= len(self.levels):
t = self.font_big.render("全部通关!", True, BLUE)
else:
t = self.font_big.render("本关通关!", True, BLUE)
pygame.draw.rect(self.screen, BLUE, self.next_level_btn)
btn_text = self.font_mid.render("下一关", True, WHITE)
self.screen.blit(
btn_text,
(
self.next_level_btn.x + self.next_level_btn.width // 2 - btn_text.get_width() // 2,
self.next_level_btn.y + self.next_level_btn.height // 2 - btn_text.get_height() // 2,
)
)
self.screen.blit(t, (WIDTH // 2 - t.get_width() // 2, HEIGHT // 2 - 40))
pygame.display.flip()
def draw_lose(self):
self.screen.fill(WHITE)
t = self.font_big.render("游戏失败", True, RED)
tip = self.font_mid.render("点击重新开始重试本关", True, GRAY)
self.screen.blit(t, (WIDTH // 2 - t.get_width() // 2, HEIGHT // 2 - 60))
self.screen.blit(tip, (WIDTH // 2 - tip.get_width() // 2, HEIGHT // 2 - 10))
# 失败界面也绘制重启按钮
pygame.draw.rect(self.screen, YELLOW, self.btn_restart)
rt = self.font_mid.render("重新开始", True, BLACK)
self.screen.blit(
rt,
(
self.btn_restart.x + self.btn_restart.width // 2 - rt.get_width() // 2,
self.btn_restart.y + self.btn_restart.height // 2 - rt.get_height() // 2,
)
)
pygame.display.flip()
def find_path_target(self, arrow):
"""沿箭头方向路径检测,返回路径上第一个阻挡箭头;没有阻挡返回 None"""
dr, dc = DIR_DELTA[arrow.dir]
cr = arrow.row + dr
cc = arrow.col + dc
while 0 <= cr < 6 and 0 <= cc < 6:
for target in self.arrows:
if target is not arrow and target.row == cr and target.col == cc:
return target
cr += dr
cc += dc
return None
def handle_click_grid(self, mx, my):
clicked_arrow = None
for ar in self.arrows:
if ar.get_rect().collidepoint(mx, my) and not ar.flying:
clicked_arrow = ar
break
if clicked_arrow is None:
return
target = self.find_path_target(clicked_arrow)
if target is not None:
# 前方有遮挡:闪红提示,扣除失误
clicked_arrow.flash_timer = 15
self.mistake_left -= 1
if self.mistake_left <= 0:
self.state = STATE_LOSE
else:
# 前方无遮挡:启动飞行动画,不扣失误
clicked_arrow.start_fly()
def update(self):
# 更新闪烁计时
for ar in self.arrows:
if ar.flash_timer > 0:
ar.flash_timer -= 1
if ar.flying:
ar.update_anim()
# 飞出屏幕的箭头移除
remove_list = []
for ar in self.arrows:
if ar.flying and ar.is_out_of_screen():
remove_list.append(ar)
for ar in remove_list:
self.arrows.remove(ar)
self.arrow_remain = len(self.arrows)
if self.arrow_remain <= 0 and self.state == STATE_PLAY:
self.state = STATE_WIN
def run(self):
while True:
mx, my = pygame.mouse.get_pos()
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
if self.state == STATE_START:
if self.start_btn.collidepoint(mx, my):
self.cur_level = 0
self.load_level(0)
elif self.state == STATE_PLAY:
#【新增】游玩状态:优先判断点击重新开始按钮
if self.btn_restart.collidepoint(mx, my):
self.load_level(self.cur_level)
else:
self.handle_click_grid(mx, my)
elif self.state == STATE_WIN:
if self.cur_level + 1 < len(self.levels):
if self.next_level_btn.collidepoint(mx, my):
self.cur_level += 1
self.load_level(self.cur_level)
elif self.state == STATE_LOSE:
if self.btn_restart.collidepoint(mx, my):
self.load_level(self.cur_level)
self.update()
if self.state == STATE_START:
self.draw_start()
elif self.state == STATE_PLAY:
self.draw_game()
elif self.state == STATE_WIN:
self.draw_win()
elif self.state == STATE_LOSE:
self.draw_lose()
self.clock.tick(60)
if __name__ == "__main__":
try:
g = Game()
g.run()
except Exception as e:
print(e)
input("按回车退出")
四、AIGC 使用过程
AIGC使用过程记录表
| 子任务 | 借助何种AIGC技术 | AI实现或提供了什么 | 效果如何 | 人工修改 |
|---|---|---|---|---|
| 实现箭头路径检测 | ChatGPT | 生成四个方向的检测代码,判断箭头前进方向是否存在其他箭头 | 向上判断时出现越界,部分方向检测结果不准确 | 修改边界条件,统一方向偏移量,使路径检测按同一规则判断同行/同列箭头 |
| 实现箭头飞行动画 | Copilot | 补全箭头晃动或移动动画代码,建议箭头飞出棋盘后消失 | 基本可用,但动画速度和飞出逻辑不稳定 | 调整动画速度,明确飞行状态与静止状态,箭头飞出边界后从列表移除 |
| 实现碰撞反馈 | Copilot | 提供碰撞反馈实现思路,建议碰撞时闪烁红色并扣除失误 | 初始逻辑把碰撞反馈加在障碍物上,不符合题目要求 | 将碰撞反馈改为作用于被点击的箭头;前方有阻挡时,被点击箭头闪红并扣失误;前方无阻挡时箭头飞出 |
| 修复程序闪退与细节问题 | ChatGPT | 根据报错定位对象属性问题,给出修复方案 | 修复了闪退,但部分UI与交互细节仍不符合要求 | 补全失误计数与失误提醒UI,过滤空白格子点击响应,优化文字位置与按钮逻辑 |
五、测试结果
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 箭头飞出棋盘并消失 | 通过 |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,失误次数减 1 | 箭头保留不动,失误次数‑1 | 通过 |
| T03 | 点击位于边缘且朝向棋盘外的箭头 | 箭头正常消失,不发生越界错误 | 箭头消失,程序无数组越界崩溃 | 通过 |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 弹出通关提示,自动加载下一关布局 | 通过 |
| T05 | 失误次数耗尽 | 显示失败并允许重新开始 | 弹出失败界面,可点击按钮重玩本关 | 通过 |
| T06 | 游戏进行中重新开始 | 箭头布局和失误次数恢复 | 棋盘箭头重置,失误次数回到初始值 | 通过 |
六、PSP表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 2 | 2.5 | +0.5 |
| Python 与图形库学习 | 3 | 4 | +1 |
| 游戏界面实现 | 3 | 2.5 | -0.5 |
| 路径与碰撞逻辑实现 | 4 | 5 | +1 |
| 关卡设计 | 2 | 1.5 | -0.5 |
| AIGC 辅助开发 | 1 | 1 | 0 |
| 测试与修改 | 2 | 3 | +1 |
| README 与博客撰写 | 2 | 2 | 0 |
| 合计 | 19 | 21.5 | +2.5 |
七、心得体会
本次箭头小游戏开发过程中,AI 给我带来了不少帮助。遇到图形库 API 不熟悉时,AI 可以快速提供示例代码,帮我理解函数用法;在碰撞检测、边界判断等逻辑调试阶段,能够协助分析报错信息,定位代码 BUG;同时也辅助我完成测试用例、工时表格、博客文档的撰写,节省了整理文档的时间。
项目过程中也出现了不少问题。AI 生成的代码有时会和我的项目整体结构不匹配,不能直接复制运行,需要手动修改适配;部分游戏边界逻辑 AI 考虑不全,会出现数组越界、箭头状态异常的问题;有时候给出的方案存在逻辑漏洞,不能完全依赖,需要自己逐行阅读理解,结合需求进行验证。
经过本次项目,我的收获很多。我熟悉了 Python 图形库的基础使用,掌握了游戏碰撞、关卡切换、状态管理的实现思路;学会了划分开发任务,预估与统计项目工时,懂得编写测试用例验证程序功能;也明白了 AIGC 只是辅助工具,开发时要做到理解代码、独立思考,对 AI 输出内容进行甄别校验,不能直接照搬,提升了自己调试排错与工程实践的能力。
浙公网安备 33010602011771号