[P] 结对项目:花见小路

项目 内容
这个作业属于哪个课程 2026年春季软件工程
这个作业的要求在哪里 [P] 结对项目:花见小路
我在这个课程的目标是 学习到软件工程中的各种知识并进实践,为实习与工作积累经验
这个作业在哪个具体方面帮助我实现目标 学习了解结对编程与敏捷开发的相关知识,积累协作经验

作业博客总结

Chapter.0 导论

Q0.0(P)

仓库中提交的代码包含 AIGC 的部分,使用模型为 Claude Sonnet-4.6GPT 5.4-Thinking,主要用途为:

  • T1,T2 中检查语法错误,代码风格优化
  • T3 中构造自动化测试脚本与基线策略

Q0.1(P)

开始时间:2026-03-27 15:00

Q0.2(I)

就我个人而言,在开始这次项目之前,我对 Wasm 的熟悉程度大致处于 II 到 III 之间,我了解 WebAssembly 是一种可以在 Web 场景中运行的二进制格式,常被用来承载性能更敏感的模块,同时也了解 AssemblyScript 的语法规则和 TypeScript 语法类似,但是多了形如 LLVMi32 这种变量类型的显式约束,但此前我并没有独立完成过一次从源码到 Wasm、再到 JS 胶水对接测试脚本的完整实践

在接触到本项目后,我才把 AssemblyScript 代码如何编译成 Wasm,以及 JS 如何通过 loader 调用 Wasm 导出函数,以及 Node.js 测试脚本如何与胶水代码对接的整个过程串起来,完成了一次统一且完整的开发实践

对《花见小路》这款桌游,我在项目开始前的熟悉程度是 I,我几乎没有接触过以二次元为背景的桌游,并不了解它的完整规则,也没有真正玩过,基于网上的简单教程以及助教团队开发的模拟器,我们才系统地阅读规则,理解七种礼物、四种行动以及三小轮内的胜负判定逻辑,后续 T2T3 的编码其实都建立在这一步对规则的准确理解之上,题目也明确要求先熟悉 Wasm 场景与花见小路规则,再进入后续三问的实现

Q0.3(P)

结束时间:2026-03-27 15:52


Chapter.1 七色之缨

Q1.1(P)

开始时间:2026-03-27 18:00

Q1.2(P)

在这一章节,我们首先阅读了题目对胜负判定的要求,因为胜负判定本质上对应程序的分支与嵌套结构,因此我们选择把规则抽象进行分拆,形成明确的步骤和顺序:

  • 先判断是否有人达到 11 分,再判断是否有人得到 4 枚倾心标记
  • 若仍未结束,则判断当前游戏的轮数,如果是前两轮则返回继续游戏
  • 如果已经是第三轮,则继续比较总分,总分高者胜出
  • 若总分相同,再按 G > F > D/E > A/B/C 的优先级做最终判定

这个顺序即为我们整个函数分支与嵌套逻辑的核心,在编码阶段我们做的只是把这个逻辑翻译成 AssemblyScript 语言

在具体实现上,为了让逻辑更清晰,同时也降低分支之间的耦合,我们没有把所有规则硬塞进一个大函数里,而是先定义了固定的分值表 SCORES,再分别写了 calcScore()countMarks()sideHasAny() 这几个辅助函数,最后在 hanamikoji_judge() 主函数中按规则顺序组织判断,这样做的好处是:每个辅助函数只负责一个小目标,主函数只负责调用判定大流程,符合高内聚低耦合的开发要求,让人读来更加清晰简洁,也更方便我们后续排查错误

完成核心函数后,我们又实现了对应的 JS 胶水代码,用 @assemblyscript/loader 同步加载 release.wasm,并把 JS 侧的 Int8Array 转成 Wasm 可用的数组指针,再调用导出的 hanamikoji_judge,由于课程组的测试脚本最终是在 Node.js 环境里跑的,真正交上去的不只是 AS 源码,还包括能够被测试脚本正确加载和调用的胶水层,因此完成一段正确的胶水代码,是对测试脚本和答案代码之间的正确衔接,其重要性也不可忽视

从分工上看,在这部分实现中,李昊宸同学主要负责规则抽象、AssemblyScript 函数实现和胶水代码编写;任天宇同学主要负责围绕接口编写和检查测试驱动,确认函数名、返回值范围、模块加载方式与课程组脚本兼容,在结对过程中,我们遵循以下的开发顺序:

  • 先一起读题,理解题目要求
  • 李昊宸同学实现主体代码编写,任天宇同学全程辅助
  • 任天宇同学做覆盖测试与接口检查,李昊宸同学全程辅助

这种方式既能发挥每位同学的专长,同时也为每一步的正确性添加了双重保障,因此相比于两人独立开发,或者每一个步骤都均等分工更加高效,正确性也更有保障

预估耗时:45 分钟左右
实际耗时:55 分钟

Q1.3(P)

在这个判定模块里,我们没有直接把所有条件写成一长串 if-else,而是设计了若干中间变量和辅助函数

  • 第一类是负责统计的辅助函数,比如calcScore(board, side) 用来统计某一方当前获得的总分,countMarks(board, side) 用来统计某一方当前获得的倾心标记数量,由于题目中的立即胜利条件分别对应 11 分和 4 枚标记,这两个函数几乎就是把题目里的自然语言规则直接翻译成了代码层面的可复用操作,实现比较简单

  • 第二类是优先级相关的辅助函数,第三轮总分相同的时候,需要按照 G > F > D/E > A/B/C 的优先级比较最高档位倾心标记,我们在实现中没有为每种情况都单独写大段重复判断,而是抽出了形如 sideHasAny(board, side, a, b) 这样的区间检查函数,用它来统一处理 D/EA/B/C 这两档,这样既减少了重复,也让代码结构更接近题面中的分层比较过程

Q1.4(I)

我觉得这种相对复杂的规则判定类模块最容易出问题的地方,不是某个公式写错,而是规则顺序错、边界漏掉、以及局部正确但整体不一致,在处理深层嵌套的 if-sles 分支结构时,为了避免漏判和错判,我主要做了三件事:

  • 第一,是先把题面的自然语言规则改写成结构层次清晰的伪代码,再据此开始写代码,比如题面中 “达到 11 分立即胜利” 的判定优先级在 “达到 4 枚标记胜利” 之前,但二者处于同一层分支的深度;前两轮和第三轮的分支也必须先分开,否则很容易把第三轮的最终判定提前套用到前两轮
  • 第二,在具体是线上,是尽量把规则拆成小函数,这样可以使得主函数框架更清晰,像总分统计、标记数量统计、档位检查,这些都比直接写在主流程里更不容易出错,因为主流程一旦只剩先判定 A,再判定 B 的框架,检查逻辑顺序就会变得简单许多
  • 第三,多想没有命中某分支时的边界情况,而不是只看正常情况,例如 “前两轮不分胜负应继续”、“第三轮同分比最高档位”、“第三轮仍无法区分则平局”,这些都属于很容易在第一次实现时漏掉的地方,要确保所有 if-else 分支之间交集为空,同时并集应当覆盖这个游戏的全部情况

Q1.5(P)

这一问的测试思路,我们是按题目给出的规则类别来拆的,而不是随便挑几个局面试一试。题目要求至少覆盖 11 分获胜、4 枚标记获胜、前两轮继续、第三轮比总分、第三轮同分比最高档位、第三轮平局这几类情形,因此测试设计本身就应该围绕这几类规则展开,做全面而充分的测试

从已经写进测试脚本的自动化部分来看,课程组给的提交测试会检查导出函数是否能被正确加载、返回值是否属于 {-1, 0, 1, 2},并直接用三组样例覆盖 “我方获胜”、“对手获胜” 和 “第三轮平局” 三种情况,我们在此基础上理解并补齐了其余规则类别,重点关注的是:

  • 得分 11 分与得到 4 枚标记这两种立即胜利条件不能混淆
  • 前两轮即使局面看起来已经很接近胜负,也不能误判为结束
  • 三是第三轮的判定必须先比总分,再比最高档位,最后才可能是平局,同一分支的判定也可能存在优先级

Q1.6(I)

对于 “先写测试再实现” 和 “先实现再补测试” 这两种方式,我在完成本小问后有了更加深刻的理解,前者虽然看起来有些反直觉,但在某些开发场景下确实会发挥更好的效果

如果题目规则已经很稳定、输入输出也很清楚,那么先写测试再实现确实会更有优势,因为测试可以提前把行为边界定死,后续实现基本是在朝着这些具体目标逼近,尤其对于这种规则判定题,测试可以起到一个整体框架约束和边界强调的作用,它的好处是不会让我们一开始就陷进代码细节,而是先确认各个情况的覆盖是否全面,确保了宏观设计思路上的正确性

但从这次项目的实际感受来说,我在 Wasm 这条链路上并没有完全做到纯粹的测试先行,原因也很简单:这里除了算法逻辑,还有 AssemblyScript 编译、Wasm 导出、JS 胶水实现、Node.js 测试脚本对接这些工程性问题,由于对相关知识存在一定的不熟悉性,很多时候我需要先把最小可运行版本搭起来,才能知道问题究竟出在规则、类型,还是接口上

因此,结合本次开发的实际经历与个人情况,面对这种技术栈不是很熟悉的情况,我个人更认可的做法是:规则层尽量先想测试,工程层允许先打通最小链路,再逐步补测试和修正实现

Q1.7(P)

结束时间:2026-03-27 18:55

Personal Software Process Stages 个人软件开发流程 预估耗时(分钟) 实际耗时(分钟)
PLANNING 计划 5 3
- Estimate - 估计这个任务需要多少时间 5 3
DEVELOPMENT 开发 45 34
- Analysis & Design Spec - 需求分析 & 生成设计规格(确定要实现什么) 5 3
- Technical Background - 了解技术背景(包括学习新技术) 3 4
- Coding Standard - 代码规范 2 2
- Design - 具体设计(确定怎么实现) 5 3
- Coding - 具体编码 15 12
- Code Review - 代码复审 3 2
- Test Design - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) 4 3
- Test Implement - 测试实现(设计/生成具体的测试用例、编码实现测试) 8 5
REPORTING 报告 10 6
- Quality Report - 质量报告(评估设计、实现、测试的有效性) 4 3
- Size Measurement - 计算工作量 2 1
- Postmortem & Process Improvement Plan - 事后总结和过程改进计划(总结过程中的问题和改进点) 4 2
TOTAL 合计 60 43

Q1.8(I)

这一问给我的最大收获,是把看起来很简单的规则题真正当作软件工程问题来做,而不是当作一道随手写掉的算法题(因为可能从算法或者程设的角度来看,这个问题真的很 Normal),真正写的时候才发现,题面再短,只要涉及优先级、边界和多轮状态,随便凭印象写都很危险,相反,把规则拆清楚、把辅助函数抽出来、让测试和接口都尽早跟上,整个实现就会流畅许多

另外,这也是我第一次比较完整地走完 AssemblyScriptWasmJS 胶水 → Node.js 测试这条链路,之前我对 Wasm 的理解更偏向理论和概念,这次才真正意识到,所谓能跑通并不只是代码逻辑正确,还包括导出函数名、参数形式、数组和字符串的传递方式都要匹配,这个体验对我后面做 T2T3 时提升编码速度,减少不必要的出错很有帮助

Chapter.2 不祥之影

Q2.1(P)

开始时间:2026-03-29 14:20

Q2.2(P)

本问比 T1 难得多,因为它不再是单纯做一个静态判定,而是要根据整轮 history 去还原当前双方场面和小轮结算后的标记状态,题目中给了行动记录的编码方式,也给了最终需要返回的三行数组含义,因此我们一开始先做的不是实现代码,而是把 1/2/3/4 四类行动逐条翻译成它们对场面的影响

  • 1 会向行动者区域放一张牌
  • 2 只弃牌不入场
  • 3 是对手拿走 choice,自己获得剩余
  • 4 是两组里对手选一组,另一组归自己

在实现层面,我们仍然沿用了固定的 7 维数组来表示 A-G 七类牌,这样可以和 T1board 表示保持一致。随后我们实现了 charToIndex()addLetters()removeLetters()sameMultiset()actorIsSelf()applyToken() 等一众辅助函数

整体思路是:先把 history 按空格拆成 token,再逐条根据行动类型更新 selfAreaoppArea,最后用 updateBoard 将当前双方区域内的牌数与轮前 board 对比,生成轮后标记状态,从代码中可以直接看到,applyToken() 方法专门负责分发四类行动,而 calc_current_state_raw() 则负责按顺序扫描整条历史并拼出长度为 21 的扁平结果

本问还有一个工程上的细节,即 Wasm 侧返回的是长度 21 的扁平数组,而题目语义上更像是 int[3][7] 的二维结构,为了兼容这两种情况,我们选择在 JS 胶水层把它导出为普通数组,再由测试脚本里的 normalizeMatrix3x7() 方法统一整理成 [[7],[7],[7]] 形式进行比对,这种 Wasm 中用更容易处理的平铺表示,JS 层负责适配题目接口的做法,我认为是一种很实用的工程思想,也符合这次项目 Wasm 负责核心逻辑,JS 负责胶水和对接的题目要求

在分工上,本问中李昊宸同学主要负责解析规则、状态表示和核心还原函数实现;任天宇同学主要从测试视角核对样例历史是否能被正确解释,并重点检查返回值格式、矩阵归一化以及提交脚本能否顺利加载模块,在自动化测试脚本中也能看到,对 calc_current_state() 方法的检查不仅看值对不对,也会检查返回结构是否合法,因此本问中代码实现和检查工作同样重要

预估耗时:150 分钟左右
实际耗时:155 分钟

Q2.3(P)

针对这一问,我们对 T1 的复用更多体现在问题的表示方式和拆分思路上,首先,在 T1 的实现中我们已经把 A-G 七种角色固定映射到长度为 7 的数组下标,这种表示方式在 T2 里依旧非常合适,所以我们继续沿用了固定顺序 + 定长数组的方式来存储牌数和标记状态,这样做的好处是状态表达统一,T1T2 之间不会出现同一类角色在不同模块里位置不同的问题

其次,T1 的完成让我们意识到,规则约束类题目一旦直接写成大段分支会很难检查,因此到了 T2,我们更主动地把逻辑拆成小函数:字符转下标、向计数器累加字母、从多重集合中减去被选牌、判断两组牌是否是同一多重集合、根据双方区域更新标记状态等,也就是说,T2 沿用了 T1 中高内聚低耦合的设计习惯,先抽象出稳定的辅助操作,再让主函数只保留流程控制

另一方面,T2 相比 T1 的最大变化,在于它从判定结果升级到了恢复过程,因此我们考虑把重点放在 history 的解析和场面还原上,这里我们需要确保尽可能客观、忠实地原样复现系统提供的一句对局记录,因此对各种情况考虑的全面性与严谨性有了更高的要求

Q2.4(I)

我认为,要让既有代码更能适应需求变化,最重要的不是写得多通用,因为越通用就越意味着越 Normal,一味追求通用性反而会导致我们的设计落入俗套,我们应该考虑的是把真正稳定的部分抽出来,把容易变化的部分隔离开,抓住每一部分功能的底层要求

以本问题为例,贯穿整个题目 / 游戏规则不变的是 A-G 的映射关系、计数器更新、字符串拆分、以及某类行动如何改变区域计数等基础操作;而时刻改变的,则是输入格式细节、返回形式、以及是否需要处理 X 这类不完全信息,如果一开始就把后者写死在一个大函数里,后面无论改输入还是改输出都会很痛苦

所以我们的做法是:把动作语义单独收敛到 applyToken() 方法,把数据表示统一为 7 维数组(调整顶层接口),把最终接口适配放在 JS 胶水层而不是 Wasm 核心逻辑里,这样一来,即使后续题目再改成别的输出形式,或者需要在不同先后手视角下复现一场对局,真正需要动的通常只是局部,而不需要把整个架构都推倒重来

Q2.5(P)

T2 中不带 X 的操作记录,相比真实对局,额外给出的最关键信息,即原本作为牌背面的信息现在被完全公开,通俗来讲,真实对局中对手通过 “密约” 放下的那张牌、通过 “取舍” 弃掉的两张牌,在行动当下我们是不知道具体身份的;但 T2 输入的是整轮结束后的完整记录,所有这些牌都已经被还原成了具体字母,所以我们实际上是在上帝视角审视这一场对局的历史,这意味着 T2 不是在做不完全信息推理,而是在做根据完整日志恢复场面,本质上也是一道大模拟题

如果把这些位置重新换回 X,问题就会立刻变成不完全信息建模,此时我们能确定的,只有公开展示过的牌、自己手里的牌、行动类型以及每种牌在总牌库中的数量上限;至于那些通过密约和取舍进入隐藏区或弃牌区的牌,只能落在若干个满足约束条件的可能集合里,也就是说,最后的小轮状态不再是唯一的,而会变成一个可能状态集合

如果继续往下做,我们找到了三种比较合理的处理方式:

  • 做一致性枚举:枚举所有与历史、总牌数、已知手牌、已公开信息相一致的隐藏牌分配
  • 做概率估计:不是只保留可行状态,而是给它们分配概率,算一个期望局面
  • 做保守决策:在所有可行状态里看最坏情况和最好情况,把策略写成稳健型而不是赌概率型

T2 因为输入不带 X,所以我们可以直接还原唯一状态;但 T3 真正在打比赛时,这部分不完全信息的处理就会变得非常重要,题目本身也明确指出了这一点

Q2.6(P)

结束时间:2026-03-29 16:55

Personal Software Process Stages 个人软件开发流程 预估耗时(分钟) 实际耗时(分钟)
PLANNING 计划 10 6
- Estimate - 估计这个任务需要多少时间 10 6
DEVELOPMENT 开发 140 125
- Analysis & Design Spec - 需求分析 & 生成设计规格(确定要实现什么) 15 12
- Technical Background - 了解技术背景(包括学习新技术) 10 13
- Coding Standard - 代码规范 10 8
- Design - 具体设计(确定怎么实现) 10 10
- Coding - 具体编码 50 40
- Code Review - 代码复审 10 7
- Test Design - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) 20 25
- Test Implement - 测试实现(设计/生成具体的测试用例、编码实现测试) 15 10
REPORTING 报告 30 24
- Quality Report - 质量报告(评估设计、实现、测试的有效性) 15 13
- Size Measurement - 计算工作量 5 3
- Postmortem & Process Improvement Plan - 事后总结和过程改进计划(总结过程中的问题和改进点) 10 8
TOTAL 合计 180 155

Q2.7(I)

在我个人看来,T2 是整个项目里第一个真正体现出系统感的部分,相比而言,T1 更像一个精确的小模块,而 T2 已经开始要求我们同时处理输入编码、状态表示、流程解析和接口返回,写完这一问之后,我明显感觉自己对这次项目的理解从实现几个功能独立的模块,变成了在逐步搭一个可以打完整对局的程序骨架

另外,我在这一问中也更加深地体会到:解析类问题最怕想当然,比如 34 这两种行动,如果不认真区分提供者和选择者的获得关系,很容易把牌加到对方的牌堆中;而一旦前面某一步做反,最后的 board 再怎么更新都不可能对,可谓之牵一发而动全身,所以 T2 的完成更加让我意识到,实现一个包含复杂规则的模拟题,最好不要仅靠脑内模拟,最好先简化自然语言的题目描述,然后把状态变化一步一步列出,先写出伪代码,在逻辑层面和队友进行模拟,确保建模无误以后,在把思路具体翻译成代码

毕竟在 AI 编码能力如此之强的时代,一份严谨、专业的设计,有时候比写出流畅、美观的代码更加重要,因为后者 AI 总是可以代劳,而前者却大概率依靠的是开发者的职业素养,开发习惯与聪明才智,才能为一份好的软件打下坚实的基础

Chapter.3 道途之荆

Q3.1(P)

第一段开始时间:2026-04-03 18:50;第二段开始时间:2026-04-04 10:02

Q3.2(P)

这一问的目标已经从判定和还原变成了真正的决策,题目要求程序根据 historycardsboard 输出一个合法行动字符串;而且既可能轮到我们先手出牌,也可能轮到我们回应对手的 34 ,做出选择,因此,要完成 T3 首先不是想一个策略,而是先保证程序能够正确表示:

  • 现在轮到谁:区分先手和后手,以及当前能否出牌
  • 当前还能用哪些行动、给出的行动是否合法:建模每个动作,并持续维护每个动作为对局带来的影响

具体来讲,我们延续了 T2 的很多基础工具函数,例如 splitHistory()actorIsSelf()addLetters()removeLetters()sameMultiset() 等,然后额外实现了 inferSeat()

这个新增函数的作用,是根据历史里哪些位置出现了 X、当前手牌长度是多少、以及这一步是不是一个选择响应,去推断自己在当前历史视角下到底是先手还是后手,因为 T3history 不再是固定以我方为第 0 条的完整结算日志,而是真实对局中的某一时刻记录,所以这个推断非常重要,在代码中可以看到,inferSeat() 同时利用了 X 的分布和手牌数量守恒关系来判断当前座次,这种判断方式是可以证明比较可靠的

先后手关系确定后,我们实现了 applyKnownToken()getKnownAreas() 方法,以便恢复当前可见信息下双方已经确定落在场上的牌数,和 T2 不同的是,这里我们遇到 1X 这类未知牌时不会强行猜测,而是先跳过,只保留确定信息。然后在此基础上构造 cardWeight() 这一套启发式评分:基础分会参考该牌本身的分值,之后再结合当前 board 上该角色是偏向自己、偏向对手还是中立,以及双方在该类牌上的场面差值和自己手里是否还有重复牌,给出一个综合权重,在代码中对应:若当前该角色还在对手手中,会给更高的紧迫性权重;若我方在该类牌上落后或持平,也会得到额外加分

在真正选择动作时,我们没有把四种行动混在一起眉毛胡子一把抓,而是分别为四种行动做显式建模,进行差异化候选生成与评分:

  • 1 密约:在单牌中找高权重牌
  • 2 取舍:更倾向于丢掉低价值组合
  • 3 赠予:在所有三张组合里做选择,找到即便对手拿走其中最好的一张,自己剩下两张仍然不太亏的方案
  • 4 竞争:枚举四张牌的不同两两分组,优先挑选“两组都不算差、而且差距不要太大”的分法,避免给对手一个过于明显的最佳选择

代码中实现的 evaluateSingle()evaluateDiscard()evaluateGift()evaluateCompetition() 以及 generateCompetitionPayloads() 等方法,即用于对四种行动建模的子函数

对于响应阶段,我们单独实现了 bestChoiceFor3()bestChoiceFor4(),也就是当对手已经给出三张或两组牌时,直接从这些候选里选当前权重更高的牌或牌组,与此同时,为了避免程序因为推断失误或极端输入而返回非法行动,我们还额外添加了actionValidForSeat() 做结果校验,并准备了 fallbackAction() 作为兜底分支:一旦主策略没产生合法行动,就退回到按尚未使用的行动顺序,拿当前可用的最小合法方案上,至少保证不会因为异常字符串而直接输掉当前这一局,对比赛型程序来说,这种兜底策略是非常有必要的,因为我们不能保证别人不会出错,需要为我们自己的程序添加一定的容错能力

最后,在工程对接上,T3JS 胶水依旧延续前两问的模式,用 @assemblyscript/loader 同步加载 Wasm,并把字符串和 board 传给 hanamikoji_action_raw,返回最终字符串,课程组提供的测试脚本会加载双方选手模块,运行一场完整对局,并输出赢家和双方决策耗时,因此本问除了策略本身,还必须关心接口名称正确、模块能被正常导入、以及决策过程不能异常退出

预估耗时:240~300 分钟
实际耗时:316 分钟

Q3.3(P)

如果课程组能够给出更充裕的时间和资源,我们认为该游戏里更接近最优策略的形式(之所以说接近,因为只是我们没有推演出最优策略,但不代表这个游戏不存在所谓的版本答案),不是现在这种单步启发式打分,而更可能是一种针对不完全信息博弈的混合策略求解

原因很简单:《花见小路》不是一个完全信息、单步最优就等于全局最优的游戏,真实对局里存在隐藏信息,存在对手选择,存在先后手转换,还存在本轮收益和为下一轮留后手之间的权衡,因此,一个真正强的策略往往不是固定动作模板,也不是做简单的贪心取局部最优解,而是围绕信息集工作的策略,通过在当前收益和未来可能收益之间做一个全局权衡:在自己当前能观察到的信息下,哪些动作在长期期望上更优,这可能更接近近端策略优化中的部分思想

如果往更严谨的博弈策略,我们设想了几种可能的路线:

  • 方向一:把策略建模成扩展式博弈,在信息集上使用类似 CFRMCCFR 的方法去逼近均衡策略
  • 方向二:使用基于信念状态的搜索,把未知牌分配表示成一个概率分布,再用带采样的搜索方法去估计每一步动作的长期价值
  • 方向三:做前中后期分层,前期用概率与信息保留策略,中后期状态空间收敛后,再切换到更精细的搜索甚至局面表

相比之下,首先于时间与工程规模,我们这次实现的策略更偏工程上可落地的启发式版本,重点在于保证合法、稳定、可解释,并在有限时间内尽量把局部价值做高

Q3.4(P)

我们目前已经完成的决策优化,本质上采用的是一种状态恢复 + 启发式评估 + 穷举候选 + 合法性兜底的思路

(1)首先要考虑的是每一步的行动类型怎么选,程序会先根据 history 推断当前身份和已使用行动,再分别枚举当前还能用的 1/2/3/4 四类动作,也就是说,我们不是先人为规定当前这步动作一定要选某个操作,而是对所有可用动作都生成候选,再用统一的评分方式比较优劣,最后选当前分数最高的那个,这样做的好处是动作类型的选择也被纳入了评估,而不是只在某个固定类型里选牌,决策具有更高的灵活性

(2)其次需要考虑选牌如何真正做到更优,我们的核心方法是 cardWeight(),它不是只看牌面分值,而是综合考虑四方面:

  • 牌本身对应角色的基础分值,G 和 F 天然更重
  • 当前 board 的归属,如果某个角色目前偏向对手,就会被赋予更高的争夺权重(当然这个决策有待商榷,我的队友如是补充:如果一个角色已经大概率无法回到自己阵营,我们也可以放弃对它的争取,转向去获得其余角色的好感)
  • 当前双方场面在该类牌上的差值,如果我方还没占优,这类牌的边际价值会更高
  • 自己手中是否还有重复牌,重复牌更容易形成后续合力,因此会有一定的额外加分,这样一来,程序选的就不是分值最高的牌,而是在当前局面里最值得投入的那张牌

在四种行动内部,我们又采用了不同的评价方式,密约偏向保留高价值单牌;取舍偏向丢低价值组合;赠予会避免把一个明显过强的三张组合直接交给对手选择,因此它更关注总收益减去对手最容易拿走的最好一张;竞争则强调把四张牌拆成两个尽量平衡的二元组,这样无论对手怎么选,我们都不至于只剩下一组很差的牌,代码层面的 evaluateDiscard()evaluateGift()evaluateCompetition() 等方法对应的就是我们这套差异化设计

最后,在编程实现上,我们专门加了一层 actionValidForSeat()fallbackAction() 做异常保护,前者负责检查当前输出是不是题目允许的合法行动,后者负责在主策略失效时给出一个仍然合法的保底动作,这部分工作严格来讲并不算优化策略,但我们认为它同样重要,原因前面也提到过,因为 T3 是比赛型任务,非法返回、超时或异常都会直接导致输局,所以一个具有一定错误处理能力、能够稳定不崩的程序,本身就是策略强度的一部分

Q3.5(P)

T3 中对 T2 的复用是非常直接的,而且这种复用不是表面上的抄函数,而是把 T2 已经搭好的状态理解能力继续向前推进

最明显的复用,是我们继续使用了固定的 A-G 下标映射、字符串拆分、多重集合比较、从候选中删除已选牌等基础工具函数,因为 T3 依然围绕 history 和牌组字符串工作,所以 T2 里这些面向行动记录解析的子函数,在 T3 里仍然非常有用

更重要的复用,是 T2 中得到的对历史对局做还原的策略,T3 虽然不能像 T2 那样直接拿到完整公开信息,但它依旧需要根据当前 history 来分析:哪些行动已经用过,双方哪些牌已经确定落在场上,当前轮到谁行动,以及这个行动到底是主动出牌还是响应选择,可以说,T2 帮我们解决了如何读懂一条对局信息的问题,而 T3 则是在这个基础上再继续解决读懂环境以后应该怎么出牌,这已经有一点 Agent 设计的味道了

从修改角度看,T3T2 最大的变化有两点:

  • 第一,T2 假设整轮结束后信息完整,而 T3 必须接受 X 带来的不完全信息,所以我们增加了 applyKnownToken()getKnownAreas(),只累积当前能确定的信息
  • 第二,T2 是静态还原,T3 是动态决策,因此在状态恢复之后,又叠加了动作枚举、启发式评分、合法性校验和兜底动作这些模块,也就是说,T3 是在 T2 骨架上继续往前搭,而不是从头另起炉灶

Q3.6(I)

T3 这类会持续演化的策略程序里,我觉得适应需求变化的关键,不是把所有情况一次性考虑完,而是把变化最频繁的部分做成可替换层,这样才能做到 “任尔东南西北风,我自岿然不动” 的设计境界

以本次实践为例,最应该和业务逻辑分开的,我认为其实应该是评分策略,因为题目一旦改性能要求、改牌权重,甚至只是我们自己想继续优化策略,最先想动的一定是 cardWeight() 和四类行动的 evaluate*() 函数,如果一开始就把这些权重、系数和动作选择逻辑散落在各个分支里,后面任何一次调参都会变成灾难

针对这一场景,我比较认同的设计方式是:底层保留稳定的状态工具层,中间单独抽出动作生成层和动作校验层,最上面再放可替换的评分策略层,这样后续如果想把启发式换成搜索,或者只想调某几个系数,都不会把底层解析和接口对接一起拖着改

Q3.7(P)

对于一个策略模块来说,有效性不能只靠主观感觉来判断,我们至少找到了以下几个可以用于量化的角度:

  • 最直观的一定是胜率,可以让程序和若干不同强度的基线策略反复对局,例如随机策略、固定模板策略、以及自己的旧版本策略,然后分别统计先手胜率、后手胜率和总胜率,只有当它稳定优于这些基线时,才能说明策略确实有提升
  • 其次是局面质量指标,即使一局最后没赢,也可以统计程序在每小轮结束时获得的平均分数差、平均倾心标记差、以及能否稳定争夺高分角色,这样可以判断程序在输掉对局中到底是直接摆烂,还是做了自己的思考,也形成了一定的局部优势,也有利于我们有针对性地做优化改进
  • 也可以考虑鲁棒性指标T3 是比赛任务,所以非法行动率、异常退出率、超时率都应当纳入评价,课程组测试脚本本身就会输出赢家和双方耗时,因此能否在时间限制内给出合法行动本身就是一个非常重要的衡量标准
  • 最后是局部决策质量,可以专门抽出一些典型局面,只看程序在 34 响应时是否能拿到更高价值的牌组,或者在某些关键回合是否倾向于争夺已经被对手占据的高分角色,这类测试比整局输赢更细,可以帮助我们知道策略到底强在哪里、弱在哪里

Q3.8(P)

第一段结束时间:2026-04-03 22:38;第二段结束时间:2026-04-04 11:30

Personal Software Process Stages 个人软件开发流程 预估耗时(分钟) 实际耗时(分钟)
PLANNING 计划 10 10
- Estimate - 估计这个任务需要多少时间 10 10
DEVELOPMENT 开发 310 277
- Analysis & Design Spec - 需求分析 & 生成设计规格(确定要实现什么) 20 16
- Technical Background - 了解技术背景(包括学习新技术) 15 17
- Coding Standard - 代码规范 15 10
- Design - 具体设计(确定怎么实现) 30 22
- Coding - 具体编码 160 149
- Code Review - 代码复审 20 21
- Test Design - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) 30 28
- Test Implement - 测试实现(设计/生成具体的测试用例、编码实现测试) 20 14
REPORTING 报告 40 29
- Quality Report - 质量报告(评估设计、实现、测试的有效性) 20 15
- Size Measurement - 计算工作量 5 4
- Postmortem & Process Improvement Plan - 事后总结和过程改进计划(总结过程中的问题和改进点) 15 10
TOTAL 合计 360 316

Q3.9(I)

T3 是我投入最多、也最能体现从规则实现走向策略实现的一个问题,真正开始写之前,我原本以为难点主要在于找到一个制胜的策略;写完之后才发现,更大的难点其实更加底层,我们需要时刻保证程序在真实对局流里知道自己是谁、现在轮到干什么、还有哪些动作能用、以及怎样保证返回值永远合法,很多策略问题最后不是输在博弈策略过于简单,而是压根没法解析出一个正确的对局状态,进而做出正确合理(甚至不要求是最优)的反馈,最终导致即便有很高级的策略也没有用武之地,成为空中楼阁

此外,T3 的完成也让我重新理解了可优化和可交付之间的区别,理论上我们当然可以继续做更强的搜索、更复杂的概率估计,但在课程项目的时间约束下,一个能稳定运行、能解释、能对接、能比赛的启发式策略,其实已经很有价值,至少对我来说,先把这条完整链路搭起来,比一开始就追求很玄的最优更重要,当然如果后续时间允许,我还是会持续探索更加有效、严谨的博弈策略,逐步提升我们所写程序的对局能力(甚至可以考虑收集优秀对局记录,训练一个真正用于对局的 Agent

结对项目总结

Q4.1(P)

结对影像资料:

结对影像资料

Q4.2(P)

回顾这次结对过程,我觉得比较有效的地方有以下两点:

  • 第一,我们没有简单地把任务机械拆开,或者说把每一个流程的工作量都严格均分的策略,而是先两位同学共同读题,然后根据两位同学擅长的方面对工作进行划分,比如在工作段 X 中,擅长的同学 A 做实践和主导,同时接受同学 B 的监督和建议;同学 B 做辅助同时学习 A 的实现以提升这方面能力,这种方式在 Wasm 项目里尤其重要,因为问题很可能同时出在规则理解、类型传递、胶水代码和测试脚本对接上,如果两个人各自孤立推进,或者机械的均分任务,最后合并时反而更容易出错,这也是课程平台反复告诫我们需要注意的一点

  • 第二,我们三问之间是有连续性的,并不是相互孤立,也避免了重复造轮子耽误时间的问题,T1 先把胜负判定做扎实,T2 再把历史解析和状态还原做出来,T3 才在前两问基础上真正做决策,这样虽然每一问都不算特别短,但整体上逻辑是递进的,不会出现前面全靠临时写、后面完全推倒重来的情况

  • 第三,对于宏观上任务的划分,我们把任务链路大致划分为编码阶段与测试阶段,擅长编码的同学主导功能代码的实现,另一位同学在旁辅助;擅长测试与运维的同学主导测试代码的实现,另一位同学在旁辅助,这样能够充分发挥结对同学的能力,实现 1+1>2 的突破和质变

当然,在首次结对作业中,我们也有可以改进的地方,一个是过程记录还可以更细,尤其是时间记录和 commit 信息,最好在每次关键改动后立即补上,而不是做完一段再回忆;另一个是策略调参部分应该更早开始做对局对比,这样就不会把很多优化都堆到最后;最后,针对我个人而言,有相当一部分测试代码我只是参与了伪代码的设计与思考,以及在另一位同学编码的时候在旁学习与指导,并没有自己亲手敲一遍,这部分代码需要在课后自己手打一遍,才算完整实现了整个项目

Q4.3(I)

就我本次结对作业的个人感受而言,我的结对队友有三个很突出的优点:

  • 测试意识强:很多时候我还在关注逻辑本身,他已经会提醒我去看“这个导出函数名是否和测试脚本一致”、“这个返回值结构在 JS 里是不是能被正常识别”、“异常情况下会不会直接崩掉”,这种思路对 Wasm 项目非常有帮助,因为真正交上去以后,评测首先面对的是接口而不是代码美感
  • 沟通直接有效:我们在讨论规则分支或者样例历史时,他能比较快指出哪些地方说得还不够精确,哪些地方需要回到题面再核对,而不是默认“差不多应该就是这样”,这种及时打断反而减少了后面返工
  • 做事稳妥细心:在看测试输出和对照题面时,他不会急着下结论,而是愿意把一条历史、一个返回值、一段规则顺序慢慢捋清楚,我觉得这对规则类项目是很重要的品质

如果一定要说一个缺点,我觉得我的队友可能缺乏一定的时间观念和主观能动性,很多时候需要我主导确定共同开发的时间,并进行任务的分配,在敏捷开发的景下,这有时可能会稍微降低迭代速度

Q4.4(I)

做完整个结对项目以后,我对结对编程的理解比以前更具体,它的优点不是简单的两个人一起写更快,而是两个人分别站在不同视角看同一个问题,真正发挥 1+1>2 的作用:一个人更容易陷进实现细节,另一个人更容易持续盯住规则、接口和测试,例如本次项目,代码实现、Wasm 对接、Node.js 测试、策略合法性都不能掉链子,单人做时很容易顾此失彼,而结对刚好能把这些角度补齐

当然结对编程也不是没有代价,它确实要求两个人在同一节奏上推进,需要频繁沟通,可能并不适应一部分人独立开发的工作习惯,如果两个人没有形成稳定的讨论方式,或者结对的两位同学在开发能力、工作积极性上相差较大,结对很可能变成一个人一直在写,另一个人只是旁观,最后既不高效,也谈不上真正的协作,因此选一名和自己优势互补的结对队友,对于结对开发至关重要

所以我现在更愿意把结对编程理解成一种高密度同步协作,它适合这次这种规则复杂、接口严格、又需要不断验证的任务;但前提是两个人都真的参与到问题分解和决策中,而不是形式上组队在一起

Q4.5(P)

代码仓库链接:https://github.com/agilawood4/_Hanamikoji_

posted @ 2026-04-11 08:44  l-h-c  阅读(14)  评论(0)    收藏  举报