第二次作业-一箭又一箭

利用 AIGC 完成“一箭又一箭”网格箭头小游戏

一、作业信息

项目 内容
这个作业属于哪个课程 2026 秋软件工程与软件工程实践
这个作业要求在哪里 第二次个人作业
这个作业的目标 使用 Python 和 AIGC 完成“一箭又一箭”小游戏
学号 102402141
GitHub 仓库 项目仓库

二、项目展示

1. 开始界面

image

2. 游戏界面

image

3. 碰撞反馈

点击被阻挡箭头后的,箭头震动、阻挡箭头变红且生命值减少。

image

4. 通关或失败界面

image
image

三、项目介绍

“一箭又一箭”是一款基于 Python Tkinter 开发的网格箭头解谜小游戏。

玩家需要观察每支箭头的方向,判断箭头前方是否存在其他箭头。当前方没有阻挡时,点击箭头后它会飞出棋盘;如果前方存在阻挡,箭头不会消失,同时会触发碰撞反馈并扣除一次生命。

游戏包含四个难度逐步增加的关卡。现在每次进入关卡或重新开始时,都会随机生成新的箭头位置和方向,但棋盘仍然保证存在可行的通关顺序。

四、主要功能

  • 支持上、下、左、右四种箭头方向;
  • 支持鼠标点击箭头;
  • 自动检测箭头前方的阻挡关系;
  • 无阻挡时播放飞出棋盘动画;
  • 被阻挡时播放震动和红色错误反馈;
  • 每个关卡有三次失误机会;
  • 支持重新开始、通关、失败和下一关;
  • 支持随机生成保证可解的棋盘;
  • 使用自动化测试验证核心规则。

五、实现思路

1. 数据结构

项目主要使用以下数据结构:

  • Level:保存关卡名称、棋盘布局和失误次数;
  • Arrow:保存箭头所在行、列和方向;
  • Board:保存当前仍在棋盘上的箭头;
  • GameSession:管理当前关卡、生命值和游戏状态。

游戏状态包括:

START
PLAYING
WON
FAILED
ALL_CLEARED

2. 路径检测

点击箭头后,从箭头相邻的下一格开始,沿着箭头方向逐格检查,直到棋盘边界。

如果检测到其他箭头,则说明当前箭头被阻挡;如果一直检测到边界仍没有发现箭头,则当前箭头可以飞出。

路径检测只检查箭头前方,不检查后方和斜线方向。

核心逻辑可以概括为:

while 当前坐标没有超出棋盘:
    if 当前格存在其他箭头:
        说明被阻挡
    沿箭头方向继续移动

3. 随机棋盘生成

随机生成器继承每个关卡模板的棋盘大小、箭头数量和失误次数。

生成时采用逆向构造:

  1. 随机选择箭头位置;
  2. 从最终消除顺序的反方向逐步放置箭头;
  3. 每次只选择前方没有已放置箭头的方向;
  4. 因此构造出的棋盘天然保留一条合法消除顺序;
  5. 测试中再使用求解器验证棋盘确实可解。

核心代码讲解

1. 使用统一的数据结构表示方向

项目使用字符表示四个方向:

DIRECTION_DELTAS = {
    "^": (-1, 0),  # 向上
    "v": (1, 0),   # 向下
    "<": (0, -1),  # 向左
    ">": (0, 1),   # 向右
}

每个方向都被转换成行列坐标的增量。这样四个方向可以使用同一套路径检测逻辑,而不需要为上、下、左、右分别编写代码。

例如:

  • (-1, 0) 表示行号减一,箭头向上;
  • (1, 0) 表示行号加一,箭头向下;
  • (0, -1) 表示列号减一,箭头向左;
  • (0, 1) 表示列号加一,箭头向右。

箭头使用 Arrow 数据类表示:

@dataclass(frozen=True)
class Arrow:
    row: int
    col: int
    direction: str

    @property
    def position(self):
        return self.row, self.col

Arrow 保存箭头所在的行、列和方向。箭头的位置使用 (row, col) 表示,左上角棋盘格为 (0, 0)

2. 检测箭头前方是否被阻挡

棋盘使用字典保存当前仍然存在的箭头:

self._arrows: Dict[Position, Arrow] = {}

字典的键是箭头位置,值是对应的 Arrow 对象。当箭头飞出棋盘后,直接从字典中删除,因此后续路径检测不会再把已经消除的箭头当作阻挡物。

路径检测代码如下:

def blocking_arrow(self, arrow: Arrow) -> Optional[Arrow]:
    for position in self.path_cells(arrow):
        blocker = self._arrows.get(position)
        if blocker is not None:
            return blocker
    return None

path_cells() 从箭头相邻的下一格开始,沿着箭头方向逐格检查,直到棋盘边界:

def path_cells(self, arrow: Arrow):
    row_step, col_step = DIRECTION_DELTAS[arrow.direction]
    row = arrow.row + row_step
    col = arrow.col + col_step
    cells = []

    while self.in_bounds(row, col):
        cells.append((row, col))
        row += row_step
        col += col_step

    return tuple(cells)

这里有两个重要细节:

  1. 检测从箭头前方的下一格开始,不能从箭头自身开始,否则会把自己误判为阻挡;
  2. 每次循环先检查是否越界,再读取棋盘位置,可以避免上边界和左边界出现负索引问题。

路径上只要出现一支其他箭头,就判定为阻挡。箭头的朝向不会影响阻挡结果,只要它位于当前箭头的前进路径上即可。

一次路径检测最多扫描一整行或一整列,因此时间复杂度为:

O(max(行数, 列数))

额外空间复杂度为:

O(1)

3. 处理玩家点击

玩家点击棋盘后,首先判断当前游戏是否处于 PLAYING 状态,然后查找点击位置是否存在箭头:

def click(self, row: int, col: int) -> ActionResult:
    if self.status is not GameStatus.PLAYING:
        return ActionResult("ignored", status=self.status)

    arrow = self.board.arrow_at(row, col)
    if arrow is None:
        return ActionResult("invalid", status=self.status)

点击空格、棋盘外或已经消除的位置时,不改变棋盘,也不会扣除失误次数。

如果箭头前方存在阻挡,则保留箭头并扣除一次失误机会:

blocker = self.board.blocking_arrow(arrow)

if blocker is not None:
    self.mistakes_left -= 1

    if self.mistakes_left <= 0:
        self.mistakes_left = 0
        self.status = GameStatus.FAILED

    return ActionResult(
        "blocked",
        arrow,
        blocker,
        self.board.remaining,
        self.mistakes_left,
        self.status,
    )

界面收到 blocked 结果后,会让被点击的箭头震动,同时把真正的阻挡箭头短暂显示为红色,并播放生命值爆炸反馈。

如果前方没有阻挡,则移除箭头:

self.board.remove(arrow)

if self.board.remaining == 0:
    self.status = GameStatus.WON

return ActionResult(
    "fly",
    arrow,
    None,
    self.board.remaining,
    self.mistakes_left,
    self.status,
)

箭头的状态变化顺序是:

点击箭头
    ↓
检测前方路径
    ↓
有阻挡:扣除生命并播放碰撞反馈
    ↓
无阻挡:播放飞出动画
    ↓
动画结束后移除箭头
    ↓
判断当前关卡是否通关

游戏界面在播放动画期间会暂时忽略后续点击,避免一次操作重复扣除生命或重复删除箭头。

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

为了避免固定布局过于规律,项目增加了随机棋盘生成器。

随机棋盘会继承关卡模板的以下属性:

  • 棋盘行数和列数;
  • 箭头数量;
  • 每关的失误次数。

生成器采用逆向构造的方法保证棋盘存在解法:

for _ in range(count):
    candidates = []

    for position in remaining_positions:
        available = [
            direction
            for direction in directions
            if not _has_arrow_ahead(
                position,
                direction,
                occupied,
                rows,
                cols,
            )
        ]

        if available:
            candidates.append((position, available))

    position, available = randomizer.choice(candidates)
    direction = randomizer.choice(available)

    symbols[row][col] = direction
    occupied.add(position)
    remaining_positions.remove(position)

生成过程中的 occupied 表示已经放置的箭头。每次放置新箭头时,只从前方没有已放置箭头的位置和方向中选择。

假设箭头的最终点击顺序为:

A → B → C → D

生成器会按照反方向:

D → C → B → A

进行构造。这样在最终棋盘中,先点击 A 时,A 的路径不会被后续箭头阻挡;点击 A 后,再点击 B,以此类推。因此构造过程天然保留了一条合法的消除顺序。

生成完成后,项目还会使用求解器对棋盘进行验证:

def is_solvable(level: Level) -> bool:
    return find_solution(level) is not None

这样既通过生成算法保证可解,又通过自动化测试进行二次确认。

六、AIGC 使用过程

第一次协作:需求分析和代码结构

image

da036aa1d7a2c6256db3f0bb91fc3aaa

  • 提出的要求:根据作业要求创建 PRD.md,明确游戏规则、界面、测试和交付要求。
  • AI 提供的内容:整理了产品目标、游戏状态、关卡要求、测试要求和博客要求。
  • 实际效果:后续开发有了明确的功能边界。
  • 人工检查:结合老师作业页面重新检查需求,并补充了随机棋盘、动画和博客记录要求。

第二次协作:界面和 Bug 修复

image

  • 遇到的问题:Tkinter 启动时报错,首页和游戏界面视觉效果也不够像游戏。
  • 提出的要求:修复启动错误,优化浅色主题、首页布局、箭头颜色和碰撞反馈。
  • AI 提供的内容:定位并删除了 Tkinter 不支持的 spacing3 参数,并重新设计了首页和游戏界面。
  • 实际效果:程序可以正常进入界面,首页和游戏页更加清晰。
  • 人工检查:实际运行程序,并测试飞出动画、阻挡反馈和生命值变化。

第三次协作:随机生成可解棋盘

  • 遇到的问题:固定关卡的箭头方向仍然存在明显规律。
  • 提出的要求:在保证棋盘有解的情况下随机生成箭头位置和方向。
  • AI 提供的内容:实现了基于逆向构造的随机关卡生成器。
  • 实际效果:每次开始或重新开始都会得到不同的棋盘。
  • 人工检查:运行自动化测试,并使用多个随机种子验证棋盘可解。

七、自动化测试

运行命令:

python -m unittest discover -s homework2/tests -v

本项目共运行 15 个自动化测试,全部通过。

编号 测试内容 预期结果 实际结果
T01 点击前方无阻挡的箭头 箭头飞出并消失 通过
T02 点击前方有阻挡的箭头 箭头保留并扣除一次生命 通过
T03 点击边缘且朝向棋盘外的箭头 正常飞出,不发生越界 通过
T04 清空当前关卡 显示通关并进入下一关 通过
T05 生命值耗尽 显示失败界面 通过
T06 重新开始当前关卡 棋盘重新生成,生命值恢复 通过
T07 点击空格 游戏状态不变 通过
T08 随机棋盘生成 每张棋盘都可以求解 通过

八、PSP 表格

阶段 预计耗时 实际耗时 差异说明
需求分析和 PRD 整理 30min 25min
核心规则实现 30min 30min
Tkinter 界面开发 30min 40min
动画和视觉优化 30min 37min
随机棋盘生成 30min 28min
自动化测试 30min 25min
文档和博客整理 1h 1.5h

九、Git 开发过程

本项目按照功能阶段进行了多次提交:

2218e47 feat: implement arrow game core rules
514a4ba feat: add tkinter game interface and feedback
55dff4 test: expand regression coverage and add usage docs
7466b4b fix: remove unsupported tkinter label option
7d9865c feat: polish home screen and feedback animations
7fbefa8 feat: increase level depth and speed up fly animation
cf86675 style: randomize fixed level direction layouts
8ec79db feat: generate solvable random boards

这些提交分别对应核心规则、界面、测试、Bug 修复、动画优化和随机关卡等开发阶段。

十、运行方式

在项目根目录执行:

python -m homework2

运行测试:

python -m unittest discover -s homework2/tests -v

十一、心得体会

这次开发中,AIGC 工具在需求整理、代码结构设计、Bug 定位和测试编写方面提供了帮助。但 AI 生成的代码不能直接视为正确代码,仍然需要自己运行程序、查看报错、设计测试并检查实际效果。

本项目中比较重要的人工修改包括:

  • 修复 Tkinter 不支持的控件参数;
  • 调整首页和游戏界面的颜色与布局;
  • 修改箭头飞出和碰撞反馈动画;
  • 重新设计随机棋盘生成逻辑;
  • 使用自动化测试确认随机棋盘不会出现无解情况。
posted @ 2026-09-22 16:16  yesx  阅读(11)  评论(0)    收藏  举报