• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • YouClaw
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
sunnyrain
博客园    首页    新随笔    联系   管理    订阅  订阅

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

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

Chapter.0 导论

Q0.0(P)

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

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

Q0.1(P)

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

Q0.2(I)

在开始项目之前,我对wasm和花见小路桌游的熟悉程度均为I,不过此前我曾参与过vue架构的前端制作,对HTML、JavaScript、CSS也都有了解,经过一段时间的了解和学习,我能够迅速掌握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/E 和 A/B/C 这两档,这样既减少了重复,也让代码结构更接近题面中的分层比较过程

Q1.4(I)

  • 首先从宏观上来说,我认为对于这种规则判定类问题,往往我们会在判定的顺序和边界特殊情况的处理上存在没有考虑到的情况,导致漏判或者错判,所以对于这类问题在开始代码实现之前,我们一定要考虑清楚各个判断条件的顺序,也就是if-else分支的嵌套关系,我们应该先写出结构清晰、覆盖情况完全的伪代码,如果一开始想不明白各个判断条件的顺序关系,我们也要用完整的判断条件表述每一个分支,如if(己方达到11分 && 轮次小于等于2)和if(己方达到11分 && 轮次等于3)等,最后我们再根据逻辑的先后来决定将哪个判断条件向外提
  • 其次对于这道题目来说,我们可以在编写之前自己为自己构造一些特殊情况,例如是否会出现两人同时到达11分(就本题来说这种情况不可能发生)、一人到达11分的同时另一人获得至少4枚倾心标记、第三小轮结束时总分相同且双方最高档位倾心标记数量相同但下一档位倾心标记数量不同等,尽量自己想一些可能会出现的边边角角的情况,帮助覆盖所有边界情况的同时也能让自己对整个游戏规则有更全面的把握

Q1.5(P)

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

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

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

Q1.6(I)

  • 先写测试再实现:这种方式对于判断类问题比较友好,其一我们在构造测试样例的时候能够不断思考判断的逻辑,最终达到充分掌握整个判断链的效果,有助于我们后面开始编写代码实现,其二除了常规的测试样例,我们会尽可能构造边界条件,这也会帮助我们在最后测试的时候不至于再发现许多没有考虑到的特殊情况,不会出现大规模修改代码的情况
  • 先实现再补测试:这种方式对于不能非常清晰的了解整个程序最终运行效果的情况比较有帮助,我们在搭建wasm框架时更希望先能完成一版能够运行的程序,即使它运行起来可能会出现各种各样的问题,后期我们再编写测试样例补充测试,修复其中的bug就显得比较容易了

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)

在第一部分的学习的实践中,我再次体会到了判断性问题的重要性以及难点,尤其是在工程性问题当中,我们不仅要编写出一份严谨可用的运行代码,还要时刻注意代码的可维护性,我们不能十分暴力的随意枚举所有可能出现的情况,一定要捋清楚所有的逻辑与顺序,确保后面迭代时不要再回头修复或完善前面的代码

并且通过这一部分的学习,我也体会到了wasm的工作流程,了解了各个文件之间的依赖关系,同时对其中涉及到的语言有了更充分的了解,我相信在这一次作业中的收获,一定能够对我未来的工作和学习提供非常大的帮助

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 七类牌,这样可以和 T1 的 board 表示保持一致。随后我们实现了 charToIndex()、addLetters()、removeLetters()、sameMultiset()、actorIsSelf()、applyToken() 等一众辅助函数

整体思路是:先把 history 按空格拆成 token,再逐条根据行动类型更新 selfArea 和 oppArea,最后用 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 里依旧非常合适,所以我们继续沿用了固定顺序 + 定长数组的方式来存储牌数和标记状态,这样做的好处是状态表达统一,T1 和 T2 之间不会出现同一类角色在不同模块里位置不同的问题

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

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

Q2.4(I)

在我看来,要想提高代码适应需求变更的能力,我们要做的是将功能进行细化拆分,让每一个模块仅负责其自己的功能,比如说输入字符串语义的映射、游戏结果的结算等,不能让一个模块去实现两个功能,比如接收输入和字符串拆分,这样如果后面输入的逻辑要发生改变,那么就去分别修改这两个模块的功能,而不是在某一个大函数中去做复杂的修改

也就是说将各个模块进行拆分相互独立之后,后面出现需求上的变更只需要修改那些发生变化的模块,而大部分要求不变的模块则不需要修改,这就很好的防止了修改过程中导致之前正常工作的模块出现异常的情况

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的程序,我们便可以知道双方的胜负

所以T1和T2两个模块其实也是相辅相成的,前者由游戏结果判定双方胜负,后者由游戏行为推导游戏结果,这种架构以及模块与模块之间的关系让我深切体会到了工程化编写软件的过程,我们一定是从各个模块、各个部分功能出发,编写好各自的实现,确保各部分功能能够正确运行,最后再来处理各个模块之间的接口等问题,以实现一份完整的软件的制作

架构与模块化设计往往才是一项优秀的软件设计中最关键最核心的部分,现如今,AI可以掌握各种各样高效的算法,也能够为我们实现各种各样的功能,但是却无法为整个项目设计出一份优秀的架构,同时也无法保证各个模块之间能够紧密联系完整运行,这也正是目前AI无法代替人类编码的重要原因之一

Chapter.3 道途之荆

Q3.1(P)

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

Q3.2(P)

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

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

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

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

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

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

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

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

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

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

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

Q3.3(P)

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

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

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

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

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

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 设计的味道了

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

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

Q3.6(I)

与T2同名问题类似,我认为提高既存代码适应需求变更的能力最重要的一点仍然是实现每个模块负责功能“最小化”,尤其我们要将“可能出现频繁变化或大幅度改动”部分与“基本不会改动或改动对整个游戏规则逻辑影响不大”的部分剥离开,保证不要出现模块之间相互依赖的情况,防止修改某一小部分功能时牵扯到另一部份功能的重写

在本部分任务中,我认为如果想要真正实现最优决策,最重要的部分是各项评分机制,如当前场面评分、目前剩余行动评分、当前手牌评分、对手剩余手牌评分等,并且可能要为各项评分赋予各自的权重,综合考虑下一步的决策是什么,所以经过这样的一系列分析,我们不难发现评分机制其实是一项庞大又复杂的模块,并且其改动的频率应当是最高的,所以在综合评分机制下面的各子评分机制,都应当单独进行计算,甚至于每张牌的权重都单独列出来,这样后面如果发生游戏规则的修改等问题,这样的设计能保证修改的幅度最小,修改的过程最顺利

Q3.7(P)

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

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

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)

完成了T2部分再来看T3时,我认为最难的部分是如何实现最优策略,所以开始时我思考的重点为评分机制或者枚举所有可能的情况找最优等等,然而实现过程中,我发现T3相较于T2的迭代幅度非常大,T2和T1本质上只是需要我们对一场对局的结果进行分析,输入的格式都是不变的,然而T3的情况一下子就多起来了,我们要分析当前对局进行到什么阶段了、轮到谁出牌了、还剩下哪些动作可以执行等等,决策结果的合法性反而成为了我们寻找最优策略之前一项非常困扰的工作

这部分作业也以小见大的反映了工程上软件开发的实际完成时会出现的问题,有时候迫于时间等因素,会无法完美的实现用户的某些需求,这种时候我们只能先确保整个软件系统的完整性与连通性,先向用户交付一份可用的版本,后续再根据用户的反馈等再进行迭代、修改与完善

结对项目总结

Q4.1(P)

结对影像资料:

image-20260410234313692

Q4.2(P)

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

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

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

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

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

Q4.3(I)

我认为我的队友在下面几个方面非常突出:

  • 编程能力强:AssemblyScript作为一项我们没有接触过的语言,他能够很快速的学习这门语言的语法结构,并能够按照需求快速的实现相应的功能,并且他对各种数据结构、算法等十分熟练,能够快速准确的分析出某项任务实现的流程
  • 严谨认真、思维敏捷:在完成每项任务时,他能够不急于开始编程实现,而是带我一起梳理各种可能出现的问题,会先在纸上或者利用画图工具来画一些简要的思维导图或流程图,并且也会先写一份较为完善的伪代码,这为后面的编程实现提供了非常大的帮助
  • 领导能力强:他对整个项目的实现流程会比较清晰,有些时候他会主动领导我完成某部分任务,能够为我详细的安排和说明这部分任务的需求以及实现方式,我在完成的时候基本不需要再进行过多的考虑

我的队友可能在测试意识上会比较弱,完成一部分工作后他更倾向于开启下一部分的工作,而较少会对已完成的工作进行一些测试,如果能够更正这一点我相信我的队友在之后的软件开发中会非常的强大

Q4.4(I)

通过本次结对编程任务的完成,我对结对编程的理解更加深刻

  • 结对编程的优点:我认为每个人对于编程的掌握程度不一样,每个人擅长的编程领域也不尽相同,有的人可能代码能力强擅长编码实现,有的人可能思维严谨擅长测试,也有的人对于架构分析比较敏感,擅长将一项比较大的功能拆分成若干个小功能分别实现,结对编程正是会将二人取长补短,将每个人的优势发挥出来同时极大削减了个人在某方面的不足对整体的影响
  • 结对编程的缺点:想要实现真正对二人“取其精华,去其糟粕”也是一项非常考验二人水平的事情,首先对两个人的沟通能力要求很高,两人都要能够明白对方的想法,并且要经常保持交流,其次能够真正找到一位能够弥补自身缺点的队友也是一项比较困难的事情
  • 对结对编程的理解:结对编程本质上其实就是擅长不同领域的双方各司其职,最终实现一份复杂困难的任务,在此基础上也可以衍生出多人组队编程,但始终要对每个人擅长的工作有充分的了解,以及互相的工作节奏要适应

Q4.5(P)

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

posted on 2026-04-11 09:49  晴雨QY  阅读(11)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3