[P] 结对项目:花见小路
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 2026年春季软件工程 |
| 这个作业的要求在哪里 | [P] 结对项目:花见小路 |
| 我在这个课程的目标是 | 完成一个团队开发项目并在实践中学习软件开发理论 |
| 这个作业在哪个具体方面帮助我实现目标 | 体验结对编程,体会在结对编程中体现的编程实际问题 |
结对项目:博客问题清单
请将本文件在代码仓库外复制一份,一边阅读和完成结对项目、一边填写入代码仓库外的版本,或采取简记、语音备忘等方式记载较复杂问题的要点之后再补充。请不要将本文档内的作答提交到代码仓库。
→ 📖 Q0.0(P) 【你可以在结对结束后补充】如果你的代码仓库包含 AIGC 的部分,列举使用的工具、模型和使用范围。若未使用则填写:本组提交的全部代码不包含AI补全或生成的部分。
t3在优化阶段用到了 vibe coding。使用 trae 模型 gpt-5.3-codex
Chapter.0 wasm从安装到入门
引入
→ 📖 Q0.1(P) 请记录下目前的时间。
2026.04.09 15:35
调查
→ 📖 Q0.2(I) 【你可以在结对结束后另行补充。】作为本项目的调查:
请如实标注在开始项目之前对 Wasm 的熟悉程度分级,可以的话请细化具体的情况。(分别回答两人各自的情况)
I. 没有听说过;
II. 仅限于听说过相关名词;
III. 听说过,且有一定了解;
IV. 听说过,且使用 Wasm 实际进行过开发(即便是玩具项目的开发)。
请如实标注在开始项目之前对桌游花见小路的熟悉程度分级,可以的话请细化具体的情况。(分别回答两人各自的情况)
I. 不了解玩法和规则;
II. 听说过,且有一定了解;
总结
→ 📖 Q0.3(P) 请记录下目前的时间。
2026.04.09 15:58
Chapter.1 七色之缨
结对过程
→ 📖 Q1.1(P) 请记录下目前的时间。
2026.04.09 16:06
→ 📖 Q1.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
| PSP 阶段 | 内容 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| PLANNING | 计划 | ||
| - Estimate | 任务整体时间评估 | 5 | 4 |
| DEVELOPMENT | 开发 | ||
| - Analysis & Design Spec | 需求分析 & 生成设计规格 | 10 | 5 |
| - Technical Background | 技术背景学习 | 20 | 15 |
| - Coding Standard | 代码规范 | 5 | 5 |
| - Design | 具体设计 | 15 | 10 |
| - Coding | 编码实现 | 25 | 20 |
| - Code Review | 代码复审 | 5 | 5 |
| - Test Design | 测试设计 | 10 | 5 |
| - Test Implement | 测试实现 | 10 | 10 |
| REPORTING | 报告 | ||
| - Quality Report | 质量报告 | 10 | 10 |
| - Size Measurement | 工作量统计 | 5 | 5 |
| - Postmortem & Process Improvement Plan | 事后总结与改进 | 10 | 5 |
| TOTAL | 合计 | 130 | 94 |
- 完成编程任务期间,依次做了什么(例如查阅了哪些资料、如何设计判定逻辑、如何设计测试样例、遇到了什么问题、如何解决)。
- 查询资料:使用https://www.assemblyscript.org手册 阅读https://blog.csdn.net/gusushantang/article/details/151833337 了解as参数规范,了解导出函数相关规则。
- 涉及逻辑:严格按照文档中的 胜负判定规则 编写判断逻辑
function judge(board: Int8Array, round: i8): i8 { const myScore = scoreOf(board, 1); const oppScore = scoreOf(board, -1); const myCount = countOf(board, 1); const oppCount = countOf(board, -1); // 任意小轮结束后立即判胜 if (myScore >= 11) return 1; if (oppScore >= 11) return -1; if (myCount >= 4 && oppScore < 11) return 1; if (oppCount >= 4 && myScore < 11) return -1; // 前两轮未触发立即胜利则继续 if (round < 3) return 0; // 第三轮 if (myScore != oppScore) return myScore > oppScore ? 1 : -1; return tieBreakByTier(board); //最高分倾心标记获胜 } - 完成 judge 方法调用的辅助函数的编写。
- 设计测试用例:
- 分值达到 11 获胜
- 标记数达到 4 获胜
- 前两轮继续(返回 0)
- 第三轮总分高者胜
- 第三轮同分按最高档位判胜
- 第三轮平局(返回 2)
设计
→ 📖 Q1.3(P) 请说明你们为这个判定模块设计了哪些中间量或辅助函数;如果没有额外设计,也请说明为什么认为直接实现已经足够清晰。
设计的中间量/辅助函数:
- SCORE_BY_ID :角色分值表(A~G 对应 2,2,2,3,3,4,5)。避免硬编码。
- scoreOf(board, side) :统计某一方总分。将算分逻辑集中,主流程更清晰。
- countOf(board, side) :统计某一方标记数量。将计算标记物数量逻辑集中,主流程更清晰。
- hasAny(board, side, ...) :判断某方是否拥有某档位中的任意标记。减少重复判断代码。
- tieBreakByTier(board) :第三轮同分时的档位比较( G > F > D/E > A/B/C )。把复杂规则独立出来,提升可读性和可维护性。
→ 📖 Q1.4(I) 请说明在这样一个规则判定类模块中,如何避免“漏判”“错判”或分支顺序错误等问题。
胜负判定规则本身就像伪代码一样,只要严格按照胜负判定规则的分支顺序来写,即可保证实际函数与其一致(如果认为规则本身是正确的,那么实际实现就认为是正确的)。
胜负判定规则规定了 6 种产生胜负的情况,严格按顺序写可避免漏判。漏判和错判的问题还可以通过写测试来检查。
测试
→ 📖 Q1.5(P) 请说明你们设计了哪些测试用例,这些测试分别覆盖了哪一类规则或边界情况。
- 分值达到 11 获胜
- 标记数达到 4 获胜
- 前两轮继续(返回 0)
- 第三轮总分高者胜
- 第三轮同分按最高档位判胜
- 第三轮平局(返回 2)
→ 📖 Q1.6(I) 请说明你对“先写测试再实现”与“先实现再补测试”两种方式的理解。
先写测试再实现
先从逻辑角度明确正确标准与规则边界,这样测试会更严谨,从源头避免漏判、错判和分支顺序错误。同时也不会受到在写实现时一些想当然的错误思想的影响。
先实现再补测试
先完成实现再补测试容易在完成代码实现时,受编程语言特性影响产生一些错误思想,从而遗漏边界情况。这样在编写测试的时候可能也想不到这种情况了,使得测试也是流于形式
总结
→ 📖 Q1.7(P) 请记录下目前的时间,并根据实际情况填写 附录A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
2026.04.09 17:38
→ 📖 Q1.8(I) 请写下本部分的心得体会。
在完成代码实现时,严格按照规则优先级编写分支,能有效避免漏判、错判与顺序错误。同时先编写主函数再实现辅助函数也有助于在编码时始终不偏离主线。先写测试再实现能提前明确规则边界,避免受编码时的影响,从源头保证判定逻辑严谨。
结对编程也是两个人思想的碰撞,在两人有不同想法时,讨论后通常能得到更好的结果,一定程度上避免了一个人编码时可能的闭门造车的问题。
Chapter.2 不祥之影
准备
→ 📖 Q2.1(P) 请记录下目前的时间。
2026.04.09 17:43
→ 📖 Q2.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| PLANNING | 计划 | ||
| - Estimate | - 估计这个任务需要多少时间 | 5 | 5 |
| DEVELOPMENT | 开发 | ||
| - Analysis & Design Spec | - 需求分析 & 生成设计规格(确定要实现什么) | 10 | 10 |
| - Technical Background | - 了解技术背景(包括学习新技术) | 10 | 10 |
| - Coding Standard | - 代码规范 | 5 | 5 |
| - Design | - 具体设计(确定怎么实现) | 15 | 10 |
| - Coding | - 具体编码 | 30 | 20 |
| - Code Review | - 代码复审 | 5 | 20 |
| - Test Design | - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) | 5 | 5 |
| - Test Implement | - 测试实现(设计/生成具体的测试用例、编码实现测试) | 15 | 10 |
| REPORTING | 报告 | ||
| - Quality Report | - 质量报告(评估设计、实现、测试的有效性) | 10 | 10 |
| - Size Measurement | - 计算工作量 | 5 | 2 |
| - Postmortem & Process Improvement Plan | - 事后总结和过程改进计划(总结过程中的问题和改进点) | 10 | 5 |
| TOTAL | 合计 | 125 | 112 |
- 完成编程任务期间,依次做了什么(比如查阅了什么资料,随后如何进行了开发,遇到了什么问题,又通过什么方式解决);
- 设计主函数逻辑
export function calcCurrentState(history: string, boardInput: Int8Array): Int32Array { // 解析历史记录 const tokens = tokenizeHistory(history); // 遍历每条历史记录 for (let i = 0; i < tokens.length; i++) { const typeCode; // 操作类型 let actor = 0; // 操作玩家 // 1: 密约 if (typeCode == '1') { // 根据规则在场面上添加卡牌 actor = 1 - actor; continue; } // 2: 取舍(不入区域) if (typeCode == '2') { actor = 1 - actor; continue; } // 3: 赠送 if (typeCode == '3') { // 根据规则在场面上添加卡牌 actor = 1 - actor; continue; } // 4: 竞争 // 判断对手选择了哪部分 // 根据规则在场面上添加卡牌 actor = 1 - actor; } // 根据场面确定最终各 geisha 偏好 const out = new Int32Array(21); // 组装返回值 return out; } - 完成辅助函数的编写
- 遇到问题:
code - 'A'这样的写法不合法,查看 AS 变量类型相关规则。AssemblyScript 是强类型,禁止隐式转换。改为code - 'A'.charCodeAt(0)。
代码可复用性与需求变更
→ 📖 Q2.3(P) 请说明针对该任务,你们对 🧑💻 T1 中已实现的代码进行了哪些复用和修改。
没有进行复用和修改。
→ 📖 Q2.4(I) 请说明在编码实现时,可以采取哪些设计思想、考虑哪些设计冗余,来提高既存代码适应需求变更的能力。
可采用的设计思想
- 功能分离 :把历史解析、行动结算、标记更新、胜负判定拆成独立函数,规则变更时只改对应函数。
- 单向数据流 :输入 history + board ,输出新状态;中间不依赖全局状态,便于修改和测试。
- 接口稳定 :对外函数签名固定,内部可重构而不影响调用方。
可考虑的设计冗余
- 多命名导出冗余 :同时导出 calcCurrentState / calc_current_state / CalcCurrentState ,降低调用方出错风险。
- 输入校验冗余 :长度、字符集、动作格式、选择后缀都做检查,尽早抛出异常,减少未来可能的错误传播。
头脑风暴环节
→ 📖 Q2.5(P) 头脑风暴环节:
我们终于快要开始让程序玩游戏了!请尝试分析:T2 中不带 X 的操作记录比起实际对局多出了多少信息?如果加上 X,也就是失去了这部分信息的话,如何处理对小轮结束后状态的估计?
- T2 不带 X 时,能知道对手 密约 和 取舍 用了什么牌,相当于对手明牌。
- 每轮结束时的状态不再是单一确定的,而是所有可能局面的集合。从确定型状态机变为了非确定型状态机。
估计策略:- 每步行动后做约束更新:基于手牌数、行动次数、牌总量等信息过滤不可能分支,收缩不确定性
- 小轮结束时估计策略:
- 最可能状态:取后验概率最大的局面
- Monte Carlo 采样:从可行状态采样,统计标记分布和胜率估计
总结
→ 📖 Q2.6(P) 请记录下目前的时间,并根据实际情况填写 附录 A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
2026.04.09 19:40
→ 📖 Q2.7(I) 请写下本部分的心得体会。
本部分最大的体会是:先搭主题架构,不要迷失在代码细节中
本部分核心在于把每种行动的时序、规则、对场面的影响想清楚。我们先把主流程搭起来,再逐步补辅助函数,最后再把返回结构统一,这种先通路、后细化的方式明显提高了开发效率。
编码过程中, AS 的强类型约束给我们很深印象。像字符与数字的运算、字符串处理、数组类型都不能按 JS 一样默认可行,需要明确类型和转换。
Chapter.3 道途之荆
准备
→ 📖 Q3.1(P) 请记录下目前的时间。
2026.04.09 23:30
→ 📖 Q3.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| PLANNING | 计划 | ||
| - Estimate | - 估计这个任务需要多少时间 | 10 | 5 |
| DEVELOPMENT | 开发 | ||
| - Analysis & Design Spec | - 需求分析 & 生成设计规格(确定要实现什么) | 20 | 15 |
| - Technical Background | - 了解技术背景(包括学习新技术) | 40 | 30 |
| - Coding Standard | - 代码规范 | 10 | 5 |
| - Design | - 具体设计(确定怎么实现) | 40 | 30 |
| - Coding | - 具体编码 | 60 | 60 |
| - Code Review | - 代码复审 | 30 | 40 |
| - Test Design | - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) | 10 | 15 |
| - Test Implement | - 测试实现(设计/生成具体的测试用例、编码实现测试) | 20 | 10 |
| REPORTING | 报告 | ||
| - Quality Report | - 质量报告(评估设计、实现、测试的有效性) | 20 | 20 |
| - Size Measurement | - 计算工作量 | 5 | 5 |
| - Postmortem & Process Improvement Plan | - 事后总结和过程改进计划(总结过程中的问题和改进点) | 10 | 10 |
| TOTAL | 合计 | 265 | 245 |
- 完成编程任务期间,依次做了什么(比如查阅了什么资料,随后如何进行了开发,遇到了什么问题,又通过什么方式解决);
- 查阅在有限信息下蒙特卡洛搜索的策略
- 先完成一个最简单的策略,保证输入输出跑通
- 编写蒙特卡洛搜索需要的数据结构
- 移植模拟对局代码并做修改
- 编写蒙特卡洛搜索核心逻辑
- 与他人的策略进行测试并进行优化
头脑风暴环节
→ 📖 Q3.3(P) 头脑风暴环节:
假设提供更充裕的时间和资源,这个游戏中你能找到的最优策略有可能是什么形式的?进行调研并总结分析。你还可以在任务结束后试着实现(不计分)。
这是一个不完全信息、零和、两人博弈的卡牌小游戏 。在这个游戏里,单纯的完全信息搜索或我们的蒙特卡洛树搜索无法达到真正的最优。最优策略应趋向于寻找纳什均衡 。
调研如下:
-
反事实遗憾最小化:这是解决德州扑克(不完全信息)的标准。它在博弈中计算行动与不行动的 Regret,并根据遗憾值更新策略,最终收敛到纳什均衡。
- 思考: 这个游戏的状态数比扑克少的多,但X带来的隐藏信息(对手手牌、暗置牌、牌堆)数量依然庞大。这种算法需要离线学习,实现起来难度较大。
-
深度强化学习: 我们调研到 ReBeL 算法将强化学习与搜索结合,在不完全信息博弈中表现出色。
- 思考: 引入神经网络来拟合决策函数。我们可以训练一个神经网络,输入为当前的状态及概率分布,输出为各行动的胜率预测。在对局中,则无需像现在进行大量的模拟,能缩短决策时间。但这样工作量会大大增加。
-
残局完美求解器: 游戏的后期(只剩1-2个行动时),隐藏信息仅限于暗置牌和手牌。此时状态复杂度急剧下降。
- 思考: 预先计算所有可能的残局状态并存储。在游戏最后只剩1回合时,直接查表获取收益最大的策略。不过残局时蒙特卡洛搜索的复杂度也会急剧下降,再写一个求解器收益不大。
需求建模和算法设计
→ 📖 Q3.4(P) 请说明针对该任务,你们采取了哪些策略来优化决策。具体而言,怎么选择行动类型?选牌如何更优?如何编程实现。
针对该任务,我们实现了一个基于 信息集蒙特卡洛树搜索 配合启发式剪枝的方法。具体优化策略和编程实现如下:
- 状态确定化与信息集处理
- 策略: 由于无法知道对手的手牌和暗置牌,我们根据已知的公开信息,随机抽牌补全缺失状态,将“不完全信息”补全成“完全信息”。
- 实现:
determinize函数实现。我们维护了TOTAL数组和p.knownUsed。通过遍历牌堆,利用RNG随机生成对手的暗置牌、弃牌和当前手牌。
- 根节点动作剪枝
- 策略: 3/4 行动,尤其是 4,会产生庞大的分支。为了避免分支爆炸,我们引入了启发式剪枝。
- 实现:
trimRoot函数中,我们通过一个轻量级的评估函数rough(a)对所有候选动作进行快速打分。保留分数最高的MAX_ROOT_ACTIONS个动作进入 UCB 搜索阶段。
- 选牌与行动启发式优化
- 策略: 卡牌的价值并不等同,我们将 A-G 的价值定义为赢得倾向时的得分,即
[2, 2, 2, 3, 3, 4, 5] - 实现细节:
- 在 Rollout阶段,
rolloutAct会根据卡牌的SCORE总和进行打分,并且对不同的行动类型(2弃置、3赠送、4竞争)施加了0.35到0.75的惩罚系数,倾向于保留高价值卡牌用于自己得分。 - 在小轮结算评估时,将双方实际得分值进行对比,并通过 Sigmoid 函数平滑化作为胜率期望反给 MCTS 的节点。
- 在 Rollout阶段,
- 策略: 卡牌的价值并不等同,我们将 A-G 的价值定义为赢得倾向时的得分,即
- 决策选择
- 在完成
ITER次迭代后,我们并不是选择胜率最高,而是选择访问次数最多且平均收益最大的动作。
- 在完成
代码可复用性与需求变更
→ 📖 Q3.5(P) 请说明针对该任务,你们对 🧑💻 T2 中已实现的代码进行了哪些复用和修改。
在模拟的部分用到了判断每轮结果的方法,此处复用了 T2 代码以及相关的辅助函数。
- 复用了 history 解析思路
- 复用了行动结算逻辑
- 复用了“状态推进”建模方式
- 复用了组合判断/多重集匹配思想
→ 📖 Q3.6(I) 请说明在编码实现时,可以采取哪些设计思想、考虑哪些设计冗余,来提高既存代码适应需求变更的能力。
设计思想
- 分层解耦:把 replay 、 apply、 decide 分开。
- 参数化: ITER/UCB_C/MAX_ROOT_ACTIONS 独立成常量,便于后期优化调参。
设计冗余
- 多命名导出冗余: hanamikoji_action / HanamikojiAction / hanamikojiAction 同时支持。
- 兜底冗余: fallback 保证异常情况下也能至少返回合法动作。
- 合法性校验冗余:长度、分组、手牌可用性等多层检查,减少不合法输入导致的连锁错误。
软件度量
→ 📖 Q3.7(P) 请说明你们如何量度所实现的程序模块的有效性,例如:“如何说明我们的程序模块决策能力很强?”,尝试提出一些可能的定量分析方式或测试方式。
-
调参测试
-
将迭代次数ITER分别设为100、300、720、1500,实际测试发现,随着ITER数值增加,模块胜率呈现对数增长趋势,符合预期。
-
在相同思考时间前提下,对比开启剪枝(仅保留前几个动作)与关闭剪枝两种模式的胜率,测试结果显示,开启剪枝的版本胜率更高,说明剪枝逻辑有效。
-
-
启发式评估的消融实验
当前代码在模拟阶段出牌、根节点剪枝过程中,融入了经验设定(惩罚权重分别为0.35、0.75,同时参考SCORE的卡牌价值分布),为验证这些经验设定的作用,做了如下消融测试:
-
剥离所有专家知识设定,实现一个纯随机Rollout版本,让其与当前带启发式偏置的Rollout版本进行对打测试。
-
若当前带启发式偏置的版本胜率能明显碾压纯随机版本,就能直接证明,我们结合游戏理解设计的启发式函数,确实有效提升了模块的决策质量。
-
-
时间开销测试
- 记录模块在游戏不同阶段,运行单次迭代所需的平均CPU耗时。经过多次测试,模块可稳定在2秒时限内,完成足够深度的搜索,满足实际运行需求。
优化前:

优化后:

总结
→ 📖 Q3.8(P) 请记录下目前的时间,并根据实际情况填写 附录A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
2026-04-10 14:11
→ 📖 Q3.9(I) 请写下本部分的心得体会。
结对编程让我体验到平时我自己写代码的时候很多思路只是脑子里懂了,但我无法用语言表达出来。结对编程的时候要和队友交流自己的想法,这个过程就比较难受了。
结对项目总结
结对过程回顾和反思
→ 📖 Q4.1(P) 提供两人在讨论的结对图像资料。
→ 📖 Q4.2(P) 回顾结对的过程,反思有哪些可以提升和改进的地方。
首先是前期想得不够清楚就开始写代码。一开始图快,尤其在 T3 里,结果后面结构变复杂。本质问题是设计阶段没有好好设计。
其次是代码复用做得不好。T1 写过的东西,在 T2、T3 里其实并没有复用到。(感觉也不需要啊)
第三是分工有点模糊。有时候两个人一起盯一段代码,其实效率不高,这点我们做得不够好。
最后是时间预估偏乐观。越到后面,迭代时间明显不够用,我们低估了任务的复杂度。
→ 📖 Q4.3(I) 锐评一下你的搭档!并请至少列出三个优点和一个缺点。
- 反馈很快,改动后基本都会马上看结果。
- 能接受迭代,黑猫白猫抓到耗子就是好猫。
- 对文件结构的维护很好,后面没有把所有代码塞到一个文件里。
缺点也有一个明显的:
- 有时候可能交流不足,有谁敲键盘谁主导的感觉。
对结对编程的理解
→ 📖 Q4.4(I) 说明结对编程的优缺点、你对结对编程的理解。
结对编程的优点我觉得是可以一边写代码一边审查,而且写的人和审查的人不是同一个,能避免很多小错误。同时两个有不同想法时,讨论后的结果大多都要比两个想法中较差的那个强。
缺点可能是需要两人观念契合、思路相合,或者至少其中一方有着很强的包容能力。
在当今 vibe coding 的大趋势下,我感觉结对编程已经没有什么优势。但要是带入结对编程的思维,vibe coding 也可以理解成让 AI 担任驾驶员,人类担任领航员的过程,这也意味着人至少要阅读一遍 AI 写的代码,才能保证正确性,并且自己知道如何修改和扩展这份代码。所以其实也可以说结对编程的思想已经潜移默化地进入到 vibe coding 中了。
代码实现提交
→ 📖 Q4.5(P) 请提供你们完成代码实现的代码仓库链接。

浙公网安备 33010602011771号