“花见小路”结对项目
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 2026年春季软件工程 |
| 这个作业的要求在哪里 | 结对项目:花见小路 |
| 我在这个课程的目标是 | 掌握软件工程基本理论,具备需求、设计、质量、项目管理及团队协作能力,了解前沿技术发展 |
| 这个作业在哪个具体方面帮助我实现目标 | 学习结对编程知识,进行结对编程实践 |
结对项目:博客问题清单
→ 📖 Q0.0(P) 【你可以在结对结束后补充】如果你的代码仓库包含 AIGC 的部分,列举使用的工具、模型和使用范围。若未使用则填写:本组提交的全部代码不包含AI补全或生成的部分。
本组代码仓库包含 AIGC 辅助生成的部分,使用工具为 Gemini 3 Pro,主要用于代码局部实现、测试用例构造与错误分析,并在 Chapter 3 辅助思考最优策略。
Chapter.0 wasm从安装到入门
引入
→ 📖 Q0.1(P) 请记录下目前的时间。
19:30
调查
→ 📖 Q0.2(I) 【你可以在结对结束后另行补充。】作为本项目的调查:
请如实标注在开始项目之前对 Wasm 的熟悉程度分级,可以的话请细化具体的情况。(分别回答两人各自的情况)
I
I. 没有听说过;
II. 仅限于听说过相关名词;
III. 听说过,且有一定了解;
IV. 听说过,且使用 Wasm 实际进行过开发(即便是玩具项目的开发)。
请如实标注在开始项目之前对桌游花见小路的熟悉程度分级,可以的话请细化具体的情况。(分别回答两人各自的情况)
I
I. 不了解玩法和规则;
II. 听说过,且有一定了解;
总结
→ 📖 Q0.3(P) 请记录下目前的时间。
19:40
Chapter.1 七色之缨
结对过程
→ 📖 Q1.1(P) 请记录下目前的时间。
19:40
→ 📖 Q1.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
- 完成编程任务期间,依次做了什么(例如查阅了哪些资料、如何设计判定逻辑、如何设计测试样例、遇到了什么问题、如何解决)。
查阅资料:
- 学习了 AssemblyScript 的基础语法,特别是 Int8Array 的使用方式以及如何导出函数
- 研究了 Node.js 环境下如何加载 .wasm 模块及其配套的 debug.js 进行测试
判定逻辑设计:
- 检查 board 和 round 的输入合法性
- 使用单次循环遍历 board,同时累加双方总分和标记数
- 先进行立即胜利的判定,如果无法判断且 round === 3 则进入平局判断
- 进行层级检查,只要最高非空档位无法区分则判定平局
测试样例设计:
- 测试了恰好 11 分和超过 11 分的情况
- 测试了 4 枚标记但总分不到 11 分的情况
- 测试了 Round 1/2 在无胜负时返回 0 的情况
- 测试了总分高者胜的情况
- 测试了总分相等但档位高者胜的情况
- 测试了同一档位冲突导致平局的情况
- 测试了非法输入的情况
遇到的问题与解决:
- 最初在档位判定时,简单按索引 6 到 0 降序排列。之后测试时发现 A/B/C 属于同档位,所以修改为分组检查,只要同一档位两人都有标记即判定平局。
设计
→ 📖 Q1.3(P) 请说明你们为这个判定模块设计了哪些中间量或辅助函数;如果没有额外设计,也请说明为什么认为直接实现已经足够清晰。
- 我们设计了分值映射数组 scores,将角色 A-G 对应的分值 [2, 2, 2, 3, 3, 4, 5] 预设在数组中,这样可以通过索引访问来计算得分
- 我们设计了状态统计量 myScore、oppScore、myCount、oppCount,在单次遍历 board 中累加双方的总分和获得标记的数量
- 我们设计了优先级数组 tiers,将 7 个角色按优先级划分为四个档位 [[6], [5], [4, 3], [2, 1, 0]],这种结构将比较最高非空档位的抽象规则转化为了可遍历的数据结构
→ 📖 Q1.4(I) 请说明在这样一个规则判定类模块中,如何避免“漏判”“错判”或分支顺序错误等问题。
我们在代码中构建了清晰的优先级结构,立即胜利判定是最高优先级,随后是循环判断和最终判定,这种自上而下的结构确保了逻辑流不会出现分支错误。并且我们通过一套覆盖所有关键路径的测试集进行闭环验证,针对每种可能设计两种测试数据,确保不会出现错判和漏判。
测试
→ 📖 Q1.5(P) 请说明你们设计了哪些测试用例,这些测试分别覆盖了哪一类规则或边界情况。
- 一方分值达到 11 分而获胜
- 一方获得至少 4 枚倾心标记而获胜
- 前两小轮结束时尚未满足胜利条件,应返回 0
- 第三小轮结束时,总分不同,由总分高者获胜
- 第三小轮结束时,总分相同,由最高档位倾心标记判定胜负
- 第三小轮结束时平局,应返回 2
- 边界与防御性测试
→ 📖 Q1.6(I) 请说明你对“先写测试再实现”与“先实现再补测试”两种方式的理解。
先写测试再实现的核心是需求驱动,在写下第一行逻辑代码前先根据规则书构造出输入与预期输出。这种方法迫使开发者在动手前必须彻底理清规则,虽然初期的开发节奏较慢,但能有效防止编码时的逻辑盲点。先实现再补测试的核心是功能驱动,开发者凭借对逻辑的直觉快速构建出程序的主体。这种方法可以让代码快速成型,在初期不需要考虑各种极端的情况,但补写测试时开发者往往会不自觉地根据已有代码的逻辑来写用例而非根据原始规则,导致确认偏差。
总结
→ 📖 Q1.7(P) 请记录下目前的时间,并根据实际情况填写 附录A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
20:20
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| PLANNING | 计划 | ||
| - Estimate | - 估计这个任务需要多少时间 | 1 | 1 |
| DEVELOPMENT | 开发 | ||
| - Analysis & Design Spec | - 需求分析 & 生成设计规格(确定要实现什么) | 10 | 6 |
| - Technical Background | - 了解技术背景(包括学习新技术) | 3 | 2 |
| - Coding Standard | - 代码规范 | 1 | 1 |
| - Design | - 具体设计(确定怎么实现) | 2 | 1 |
| - Coding | - 具体编码 | 10 | 10 |
| - Code Review | - 代码复审 | 5 | 7 |
| - Test Design | - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) | 3 | 1 |
| - Test Implement | - 测试实现(设计/生成具体的测试用例、编码实现测试) | 5 | 4 |
| REPORTING | 报告 | ||
| - Quality Report | - 质量报告(评估设计、实现、测试的有效性) | 5 | 2 |
| - Size Measurement | - 计算工作量 | 5 | 2 |
| - Postmortem & Process Improvement Plan | - 事后总结和过程改进计划(总结过程中的问题和改进点) | 5 | 3 |
| TOTAL | 合计 | 55 | 40 |
→ 📖 Q1.8(I) 请写下本部分的心得体会。
花见小路的规则看似简单,但实际编码时判定顺序至关重要。我们通过将复杂的规则解构为自己理解的层级结构,让代码的逻辑变得逐渐清晰。在开发过程中,我们遇到了 Index out of range 的错误,这让我们意识到在 AssemblyScript 这种底层语言中对 TypedArray 的操作必须极其谨慎。最初我们尝试通过手动构造数据来测试,效率极低且容易遗漏。在引入基于 Node.js assert 的自动化测试脚本后,我们可以一键验证包括“最高档位冲突判定”在内的所有边缘情况。本次任务让我明白编写能够运行的代码只是第一步,编写健壮、可测试且符合工程规范的代码才是软件工程的核心要求。
Chapter.2 不祥之影
准备
→ 📖 Q2.1(P) 请记录下目前的时间。
20:20
→ 📖 Q2.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
- 完成编程任务期间,依次做了什么(比如查阅了什么资料,随后如何进行了开发,遇到了什么问题,又通过什么方式解决);
查阅资料:
- 购入了花见小路桌游,借助按照样例摆牌快速理解逻辑
- 对着教程学习 AssemblyScript 中字符串处理和二维数组操作的方法
状态推演逻辑设计:
- 实现了 calc_current_state 主函数,遍历历史记录并逐步更新状态
- 设计了 refresh_table 函数用于根据单个行动更新目前牌桌状态
- 设计了 update_board_from_table 函数将牌桌结果结算到分数计算表中
遇到的问题与解决:
- 不清楚AssemblyScript如何将行动历史按空格分隔成行动指令,查阅教程后用计数字符后分割的思路实现了
- 对于 flag 的计算(count % 2),需要理解奇数轮次是己方先手,偶数轮次是对方先手
- 一开始refresh_table的部分理解有误,还考虑了艺妓分值,最后发现只要计算礼物牌数就可以了
- 对于行动3和4的判定逻辑的实现有些无从下手,最后使用了穷举的笨方法orz
代码可复用性与需求变更
→ 📖 Q2.3(P) 请说明针对该任务,你们对 🧑💻 T1 中已实现的代码进行了哪些复用和修改。
复用了board的状态表示格式,别的好像都没有用到T1的地方...
→ 📖 Q2.4(I) 请说明在编码实现时,可以采取哪些设计思想、考虑哪些设计冗余,来提高既存代码适应需求变更的能力。
我们将指令解析与分数状态更新的逻辑分别封装为独立函数,当计分规则发生变化时,只需修改这两个函数,无需改动主函数流程。并且在 refresh_table 函数中,我们对输入的行动字符串进行合法性检查,防止数组或字符串索引越界
头脑风暴环节
→ 📖 Q2.5(P) 头脑风暴环节:
我们终于快要开始让程序玩游戏了!请尝试分析:T2 中不带 X 的操作记录比起实际对局多出了多少信息?如果加上 X,也就是失去了这部分信息的话,如何处理对小轮结束后状态的估计?
加上X,也就相当于模拟一方玩家所能看到的牌局情况(看不见对手1和2扣住的牌)
所以想要猜测对手的牌局情况,有以下几种思路:
-
概率推断:
基于已知的牌分布(总共有 21 张牌:ABC各2张,DE各3张,F4张,G5张),通过观察对手的行动模式,推断其可能持有的牌 -
约束满足:
利用已知信息建立约束条件(如:某张牌不可能同时在双方手中),枚举所有可能的隐藏状态,排除违反规则的组态 -
对手建模:
构建对手的策略模型,预测其在不同情况下的行为,根据对手的选择反推其手牌构成
总结
→ 📖 Q2.6(P) 请记录下目前的时间,并根据实际情况填写 附录 A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
22:20
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| PLANNING | 计划 | ||
| - Estimate | - 估计这个任务需要多少时间 | 2 | 2 |
| DEVELOPMENT | 开发 | ||
| - Analysis & Design Spec | - 需求分析 & 生成设计规格 | 15 | 12 |
| - Technical Background | - 了解技术背景 | 5 | 8 |
| - Coding Standard | - 代码规范 | 2 | 2 |
| - Design | - 具体设计 | 10 | 15 |
| - Coding | - 具体编码 | 60 | 75 |
| - Code Review | - 代码复审 | 15 | 20 |
| - Test Design | - 测试设计 | 10 | 8 |
| - Test Implement | - 测试实现 | 15 | 18 |
| REPORTING | 报告 | ||
| - Quality Report | - 质量报告 | 10 | 8 |
| - Size Measurement | - 计算工作量 | 5 | 5 |
| - Postmortem & Process Improvement Plan | - 事后总结 | 10 | 7 |
| TOTAL | 合计 | 159 | 180 |
→ 📖 Q2.7(I) 请写下本部分的心得体会。
这章节实验主要难点在于实现行动3和行动4的逻辑,尤其是需要准确判断当前行动者是谁才能正确计算分数。我们目前采用的是直接穷举的方式来实现,虽然能用但不知道是否有更优雅的做法。另外代码如果采用分层设计、解耦各个模块,改动起来会更方便,否则每次调整规则都得重新梳理整个逻辑。一开始我把 refresh_table 和 update_board_from_table 都写在主函数里,结果发现主函数变得极其复杂,维护起来也很麻烦。
Chapter.3 道途之荆
准备
→ 📖 Q3.1(P) 请记录下目前的时间。
19:50
→ 📖 Q3.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
- 完成编程任务期间,依次做了什么(比如查阅了什么资料,随后如何进行了开发,遇到了什么问题,又通过什么方式解决);
查阅资料:
- 分析了T2中实现的状态推演逻辑,思考如何在AI决策中复用
- 对着教程学习AssemblyScript中的数组操作、排序算法和字符串处理方法
AI策略设计:
- 设计了卡牌价值评估函数calcValue,综合考虑基础分值、board状态和可见度
- 实现了四种行动类型的选择逻辑,按照优先级依次尝试
- 针对赠予和竞争设计了具体的牌组分配策略
- 实现了对手回应时的最优选择逻辑
遇到的问题与解决:
- 最初在设计行动选择时,简单按固定顺序尝试。后来发现应该根据手牌数量和已用行动动态调整
- 在实现行动4的牌组分配时,需要枚举所有可能的分组方式并选择最优解
- 对于对手回应的处理,需要在对方视角下重新评估牌的价值
头脑风暴环节
→ 📖 Q3.3(P) 头脑风暴环节:
假设提供更充裕的时间和资源,这个游戏中你能找到的最优策略有可能是什么形式的?进行调研并总结分析。你还可以在任务结束后试着实现(不计分)。
《花见小路》属于不完全信息博弈,目前其最优策略研究主要有两个方向:
基于深度学习的反事实遗憾最小化 (Deep CFR)
这是德州扑克 AI(如 Libratus、Pluribus)用的核心技术。花见小路的状态空间比德州扑克小得多,完全可以用 CFR 算法迭代出纳什均衡策略。
其策略不再是简单的 if-else,而是一个巨大的策略表(或者神经网络),输入是当前的牌局历史序列 + 手牌,输出是每种行动(1密约/A 牌、3赠予/ABC 组合...)的概率分布。
它几乎无法被人类利用弱点,因为它是平衡的。它会在某些时候故意打出看似“亏了”的牌(比如送对手一张好牌),但目的是为了在后续回合中构建更隐蔽的陷阱。
蒙特卡洛树搜索 (MCTS) + 对手建模
在不完全信息下,标准的 MCTS 需要改造为 POMCP (部分可观测蒙特卡洛规划)。
在每次决策时,根据对手的历史行动,推断对手大概率没有什么牌、小概率有什么牌,然后针对这个分布进行千次自我对弈模拟,选出胜率最高的那条路。
对于《花见小路》这种一局只用 4 次行动的短游戏,单纯的价值计算和贪心算法容易短视。如果用 MCTS 模拟到终局,它能看到类似“现在用密约锁住这张 2 分牌,虽然眼下小亏,但终局会因为对手卡手而大胜”的结论。
需求建模和算法设计
→ 📖 Q3.4(P) 请说明针对该任务,你们采取了哪些策略来优化决策。具体而言,怎么选择行动类型?选牌如何更优?如何编程实现。
1. 行动类型选择的优先级固化(全局最优贪心)
我们没有做复杂的搜索树,而是依据游戏经验锁死了行动优先级:
密约 (1) > 赠予 (3) > 竞争 (4) > 取舍 (2)
理由:密约是零风险纯收益,必先手用掉。赠予和竞争虽然会暴露牌,但能处理多张手牌,效率高于取舍(取舍是纯负收益)。代码里用 used.includes 判断顺序,逻辑清晰且执行极快。
2. 选牌策略的差异化实现
这是最体现“策略”的地方,我们针对不同行动设计了完全相反的选牌逻辑:
- 自己打密约/自己取舍:找
max或min= - 自己打赠予/竞争:
- 赠予:选价值最低的 3 张给对手看
- 竞争:用 Maximin 算法分组
- 回复对手时:
- 回复赠予:选价值最高的那张拿走
- 回复竞争:选总分更高的那组拿走
3. 价值函数辅助决策
为了让 calcValue 既能看懂局势,又不需要复杂计算,我们用了整数量化放大法:
let value = BASE * 100; // 基础分权重大
value += (200 - board * 100); // 形势紧迫度(没拿过是+200,拿过了就扣分)
if (unseen <= 2) value += 200; // 稀缺性绝杀加成
理由:这个公式保证了 AI 会去抢 5 分大牌,但如果大牌已经被对手拿了 2 个标记(board 高了),AI 会果断转去投资那些差一点就拿满的 2-3 分小牌。
代码可复用性与需求变更
→ 📖 Q3.5(P) 请说明针对该任务,你们对 🧑💻 T2 中已实现的代码进行了哪些复用和修改。
-
复用calc_current_state函数的思路来统计已使用的行动类型和已出现的牌
-
依旧沿用board格式
-
生成行动字符串时遵循T2中的格式规范
→ 📖 Q3.6(I) 请说明在编码实现时,可以采取哪些设计思想、考虑哪些设计冗余,来提高既存代码适应需求变更的能力。
可以考虑设计一个策略切换机制,例如先定义一个统一的策略接口,然后根据对手风格和游戏阶段动态选择不同的策略实现。还可以让四个基本行动的优先级支持动态调整,以适应局势变化。
软件度量
→ 📖 Q3.7(P) 请说明你们如何量度所实现的程序模块的有效性,例如:“如何说明我们的程序模块决策能力很强?”,尝试提出一些可能的定量分析方式或测试方式。
统计胜率:统计在大量对局中获胜的比例
与固定强度的baseline AI进行若干场对局,记录胜/负/平的次数,计算胜率 = (胜 + 0.5×平) / 总场次
如果胜率显著高于随机策略(>50%),就能说明决策能力很强。
测试决策稳定性:在相似局面下能否做出相似决策
收集大量相似的游戏状态(相同的board、相似的手牌),统计AI在这些状态下选择相同行动的比例,计算一致性指数 = 最常见行动的次数 / 总次数
高一致性表明策略稳定可靠,低一致性可能意味着随机性或bug
进行压力测试
构造极端情况:手牌极差、比分大幅落后等,测试AI在不利条件下的表现
真人评价
邀请桌游爱好者与AI对局,收集主观反馈
总结
→ 📖 Q3.8(P) 请记录下目前的时间,并根据实际情况填写 附录A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
23:30
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| PLANNING | 计划 | ||
| - Estimate | - 估计这个任务需要多少时间 | 3 | 3 |
| DEVELOPMENT | 开发 | ||
| - Analysis & Design Spec | - 需求分析 & 生成设计规格 | 20 | 25 |
| - Technical Background | - 了解技术背景(AI策略调研) | 30 | 45 |
| - Coding Standard | - 代码规范 | 3 | 3 |
| - Design | - 具体设计(策略算法设计) | 25 | 35 |
| - Coding | - 具体编码 | 120 | 150 |
| - Code Review | - 代码复审 | 30 | 40 |
| - Test Design | - 测试设计 | 20 | 15 |
| - Test Implement | - 测试实现 | 30 | 35 |
| REPORTING | 报告 | ||
| - Quality Report | - 质量报告 | 15 | 12 |
| - Size Measurement | - 计算工作量 | 10 | 10 |
| - Postmortem & Process Improvement Plan | - 事后总结 | 15 | 12 |
| TOTAL | 合计 | 321 | 385 |
→ 📖 Q3.9(I) 请写下本部分的心得体会。
T1和T2是确定性的逻辑,到了T3直接变成信息缺失下的博弈,计算量一下子爆炸了。不仅要考略是否会超时,还得根据已经打出的牌和手里的牌,去猜对手可能藏了什么牌型,然后才能做最优决策。更麻烦的是桌游的牌组是随机的,手牌顺的时候和手牌烂的时候,策略完全不一样——顺的时候要维持优势,逆风了还得想办法翻盘。之后能不能不要设置时间限制......复杂的判断都不敢加
结对项目总结
结对过程回顾和反思
→ 📖 Q4.1(P) 提供两人在讨论的结对图像资料。
→ 📖 Q4.2(P) 回顾结对的过程,反思有哪些可以提升和改进的地方。
-
没有边完成任务边记录: question.md的文档是在每完成了一整章的任务后填写的,这导致了一些具体的讨论和决策过程没有被记录下来。如果能在编码的同时就同步更新文档,也许会更加完整地保留我们的思考轨迹。
-
技术预研不足: 开始任务前,我们都不太了解AssemblyScript,很多时候都是边看教程边写代码;T3之前,我们也没有充分调研现有的AI策略和相关算法,导致在设计阶段花费了较多时间探索方向。如果能在项目初期就进行更系统的学习,可能会找到更优的解决方案。
→ 📖 Q4.3(I) 锐评一下你的搭档!并请至少列出三个优点和一个缺点。
优点:有上进心、有实力、有丰富想象力
缺点:太强了我有点自卑orz
对结对编程的理解
→ 📖 Q4.4(I) 说明结对编程的优缺点、你对结对编程的理解。
结对编程的优点:
- 提升代码质量:实时审查使得语法错误、逻辑漏洞、边界条件遗漏等问题在写出的瞬间就能被纠正,减少后续调试成本。
- 知识共享与技能传递:新人可以快速熟悉代码库、业务逻辑和团队规范;老手也能从对方身上学到新的技巧或视角。团队“公共知识”积累更快,降低人员流动风险。
- 增强团队协作与责任感:两人共同拥有代码,减少个人英雄主义,也避免出现只有某个人能改某块代码的单点故障。
- 促进纪律性:因为有人在看,驾驶员会更遵守编码规范、写更清晰的注释、避免偷懒取巧。
结对编程的缺点:
- 人力成本看似翻倍:如果任务可以完美并行分解,结对会使总工时上升。但在复杂或模糊需求下,结对减少的返工和沟通成本往往被低估。
- 对性格和习惯要求高:需要双方愿意沟通、互相尊重。性格强势或内向、习惯独自思考的人容易产生冲突或沉默,反而降低效率。
- 不适用于所有任务:极其简单、重复性的代码,或者需要大量独立调研的原型探索,结对收益不大。
- 容易产生伪结对:如果一个人根本没在看,或者两人一起陷入同一个误区,结对就成了形式主义。
我对结对编程的理解:
结对编程远不止两个人写同一份代码,它是一种实时协作的思维流。对于T3这种博弈决策类任务,一个人可以专注实现当前策略,另一个人从对手的角度去攻击当前逻辑,提前发现策略漏洞。这比写完代码再去测试要高效得多。我认为结对的本质是用沟通成本替换后期调试成本和知识孤岛成本。只要团队文化鼓励平等、开放、短反馈,结对的收益会远超其开销。
代码实现提交
→ 📖 Q4.5(P) 请提供你们完成代码实现的代码仓库链接。
浙公网安备 33010602011771号