[P]结对项目:花见小路
结对项目:博客问题总结
→ 📖 Q0.0(P) 【你可以在结对结束后补充】如果你的代码仓库包含 AIGC 的部分,列举使用的工具、模型和使用范围。若未使用则填写:本组提交的全部代码不包含AI补全或生成的部分。
本组代码仓库包含 AIGC 辅助生成的部分。使用的工具与模型主要有 Claude(Opus 4.6)和 GPT-5.4。使用范围包括:辅助代码编写、局部实现思路生成、测试用例设计与补充、代码调试与错误分析等。最终提交代码均由本组成员进行理解、筛选、修改、整合与验证。
Chapter.0 wasm从安装到入门
引入
→ 📖 Q0.1(P) 请记录下目前的时间。
2026.3.26 22:00
调查
→ 📖 Q0.2(I) 【你可以在结对结束后另行补充。】作为本项目的调查:
请如实标注在开始项目之前对 Wasm 的熟悉程度分级,可以的话请细化具体的情况。(分别回答两人各自的情况)
I. 没有听说过;
II. 仅限于听说过相关名词;
III. 听说过,且有一定了解;
IV. 听说过,且使用 Wasm 实际进行过开发(即便是玩具项目的开发)。
I. 没有听说过。
在开始本项目之前,对 Wasm 基本不了解,没有系统接触过相关的概念,也没有了解过其运行方式、应用场景或开发流程,更没有使用 Wasm 进行过实际开发。
请如实标注在开始项目之前对桌游花见小路的熟悉程度分级,可以的话请细化具体的情况。(分别回答两人各自的情况)
I. 不了解玩法和规则;
II. 听说过,且有一定了解;
I. 不了解玩法和规则。
在开始本项目之前,完全没有接触过桌游《花见小路》,更没有了解过其基本规则、胜负判定方式和具体玩法。
总结
→ 📖 Q0.3(P) 请记录下目前的时间。
2026.3.26 23:00
Chapter.1 七色之缨
结对过程
→ 📖 Q1.1(P) 请记录下目前的时间。
2026.3.26 23:00
- 任务预计耗时与实际耗时(PSP 表)
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| PLANNING | 计划 | ||
| - Estimate | - 估计这个任务需要多少时间 | 10 | 10 |
| DEVELOPMENT | 开发 | ||
| - Analysis & Design Spec | - 需求分析 & 生成设计规格(确定要实现什么) | 20 | 25 |
| - Technical Background | - 了解技术背景(包括学习新技术) | 25 | 30 |
| - Coding Standard | - 代码规范 | 10 | 10 |
| - Design | - 具体设计(确定怎么实现) | 20 | 25 |
| - Coding | - 具体编码 | 70 | 90 |
| - Code Review | - 代码复审 | 20 | 25 |
| - Test Design | - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) | 15 | 20 |
| - Test Implement | - 测试实现(设计/生成具体的测试用例、编码实现测试) | 20 | 30 |
| REPORTING | 报告 | ||
| - Quality Report | - 质量报告(评估设计、实现、测试的有效性) | 10 | 10 |
| - Size Measurement | - 计算工作量 | 5 | 5 |
| - Postmortem & Process Improvement Plan | - 事后总结和过程改进计划(总结过程中的问题和改进点) | 10 | 15 |
| TOTAL | 合计 | 235 | 295 |
- 完成编程任务期间的过程记录
完成任务时,我们先阅读了项目说明、README 和作业要求,了解需要实现的功能、输入输出格式以及提交规范,并对整体耗时做了简单估计。由于开始之前对 Wasm、AssemblyScript 和《花见小路》都不熟悉,所以先查阅了相关资料,了解基本概念、项目代码结构和游戏规则。
在分析需求后,我们讨论了策略实现的大致方向,重点考虑了如何在合法行动范围内做出较优选择,以及如何设计响应阶段的判定逻辑。随后结合现有代码框架,确定了先保证功能正确、再逐步优化策略效果的实现思路。
编码时,我们先完成基础逻辑,保证程序能够正常运行并输出合法结果,再逐步补充局面评估和策略判断。过程中遇到的主要问题有:对游戏规则理解不够熟悉、部分 AssemblyScript 写法和常见 JavaScript/TypeScript 不完全一样、初版策略效果一般。针对这些问题,我们重新查看规则说明,参考已有代码接口,修改不合适的实现方式,并通过多次测试不断调整策略。
测试方面,我们主要做了合法性测试、基础局面测试,以及与随机策略或简单策略的对战测试,检查程序是否稳定、决策是否合理。完成测试后,我们又对代码进行了简单复审,检查命名、结构和接口是否符合要求,并总结了本次任务中遇到的问题和可以改进的地方。
→ 📖 Q1.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
- 完成编程任务期间,依次做了什么(例如查阅了哪些资料、如何设计判定逻辑、如何设计测试样例、遇到了什么问题、如何解决)。
- PSP 表格
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| PLANNING | 计划 | ||
| - Estimate | - 估计这个任务需要多少时间 | 10 | 10 |
| DEVELOPMENT | 开发 | ||
| - Analysis & Design Spec | - 需求分析 & 生成设计规格(确定要实现什么) | 15 | 20 |
| - Technical Background | - 了解技术背景(包括学习新技术) | 20 | 25 |
| - Coding Standard | - 代码规范 | 5 | 5 |
| - Design | - 具体设计(确定怎么实现) | 15 | 20 |
| - Coding | - 具体编码 | 60 | 80 |
| - Code Review | - 代码复审 | 15 | 20 |
| - Test Design | - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) | 10 | 15 |
| - Test Implement | - 测试实现(设计/生成具体的测试用例、编码实现测试) | 15 | 20 |
| REPORTING | 报告 | ||
| - Quality Report | - 质量报告(评估设计、实现、测试的有效性) | 5 | 10 |
| - Size Measurement | - 计算工作量 | 5 | 5 |
| - Postmortem & Process Improvement Plan | - 事后总结和过程改进计划(总结过程中的问题和改进点) | 10 | 10 |
| TOTAL | 合计 | 185 | 240 |
- 完成编程任务期间的过程记录
在完成本次编程任务时,我们先阅读了项目说明、README、题目要求和提交规范,明确了需要完成的功能、输入输出形式以及测试要求,并先对整个任务的大致耗时进行了估计。
由于在项目开始之前我们对 Wasm、AssemblyScript 以及桌游《花见小路》都不熟悉,因此先查阅了相关资料,了解 Wasm 和 AssemblyScript 的基本概念、项目已有代码的组织方式,以及《花见小路》的基本规则、行动类型和胜负判定方法。通过这一阶段的了解,我们对后续实现的大致方向有了初步认识。
在需求分析和设计阶段,我们重点考虑了两个问题:一是程序怎样保证输出合法行动,二是在合法行动的基础上怎样让策略尽可能更合理。为此,我们先梳理了不同阶段需要处理的情况,包括主动出牌阶段和响应阶段,然后结合当前局面、牌的分布情况以及不同选择可能带来的收益,设计了一个较为基础的判定思路。整体上采用的是先枚举合法操作,再根据局面做简单比较和选择的方式。
在编码实现过程中,我们先完成基础功能,保证程序能正常运行并通过基本测试;然后再逐步补充和调整策略逻辑。过程中遇到的主要问题有:对游戏规则理解不够准确,某些行动的处理细节容易混淆;AssemblyScript 与常见的 JavaScript/TypeScript 写法存在差异,部分习惯写法不能直接使用;初版策略虽然能运行,但效果不够稳定。针对这些问题,我们重新阅读规则说明,对照已有接口检查实现细节,并通过多次运行测试来定位问题,再对对应逻辑进行修改。
在测试方面,我们设计并执行了几类测试:首先是基础合法性测试,检查程序是否会输出不符合规则的操作;其次是若干典型局面测试,观察不同局面下程序的决策是否合理;最后还进行了与随机策略、简单策略的对战测试,用来大致评估当前策略的实际表现。通过这些测试,我们发现了一些评分和选择上的问题,并继续进行了调整。
任务完成后,我们又对代码进行了简单复审,检查命名、结构、注释和接口是否符合要求,同时回顾了整个实现过程。总体来看,这次任务中花费时间较多的部分主要是前期的规则理解和后期的测试调整,而不仅仅是编码本身。通过这次任务,我们对 Wasm/AssemblyScript 的基本使用方式、策略类程序的设计过程,以及测试在改进程序中的作用都有了更直接的认识。
设计
→ 📖 Q1.3(P) 请说明你们为这个判定模块设计了哪些中间量或辅助函数;如果没有额外设计,也请说明为什么认为直接实现已经足够清晰。
为了让判定模块更清晰,我们没有把所有规则和策略判断都直接写在主流程里,而是设计了一些中间量和辅助函数来拆分逻辑。因为这个模块不仅要判断当前有哪些合法操作,还要在多个合法操作里选择相对更优的一种,如果全部直接展开在一个函数中,代码会比较乱,也不方便后续修改和调试。
在中间量方面,我们主要关注了当前局面的几个信息。比如双方在各个艺伎上的得分或控制情况、当前手牌中不同点数牌的分布、某个行动执行后局面可能发生的变化、以及当前可选行动的集合。这些中间量可以帮助程序先整理出“现在是什么局面”,再去判断“接下来怎么选更合适”。
在辅助函数方面,我们主要把逻辑分成了几类。第一类是合法性相关函数,用来枚举当前所有可执行的操作,或者检查某个操作是否符合规则。这样可以避免在主流程里反复写同样的规则判断。第二类是评估相关函数,用来对某个行动或某个响应方案进行打分,比较它们对当前局面的影响。第三类是局面分析相关函数,用来统计某类牌的重要性、估计某个艺伎是否值得继续争夺,或者模拟执行某一步后的局面变化。
这样设计之后,主判定流程就比较直接:先得到所有合法操作,再调用评估函数对这些操作进行比较,最后选出最合适的结果返回。这样的写法比完全直接实现更清晰,也更方便后续优化。如果之后发现策略效果不够好,通常只需要修改评估函数或某些中间量的计算方式,而不需要把整个判定模块重写。
→ 📖 Q1.4(I) 请说明在这样一个规则判定类模块中,如何避免“漏判”“错判”或分支顺序错误等问题。
我觉得在这个模块里,不要急着敲代码,而是先把规则翻译成逻辑表。因为这种判定类任务,最隐蔽的Bug往往不是语法错误,而是逻辑覆盖不全,最终结果就是代码能跑通,但判分结果却是错的。
合适的做法是先把所有可能的胜负情形穷举出来,区分清楚哪些是终止条件(如分数达到11分),哪些是继续条件。只有把完备的状态机理清楚了,写代码时才不会被复杂的嵌套 if-else 绕晕。
另外,逻辑分层也很重要。我们要把规则判定和策略选择彻底分开。规则判定是绝对的、客观的,比如是否满足立即胜利条件;而策略是主观的。如果把这两者混在一个大函数里,很容易因为判断顺序的微小失误,比如先判断了标记数量而忽略了分数优先权导致严重的错判。所以,先写死规则,再谈策略,是保证正确性的保证。
单元测试的边界覆盖是最后一道防线。我们不能只测正常赢的情况,必须构造极端数据:比如双方分数完全相同且都没有标记的全零状态,或者刚好卡在 10 分和 11 分边缘的数据。只有当这些边界值都能通过测试,才能更加确信我们的判定逻辑是严丝合缝的。
测试
→ 📖 Q1.5(P) 请说明你们设计了哪些测试用例,这些测试分别覆盖了哪一类规则或边界情况。
我们为 T1 的胜负判定模块设计了 13 个测试用例,主要覆盖了题目要求中的核心规则和若干边界情况。
-
按总分立即获胜:
- 测试“我方总分达到或超过 11 分时返回 1”;
- 测试“对手总分达到或超过 11 分时返回 -1”。 这一组用例覆盖了题目中的第一类立即胜利条件。
-
按倾心标记数量立即获胜:
- 测试“我方持有至少 4 枚倾心标记且总分未到 11 分时,仍然直接获胜”;
- 测试“对手持有至少 4 枚倾心标记时直接获胜”。 这一组覆盖了第二类立即胜利条件,也验证了“4 枚标记判胜”和“11 分判胜”是两条并列规则。
-
前两轮未分胜负时继续游戏:
- 分别测试在第 1 轮和第 2 轮结束后,若双方都没有达到立即胜利条件,则函数应返回
0。 这组用例验证了“前两轮不做最终比较,而是继续下一轮”的规则。
- 分别测试在第 1 轮和第 2 轮结束后,若双方都没有达到立即胜利条件,则函数应返回
-
第三轮按总分判定胜负:
- 测试“第三轮结束后我方总分更高时返回 1”;
- 测试“第三轮结束后对手总分更高时返回 -1”。 这一组覆盖了第三轮在未触发立即胜利条件时,优先按总分比较的规则。
-
第三轮总分相同,按最高档位倾心标记判定:
- 测试“双方总分相同,但我方拥有
G时获胜”; - 测试“双方总分相同,但对手拥有
F而我方只有更低档位标记时,对手获胜”; - 测试“双方总分相同,最高非空档位落在
D/E,且该档位只由我方拥有时,我方获胜”。 这组用例主要验证了G > F > D/E > A/B/C的优先级顺序是否正确。
- 测试“双方总分相同,但我方拥有
-
第三轮平局情况:
- 测试“双方总分相同,且最高非空档位也无法区分时返回
2”; - 测试“第三轮结束时所有标记都仍为中立时返回
2”。 这组用例覆盖了题目要求中的平局判定,以及‘没有任何有效倾向信息’这一边界情况。
- 测试“双方总分相同,且最高非空档位也无法区分时返回
→ 📖 Q1.6(I) 请说明你对“先写测试再实现”与“先实现再补测试”两种方式的理解。
“先写测试再实现”和“先实现再补测试”各有各自的好处
先写测试的好处是,我们在设计用例时了解清楚题目的各种设定和逻辑链条,使得能使判定的规则顺序更加清晰明了。但也可能存在写完测试后,固定了思维框架,实习代码时能够轻松通过测试,但一旦测试部分就有疏漏,就可能有更大的问题。
先实现的好处,主要是后写测试时,能够更加以审视的眼光去思考边界情况以及实现的合理性。
总结
→ 📖 Q1.7(P) 请记录下目前的时间,并根据实际情况填写 附录A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
目前记录时间为 2026 年 4 月 7 日 23:20(UTC+8)。在完成本部分任务后,我们根据实际开发过程,对附录 a 中 psp 表格的“实际耗时”栏目进行了补充和整理。填写时,我们结合了从阅读题目、理解规则、设计思路、编码实现、测试验证到总结整理的完整过程,对各阶段所花费的时间进行了回顾和统计。当前记录时间为 2026 年 4 月 7 日 23:20。
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| planning | 计划 | ||
| - estimate | - 估计这个任务需要多少时间 | 10 | 10 |
| development | 开发 | ||
| - analysis & design spec | - 需求分析 & 生成设计规格(确定要实现什么) | 15 | 20 |
| - technical background | - 了解技术背景(包括学习新技术) | 20 | 25 |
| - coding standard | - 代码规范 | 5 | 5 |
| - design | - 具体设计(确定怎么实现) | 15 | 20 |
| - coding | - 具体编码 | 60 | 80 |
| - code review | - 代码复审 | 15 | 20 |
| - test design | - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) | 10 | 15 |
| - test implement | - 测试实现(设计/生成具体的测试用例、编码实现测试) | 15 | 20 |
| reporting | 报告 | ||
| - quality report | - 质量报告(评估设计、实现、测试的有效性) | 5 | 10 |
| - size measurement | - 计算工作量 | 5 | 5 |
| - postmortem & process improvement plan | - 事后总结和过程改进计划(总结过程中的问题和改进点) | 10 | 10 |
| total | 合计 | 185 | 240 |
→ 📖 Q1.8(I) 请写下本部分的心得体会。
在这一部分,我们的感受是,规则类的程序编写看似实现难度不高,但是非常容易出错,需要考虑到一系列的判断优先级和逻辑链条,如果没有提前梳理清楚就贸然些代码的话,很容易就会漏掉一些情况漏掉一些分支判断出现错误。
清晰的规则定义是判定逻辑的基石。需要花费了大量时间研读并梳理游戏规则中关于胜利条件的描述。每一个条件都必须转化为精确、无歧义的程序逻辑。我深刻体会到,任何规则理解上的模糊或遗漏,都会在测试阶段引发难以预料的逻辑漏洞。因此,建立一份详尽的判定流程图,将复杂的胜利条件拆解为可量化的子条件,是确保开发方向正确的关键。
另外,测试作为最后防线也很很重要。很多时候这样的判断失误bug很难在实际运行中自查报错发现,需要结合合适的测试用例才能确定程序是否完备,我觉得先把规则整理出来,再配合测试一点点检查,比一开始只顾着写代码更有效。
Chapter.2 不祥之影
准备
→ 📖 Q2.1(P) 请记录下目前的时间。
2026.3.27 9:00
→ 📖 Q2.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
- 完成编程任务期间,依次做了什么(比如查阅了什么资料,随后如何进行了开发,遇到了什么问题,又通过什么方式解决);
- psp 表格
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| planning | 计划 | ||
| - estimate | - 估计这个任务需要多少时间 | 10 | 10 |
| development | 开发 | ||
| - analysis & design spec | - 需求分析 & 生成设计规格(确定要实现什么) | 20 | 25 |
| - technical background | - 了解技术背景(包括学习新技术) | 15 | 20 |
| - coding standard | - 代码规范 | 5 | 5 |
| - design | - 具体设计(确定怎么实现) | 20 | 25 |
| - coding | - 具体编码 | 75 | 95 |
| - code review | - 代码复审 | 15 | 20 |
| - test design | - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) | 15 | 20 |
| - test implement | - 测试实现(设计/生成具体的测试用例、编码实现测试) | 20 | 25 |
| reporting | 报告 | ||
| - quality report | - 质量报告(评估设计、实现、测试的有效性) | 5 | 10 |
| - size measurement | - 计算工作量 | 5 | 5 |
| - postmortem & process improvement plan | - 事后总结和过程改进计划(总结过程中的问题和改进点) | 10 | 10 |
| total | 合计 | 215 | 270 |
- 完成编程任务期间的过程记录
在完成这一部分任务时,我们先阅读了 chapter.2 的题目要求、项目说明和已有代码,先弄清这一部分相较于前一部分新增了什么要求、输入输出形式有没有变化、以及需要重点处理哪些规则和逻辑。看完要求后,我们先根据任务规模对整体耗时做了一个简单估计,并大致分成了需求分析、设计、编码、测试和总结几个阶段。
在前期准备阶段,我们主要查阅了两类内容。第一类是题目本身和项目已有实现,目的是弄清楚这一部分要解决的核心问题,以及原有代码哪些地方可以复用,哪些地方需要补充或修改。第二类是和 AssemblyScript、项目测试方式有关的内容,因为在实际开发中仍然需要注意它和常见 JavaScript、TypeScript 的一些差异,避免出现语法上能想到但实际上不方便直接使用的写法。
在分析任务后,我们先讨论了这一部分的实现思路,重点考虑了判定逻辑应该如何拆分、哪些情况需要单独处理、以及哪些部分适合抽成辅助函数来减少重复。整体上,我们还是采用先保证功能正确、再逐步优化细节的方式推进。设计时先整理出需要覆盖的主要分支和边界情况,再决定主流程中每一步应该先判断什么、后判断什么,以减少后面因为顺序不当带来的返工。
进入编码阶段后,我们先补全基础逻辑,保证程序在主要场景下能够正常运行,然后再继续调整细节。开发过程中遇到的主要问题,一是对部分规则的理解一开始不够细,导致最初的实现有些情况考虑得不完整;二是不同分支之间有一定关联,如果顺序处理不当,就可能出现结果虽然能输出,但实际不符合预期;三是在调试过程中发现,部分情况在普通测试下不容易暴露出来,必须专门构造对应场景才看得出问题。
针对这些问题,我们主要用了几种方式解决。首先是重新对照题目要求,把几个容易混淆的条件重新梳理了一遍,尽量把规则顺序先写清楚再改代码。其次是在代码层面把一部分判断拆分出来,减少主流程里过长的连续分支,方便检查每一步的作用。最后是在测试阶段补充了更多针对性的样例,专门检查一些临界情况、特殊情况和容易漏掉的分支,通过测试结果反过来定位实现里的问题。
在测试设计方面,我们除了检查程序能否正常运行外,还重点考虑了不同类型规则是否都被覆盖到,例如基础情况、特殊情况、边界情况以及几种容易混淆的相邻情况。测试实现时,我们通过运行已有测试、补充额外测试样例和观察输出结果等方式,检查程序是否符合预期,并根据结果继续修改实现细节。
任务完成后,我们又对代码进行了简单复审,检查整体结构是否清晰、命名是否统一、有没有多余或重复的判断,以及测试是否覆盖了主要分支。整体来看,这一部分花费时间最多的仍然是编码和测试调整,而前期分析也比最初预估略长一些,因为实际做下来发现,很多问题只有在边写边测的过程中才会逐渐暴露出来。通过这部分任务,我们对如何处理规则类模块、如何安排判断顺序以及如何用测试帮助发现问题都有了更直接的体会。
代码可复用性与需求变更
→ 📖 Q2.3(P) 请说明针对该任务,你们对 🧑💻 T1 中已实现的代码进行了哪些复用和修改。
在完成 T2 时,我们对 T1 的复用主要体现在 数据表示方式、规则拆解思路和整体代码组织方式*上,而不是直接复用 T1 的判定函数本身。
首先,在 数据表示 上,T1 中我们已经确定了用长度为 7 的数组按 A~G 顺序表示游戏信息,这一做法在 T2 中被直接沿用。T1 里 board 使用 1 / 0 / -1 表示倾心标记分别倾向我方、中立、对手;到了 T2,这种表示没有改变,因此我们在计算一小轮结束后的新标记状态时,可以继续沿用相同的状态定义。这样做的好处是:T1 和 T2 在接口层面能够自然衔接,T2 算出的结果可以直接作为后续胜负判断的输入。
其次,在 实现思路 上,T1 给我们的经验是:面对规则题,不要把所有逻辑都堆进一个大函数里,而是先把稳定的子问题拆出来。例如 T1 中我们拆出了“计算分值”“统计标记数”“第三轮 tie-break”等辅助逻辑;到了 T2,我们继续沿用这种方式,把问题拆成“解析行动记录”“更新双方区域牌数”“根据区域结算标记”几个步骤。虽然这些函数和 T1 的具体内容不同,但它们沿用了同一种模块化思路,这使得 T2 的主流程仍然保持比较清晰。
最后,在 修改与扩展 上,T2 相比 T1 的核心变化是:T1 只关心已经结算后的 board 是否分出胜负,而 T2 需要从整轮 history 中恢复当前局面。因此我们在 T1 的基础上新增了以下内容:
- 新增了对行动记录字符串的解析逻辑;
- 新增了双方区域牌数的统计数组;
- 新增了对四种行动(密约、取舍、赠予、竞争)的分别处理;
- 新增了根据双方场面牌数重新计算倾心标记状态的结算逻辑。
也就是说,T2 对 T1 的复用更多体现在“底层表示一致”和“拆分规则、分步实现”的设计思想上,而修改和扩展则主要集中在对完整一轮过程的恢复与结算上。
→ 📖 Q2.4(I) 请说明在编码实现时,可以采取哪些设计思想、考虑哪些设计冗余,来提高既存代码适应需求变更的能力。
编码实现时,为提高既存代码适应需求变更的能力,应秉持“拥抱变化”的设计思想。核心在于遵循开闭原则,对扩展开放,对修改关闭,将易变部分抽象化,通过接口或抽象类隔离变化。同时,采用依赖倒置,使高层模块依赖抽象而非具体实现,降低模块间耦合。结合策略、工厂等设计模式,可动态替换行为,实现灵活扩展。
设计冗余并非资源浪费,而是为未来留出的弹性空间。可适度预留扩展点,如在枚举、配置文件中预定义未启用的选项;接口设计可具备一定前瞻性,支持未来可能的功能参数。但需避免过度设计,平衡当前需求与扩展成本。通过抽象封装变化点,结合适度的冗余机制,使系统在需求变更时仅需增量开发,而非重构重写,显著提升代码的可维护性与生命周期。
头脑风暴环节
→ 📖 Q2.5(P) 头脑风暴环节:
我们终于快要开始让程序玩游戏了!请尝试分析:T2 中不带 X 的操作记录比起实际对局多出了多少信息?如果加上 X,也就是失去了这部分信息的话,如何处理对小轮结束后状态的估计?
T2 中不带 X 的操作记录,相比实际对局,最大的区别在于:它把原本属于隐藏信息的内容也完整公开了。因此,T2 并不是真正意义上的“从不完全信息中推断状态”,而更接近一个完整信息条件下的状态恢复问题。
具体来说,T2 比实际对局多出的信息主要有以下几类:
-
密约牌的信息被公开了
在真实对局中,行动
1(密约)打出的牌在本轮结算前对对手是不可见的,只能记作类似1X。而在 T2 中,输入的是完整一轮结束后的记录,因此这张牌已经被明确写出,例如1D、1C。这意味着我们不需要猜测暗置牌是什么。 -
取舍牌的信息也被公开了
在真实对局中,行动
2(取舍)弃掉的两张牌对对手同样不可见,只能记作2XX。但在 T2 中,这两张牌会被完整写出,例如2GG、2FF。因此我们不仅知道对方弃过牌,还知道具体弃掉了什么牌。 -
整轮结束后的信息是“确定的”
由于密约和取舍的内容都不再隐藏,配合赠予和竞争本身就是公开行动,T2 的
history实际上已经包含了恢复场面的全部必要信息。因此在 T2 中,小轮结束后的双方区域牌数以及新的倾心标记状态,理论上都可以唯一确定。
也就是说,T2 中不带 X 的记录,相当于把真实对局中的部分隐藏信息全部揭示出来了。从信息论角度看,它减少了不确定性,使得状态恢复从“估计问题”退化成了“计算问题”。
如果重新加入 X,也就是回到更贴近真实对局的输入形式,那么问题就会发生本质变化:我们不再能唯一确定小轮结束后的状态,而只能做估计。在这种情况下,可以考虑以下几种处理思路:
- 维护“可能状态集合”
已知的信息包括:
- 每种牌在整副牌中的总数;
- 我方手牌;
- 所有公开行动中出现过的牌;
- 对方做过哪些类型的行动;
- 哪些牌已经确定进入双方区域、弃牌区或被选择过。
根据这些约束,可以枚举或构造所有与当前观测一致的可能状态。例如,对手打出 1X,就表示“这张牌来自其手牌,但具体是哪一种未知”;2XX 则表示有两张未知牌被移出本轮。这样就可以得到一组可能的隐藏牌分布与小轮末状态。
- 从“唯一结果”改为“概率分布”
如果可能状态过多,可以不再枚举所有局面,而是为每种未知牌建立一个概率估计。例如:
- 某张
X是高价值牌G的概率有多大; - 某个艺伎对应的隐藏牌最终更可能落在哪一方区域;
- 小轮结束时某个倾心标记更可能偏向谁。
这样,最终的状态就不是一个确定数组,而更像是“每一项结果的概率分布”或“期望局面”。
- 在决策时考虑期望收益或最坏情况
如果程序后续要基于这个估计结果继续做决策,就不能只假设某一个最理想状态成立,而应当:
- 计算在若干可能状态下动作的期望收益;
- 或者采用更保守的最坏情况分析;
- 也可以在两者之间折中,比如既看平均表现,也避免特别差的极端结果。
这实际上就把问题从“规则恢复”推进到了“不完全信息博弈”层面。
- 使用采样或模拟降低复杂度
如果严格枚举所有可能状态的代价太高,可以使用近似方法,例如:
- 按约束随机生成若干组合法隐藏状态;
- 在这些样本状态上模拟后续结算;
- 统计小轮结束后各种结果出现的频率,作为对真实状态的近似估计。
这种方法虽然不能保证精确,但在 T3 这类需要快速决策的场景里会更实用。
综合来看,我们的理解是:
- T2 不带
X时,问题是一个完整信息下的状态恢复问题,结果可以唯一确定; - 如果加上
X,问题就会变成一个不完全信息下的状态估计问题,此时更适合维护可能状态集合、概率分布,或者通过采样/模拟来近似评估小轮结束后的局面。
也正因为如此,T2 其实为 T3 做了一个很重要的铺垫:前者是在完整信息下训练我们“如何恢复状态”,后者则进一步要求我们思考“在信息不完整时怎样基于估计来做决策”。
总结
→ 📖 Q2.6(P) 请记录下目前的时间,并根据实际情况填写 附录 A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
2026.3.27 9:40
→ 📖 Q2.7(I) 请写下本部分的心得体会。
相较于T1中对静态局面的快照式判定,T2的挑战在于将规则还原为一个动态的、可复现的过程。当胜负不再是一次性的判断,而是需要从一轮轮历史记录中重建双方场面时,问题的复杂度便从“是什么”变成了“如何发生”。这要求我们不仅要理解规则条文,更要吃透每一次行动,比如竞争密约取舍背后引发的状态迁移,谁在何时拿走了哪张牌,剩余的牌归属如何,哪些牌被排除在场面之外。很多时候,真正的难点不在于代码语法,而在于如何将模糊的题意精准地翻译为清晰的状态流转逻辑。中间表示的设计是否干净、语义是否明确,直接决定了后续实现能否经受住复杂交互的考验。
这一阶段也让我深刻体会到先推演,再编码的重要性。许多规则看似简单,但在具体样例的手工推演中,总能发现未曾留意的细节与边界情况。通过先手算、再实现、最后用测试验证的节奏,我避免了大量因理解偏差导致的返工。这种从具体实例中提炼抽象模型、再用代码还原现实逻辑的过程,让我对程序的确定性与可追溯性有了更切身的体会。
Chapter.3 道途之荆
准备
→ 📖 Q3.1(P) 请记录下目前的时间。
2026.3.27 9:40
→ 📖 Q3.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
- 完成编程任务期间,依次做了什么(比如查阅了什么资料,随后如何进行了开发,遇到了什么问题,又通过什么方式解决);
- psp 表格
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| planning | 计划 | ||
| - estimate | - 估计这个任务需要多少时间 | 10 | 10 |
| development | 开发 | ||
| - analysis & design spec | - 需求分析 & 生成设计规格(确定要实现什么) | 25 | 30 |
| - technical background | - 了解技术背景(包括学习新技术) | 15 | 20 |
| - coding standard | - 代码规范 | 5 | 5 |
| - design | - 具体设计(确定怎么实现) | 25 | 30 |
| - coding | - 具体编码 | 90 | 115 |
| - code review | - 代码复审 | 20 | 25 |
| - test design | - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) | 20 | 25 |
| - test implement | - 测试实现(设计/生成具体的测试用例、编码实现测试) | 25 | 30 |
| reporting | 报告 | ||
| - quality report | - 质量报告(评估设计、实现、测试的有效性) | 5 | 10 |
| - size measurement | - 计算工作量 | 5 | 5 |
| - postmortem & process improvement plan | - 事后总结和过程改进计划(总结过程中的问题和改进点) | 10 | 10 |
| total | 合计 | 255 | 315 |
- 完成编程任务期间的过程记录
在完成这一部分任务时,我们先阅读了 chapter.3 的题目要求、项目说明以及已有代码,先确认这一部分相较前面任务增加了哪些内容、需要实现什么功能、以及原有逻辑哪些还能继续复用。看完要求后,我们先按照任务规模对整体耗时做了一个估计,并把过程大致分成需求分析、设计、编码、测试和总结几个阶段。
在前期准备中,我们主要查阅了题目说明、已有实现和部分相关资料。一方面是为了进一步熟悉《花见小路》的规则和这一部分涉及到的具体要求,另一方面也是为了更清楚地理解项目代码结构、接口限制以及 AssemblyScript 环境下的一些实现细节。因为这一部分比前面的任务更复杂一些,所以在真正开始写代码前,我们先花时间把输入输出、主要分支和可能出现的问题大致梳理了一遍。
在需求分析和设计阶段,我们重点考虑的是怎样在原有基础上扩展实现,既保证功能正确,又尽量保持结构清晰。我们先把这一部分涉及的主要情况和规则分开整理,再讨论哪些逻辑适合放在主流程里,哪些逻辑适合抽成辅助函数。设计时尽量保持判断顺序清楚,先处理规则和合法性,再处理具体策略和选择问题,避免把不同层次的判断混在一起,导致后面难以调试。
进入编码阶段后,我们先实现核心功能,保证主要流程能跑通,再逐步补充细节和优化策略。开发过程中遇到的主要问题有几个。第一,对部分规则和分支关系一开始理解得不够完整,导致初版实现虽然能运行,但覆盖情况不够全面。第二,这一部分逻辑比前面更复杂,一些判断之间会互相影响,如果顺序安排不好,就容易出现结果表面正常、实际上处理不对的情况。第三,在 AssemblyScript 中实现某些逻辑时,不能完全照搬平时 JavaScript 或 TypeScript 的写法,需要根据现有框架和语言限制调整实现方式。
针对这些问题,我们主要通过三种方式解决。首先是重新对照题目说明,把容易混淆的规则和分支顺序重新整理出来,再检查代码中是否一一对应。其次是在代码结构上尽量拆分功能,把合法性判断、局面分析、评分比较等部分适当分开,减少主流程中过长的连续判断。最后是在测试阶段不断补充针对性的样例,通过测试结果来反推哪些逻辑仍然存在问题,再进行修改。
在测试设计方面,我们除了做基础功能测试之外,还专门考虑了多种典型局面、边界情况和容易混淆的相邻情况,尽量覆盖这一部分的主要规则。测试实现时,我们通过运行已有测试、补充额外测试样例以及多次观察输出结果,来检查程序是否在不同场景下都能给出合理结果。测试过程中也发现了一些初版实现中不容易直接看出来的问题,比如某些分支处理不够完整、个别特殊情况没有覆盖到等,这些问题后来都通过补充测试和调整逻辑逐步解决。
任务完成后,我们又对代码进行了复审,检查整体结构是否清晰、函数划分是否合理、有没有重复判断或不必要的复杂写法,同时回顾了各阶段实际花费的时间。整体来看,这一部分耗时最多的仍然是编码和测试,其次是前期的分析和设计,因为任务本身更复杂,很多问题只有在实际实现和测试时才会逐渐暴露出来。通过这一部分的开发,我们对如何在已有框架上继续扩展规则逻辑、如何安排更清晰的判断顺序,以及如何通过测试不断修正实现细节,都有了更深一些的体会。
头脑风暴环节
→ 📖 Q3.3(P) 头脑风暴环节:
假设提供更充裕的时间和资源,这个游戏中你能找到的最优策略有可能是什么形式的?进行调研并总结分析。你还可以在任务结束后试着实现(不计分)。
如果有更充裕的时间和资源,我认为这个游戏中更接近“最优策略”的形式,应该不是若干人工编写的局部规则,而是一种建立在不完全信息博弈求解基础上的策略系统。因为《花见小路》并不是一个单纯的确定性搜索问题,它同时包含隐藏信息、行动顺序、对手响应、阶段性资源分配和终局阈值判定等因素,因此真正高质量的策略应当能够同时处理“我当前看到的信息”“我对对手隐藏信息的估计”和“对手也会反过来推测我”的问题。
从博弈论角度看,这个游戏更适合被建模为一个双人、零和、非完全信息、扩展式博弈。若要追求理论上更强的策略,比较可能的形式有以下几种。
第一种可能是基于纳什均衡近似的不完全信息博弈求解方法,例如 counterfactual regret minimization(cfr)及其变体。它的基本思想是:通过大量自我博弈,不断调整在不同信息集上的行动概率,最终逼近一个不容易被针对性利用的均衡策略。对于《花见小路》这种双方对抗、隐藏信息明确、行动分支有限但博弈深度较高的游戏,这类方法在理论上是比较契合的。它的优势在于求得的不是“某个固定对手下的最佳应对”,而是更稳健、更接近均衡的混合策略;缺点则是状态空间和信息集会比较大,实现复杂度较高,训练时间也可能比较长。
第二种可能是基于信念状态的搜索策略,也就是先根据已知历史记录推断对手可能持有哪些牌,再在这些可能状态上做期望意义下的搜索。例如可以维护一个“对手隐藏手牌分布”的估计,然后在每个可能状态上向后模拟,比较不同动作的平均收益。这类方法的核心不再是对单一确定局面做搜索,而是对一组可能局面做带权搜索,因此它比普通 minimax 更符合本游戏的性质。它的优点是解释性较强,也容易结合现有启发式;缺点是如果隐藏状态太多,计算量会迅速膨胀。
第三种可能是 information set monte carlo tree search(ismcts)或类似的采样搜索方法。具体做法是:先依据当前已知信息随机生成若干“可能真实局面”,然后在这些局面上反复模拟对局,从而估计某个行动的价值。这种方法在很多不完全信息游戏里都比较常见,因为它不要求精确枚举所有隐藏状态,而是通过采样近似获得较优决策。对于《花见小路》这种每回合选择有限、但隐藏信息又会持续影响判断的游戏,它可能是一条比较现实的提升路径。相比完全的博弈求解,它更容易实现;相比纯手写规则,它又能更系统地利用隐藏信息。
第四种可能是“残局精确求解 + 中前期启发式”的混合策略。因为本游戏在后期会出现状态空间明显缩小的阶段,例如某些行动已经用完、手牌数减少、双方倾向标记逐渐稳定,此时可以对残局做更精确的搜索,甚至尝试穷举;而在前中期,则用较快的启发式评分函数来控制计算成本。这种分阶段方法在实际工程中往往比较可行,因为它兼顾了性能和决策质量。
如果进一步调研这类策略的共同点,可以发现它们本质上都离不开三个关键能力。
第一,是对隐藏信息的建模能力。最优策略不只是看“当前牌面上发生了什么”,还要看“对手手里可能还有什么”“哪些牌已经不可能出现”“某种行动反映了什么意图”。也就是说,程序需要维护一种对未知信息的概率判断,而不是只依据公开信息机械行动。
第二,是对对手策略的适应能力。真正强的策略不应该只会执行固定规则,而应当能在一定程度上预判对手在赠予、竞争、密约、取舍时的偏好,并据此调整自己的选择。换句话说,程序既要会“评价当前局面”,也要会“评价对手对局面的评价”。
第三,是在胜利条件层面进行全局权衡的能力。《花见小路》的胜利既可能来自 11 分,也可能来自 4 枚倾心标记,因此某些局面下追求高分艺伎更优,另一些局面下尽快凑够标记数量更优。最优策略不应只盯着局部最大收益,而要结合当前轮次、双方资源和终局条件,动态调整目标。
如果从实现可行性来排序,我认为最现实的路线可能是:先在现有启发式策略的基础上,引入“隐藏信息采样 + 模拟评估”,逐步向 ismcts 一类方法靠近;如果后续还有更多时间,再尝试把问题建模为标准的不完全信息扩展式博弈,使用 cfr 或其变体去逼近更稳定的策略。前者工程上更容易落地,后者理论上更接近“最优策略”的形式。
综合来看,我认为这个游戏中最有可能接近最优的策略形式,不会是单纯的固定规则表,而应该是“基于信息集、结合隐藏信息推断、并通过自我博弈或采样搜索不断优化”的博弈型策略系统。换句话说,真正强的程序应该同时具备状态估计、对手建模、行动评估和长期收益权衡四种能力。我们目前实现的策略更像是一个能够工作的启发式原型,而如果未来继续深入,上述方向会更值得尝试。
需求建模和算法设计
→ 📖 Q3.4(P) 请说明针对该任务,你们采取了哪些策略来优化决策。具体而言,怎么选择行动类型?选牌如何更优?如何编程实现。
针对 t3 这一任务,我们采取的总体思路不是写死某一种固定套路,而是构建一个“先评估局面,再枚举合法动作,最后从中选择当前最优动作”的启发式决策框架。因为《花见小路》本质上是一个信息不完全、并且对手会实时响应的双人博弈,如果简单用固定规则,例如“高分牌优先密约”或“低分牌优先取舍”,很容易在具体局面下失效。所以我们的优化重点,放在了三个方面:如何衡量当前局面中每位艺伎的重要性,如何在不同动作类型之间做选择,以及如何在给定动作类型后把选牌做得更优。
首先,在“怎么选择行动类型”上,我们没有人为预设某一类动作一定更优,而是采用了“合法动作全枚举 + 统一评分”的方式。程序会先根据当前 history 判断本轮中哪些行动已经用过,再结合当前手牌枚举所有仍然合法的操作,包括密约、取舍、赠予和竞争。这样做的好处是,程序不会被固定模板束缚,而是能在具体局面下动态判断:此时是应该保留关键牌做密约,还是应当主动弃掉低价值牌,或是利用赠予、竞争去制造对自己更有利的分配结果。
其次,在“如何判断某个动作值不值得选”上,我们为当前局面设计了一套启发式评分机制。程序会先从 history 中提取当前轮公开可见的投入情况,恢复双方在七位艺伎上的已投入牌数;再结合当前 board 中的倾心标记归属,以及我方手牌情况,为每位艺伎计算一个重要度。这个重要度综合考虑了以下几个因素:
- 该艺伎本身的分值高低;
- 当前倾心标记属于我方、对手还是中立;
- 双方当前在该艺伎上的牌数差距;
- 这一位置还剩下多少牌可能参与争夺;
- 我方是否已经接近锁定该艺伎,或者是否已经很难翻盘;
- 该艺伎对整体胜负阈值的贡献,比如是否更有助于凑够 11 分或 4 枚标记。
基于这些因素,程序会得到一个比较动态的“局面价值分布”,而不是单纯地把高分牌永远看得最重要。这样做的目的是让决策更符合具体局势,例如某张低分牌如果正好关系到第 4 枚倾心标记,它在当前回合中的意义可能比一张高分牌更大。
第三,在“选牌如何更优”上,我们对不同类型的动作采用了不同的处理方式。
对于密约,也就是行动 1,程序会把这张牌视作我方稳定投入的一部分。由于密约牌不会立刻暴露,因此它通常适合用于保护重要艺伎、巩固已有优势,或者在对手不易精确判断的地方悄悄补强。程序在评分时会模拟打出这张牌后的局面变化,并观察它对整体评价函数的提升。
对于取舍,也就是行动 2,程序会把它看作一种“资源止损”手段。并不是所有牌都值得留下,如果某些牌所在的艺伎已经基本锁定、或者基本无法争夺,那么继续保留这些牌的边际收益就比较低。在这种情况下,程序倾向于丢掉对当前胜负帮助较小的牌,从而把注意力集中到更关键的争夺点上。在实现上,我们对被丢弃的牌设置了动态惩罚:如果某张牌所在位置仍然胶着,惩罚就更大;如果该位置已基本锁定胜负,丢掉它的代价就更小。
对于赠予,也就是行动 3,程序采用的是“最坏情况评估”思路。因为这类动作的最终结果取决于对手会从三张牌中拿走哪一张,所以不能只看自己理想中的分配情况,而必须假设对手会做出最不利于我的选择。具体做法是:程序枚举对手可能选走的每一张牌,分别模拟对应局面,再取其中最差的结果作为该赠予动作的评分。这样可以避免程序设计出一些看起来收益高、但实际上对手一选就崩掉的方案。
对于竞争,也就是行动 4,思路与赠予类似,只不过对象变成了“两组牌”。程序会枚举可能的分组方式,再分别模拟对手选择第一组或第二组后的结果,最后取较差的那个结果作为当前分组的评分。也就是说,我们在主动设计竞争时,追求的不是“存在一种选法对我有利”,而是“无论对手怎么选,我都不至于太亏”。这其实是一种比较典型的稳健优化思想。
第四,在“如何响应对手动作”上,我们没有另外写一套完全独立的规则,而是尽量复用同一套局面评估思想。当程序面对对手的赠予或竞争时,它会分别模拟不同选择后自己的收益变化,然后选择评分更高的那一种。这样做的优点是,主动出牌和被动响应使用的是同一套价值标准,整体决策逻辑更统一,也更容易维护。
从编程实现上看,我们大致把策略拆成了几个层次:
- 手牌统计与历史拆分,用于识别当前局面;
- 已使用行动的跟踪,用于限制本轮合法动作集合;
- 当前轮公开牌数恢复,用于建立局面基础状态;
- 艺伎重要度计算,用于反映局部争夺价值;
- 局面评分函数,用于比较不同动作后的收益;
- 合法动作枚举与逐个打分,用于在所有候选方案中选出当前最优动作;
- 对赠予和竞争的响应策略,用于在对手给出选项时做出较优选择。
从结果上看,这样的策略虽然还远远称不上理论最优,但它至少具备了几个比较重要的性质:
- 它不是静态死规则,而是会根据当前局面变化动态决策;
- 它在处理带有对手选择权的动作时,能够考虑最坏情况,而不是只看理想结果;
- 它能够把局部牌面优势、倾心标记状态和整体胜利条件结合起来考虑,而不只是盯着某一位高分艺伎。
因此,我们对 t3 的策略优化可以概括为一句话:以“局面评估”为核心,通过“合法动作枚举 + 模拟评分 + 最坏情况分析”来做行动类型选择和选牌优化,并用模块化方式将这一过程编程实现出来。
代码可复用性与需求变更
→ 📖 Q3.5(P) 请说明针对该任务,你们对 🧑💻 T2 中已实现的代码进行了哪些复用和修改。
在完成 t3 时,我们对 t2 的复用主要集中在“历史记录解析”“状态表示方式”和“局面恢复思路”这三个方面。虽然 t3 的目标已经从“根据一轮记录恢复场面”进一步扩展为“根据当前局面做出决策”,但它仍然建立在 t2 已经解决的一项核心能力之上:如何把字符串形式的对局记录,转化成程序可以处理的结构化状态。
首先,在数据表示上,我们延续了 t2 中已经确定下来的基本约定。也就是说,仍然使用 A~G 对应数组下标 0~6 的方式来表示七位艺伎;仍然使用长度为 7 的数组来表示某一方在各艺伎上的牌数或状态;对 board 的理解也保持一致,即 1 表示倾向我方,-1 表示倾向对手,0 表示中立。这种统一的数据表示,使得从 t2 过渡到 t3 时,不需要重建整个状态建模方式,很多后续逻辑都可以在熟悉的表示框架上继续扩展。
其次,在历史记录处理上,t3 很明显复用了 t2 的核心思路:都是从 history 字符串出发,按顺序解析行动记录,并根据行动类型来判断哪些牌进入谁的区域。t2 中我们已经完成了对密约、取舍、赠予和竞争这四种行动的规则拆分,因此到了 t3,我们不需要重新从零理解“一个记录片段意味着什么”,而是可以在已有经验上继续扩展,把记录解析这件事进一步细化到“当前轮公开可见的牌数统计”“当前是否处于响应阶段”“哪些行动已经被使用”等问题上。
第三,在状态恢复思路上,t3 对 t2 进行了直接延伸。t2 关注的是“小轮结束后双方场面是什么样”,因此它的目标是恢复完整的当前状态;而 t3 关注的是“在局面尚未结束时,我现在应该做什么”,所以它不只需要恢复结果,还需要恢复一个可供决策使用的中间局面。也就是说,t3 并没有抛弃 t2 的状态恢复思路,而是把它从“终局结算用途”改造成了“决策前局面评估用途”。例如,程序会先从历史记录中提取当前轮已经公开投入的牌数,再基于这些信息去评估哪位艺伎更值得争夺、当前行动会造成什么影响。
在此基础上,t3 相比 t2 也做了比较明显的修改和扩展,主要包括以下几个方面。
第一,新增了“响应阶段判断”。t2 只需要处理一条完整历史,因此默认所有信息都已经齐全;而 t3 中程序可能正处在“轮到自己选择对手赠予/竞争结果”的阶段,所以必须先判断当前是主动出牌,还是只需要返回一个 -X 或 -XY 形式的响应。
第二,新增了“行动使用情况跟踪”。在花见小路的规则里,每位玩家在一轮内四种行动各只能使用一次。t2 中不需要关心这个问题,因为它只负责恢复给定记录;但 t3 中程序要主动做决策,因此必须根据历史统计当前自己已经用过哪些行动,并据此限制候选动作集合。
第三,新增了“合法动作枚举”。t2 的输出是状态,输入给定后几乎只有一种正确恢复结果;而 t3 的输出是行动,因此程序需要从当前手牌中生成所有可能合法动作,再逐个比较。这个部分可以看作是在 t2 状态恢复之上新加的一层“动作空间生成”。
第四,新增了“局面评估和策略选择”机制。这是 t3 相比 t2 最大的变化。t2 只需要回答“现在是什么状态”,t3 则需要回答“在这些可能的动作中,哪一个更值得做”。因此我们在保留状态恢复能力的同时,又增加了艺伎重要度分析、局面评分函数、最坏情况评估、对手信息估计等策略模块。
第五,新增了“对手信息处理”的考虑。t2 在题目设定中基本面对的是完整信息输入,因此恢复状态时不需要强烈考虑不确定性;而 t3 则更贴近真实对局,需要面对对手暗置牌、未知弃牌等信息缺失。因此我们开始引入对手公开信息统计,以及对其剩余可能手牌的粗略估计。这可以看作是从 t2 的完整信息恢复,向不完全信息决策迈出的第一步。
总体来说,我们对 t2 的复用不是简单地“直接调用旧函数”,而是更偏向于复用了 t2 已经建立好的状态建模方式和规则解析框架。在此基础上,t3 进一步增加了行动合法性判断、候选动作生成、局面评估和策略选择等模块。也就是说,如果说 t2 的核心是“把历史记录解释成当前局面”,那么 t3 的核心就是“在解释出当前局面的基础上,进一步做出一个尽量好的决策”。从这个角度看,t3 可以视为在 t2 的状态恢复器之上,叠加了一层策略决策器。
→ 📖 Q3.6(I) 请说明在编码实现时,可以采取哪些设计思想、考虑哪些设计冗余,来提高既存代码适应需求变更的能力。
在 T3 这类策略性任务中,提高既存代码对需求变更的适应能力,核心在于解耦。不应将状态恢复、策略评估、动作选择三者混写,因为它们虽相关,但变化动因不同:状态恢复依赖规则定义,策略评估依赖算法思路,动作选择则受合法性与评分共同制约。若糅合在同一个大函数中,后续无论是更换评分函数、增加估值维度,还是将启发式升级为搜索,都会引发大规模修改。
因此,编码时应秉持以下设计思想。其一,分层模块化,将输入解析、局面恢复、合法动作枚举、局面打分、动作决策拆分为独立组件。其二,统一底层数据表示,例如始终用相同数组结构表示七位艺伎、一致方式维护 board 状态,使上层策略变更时无需重构数据基础。其三,将牌面转下标、手牌统计、行动类型提取、公开牌数恢复等基础规则封装为稳定函数,避免策略调整时重复处理底层逻辑。
所谓设计冗余,在此更接近为未来升级预留演进空间。例如,当前虽采用启发式评分,但若从一开始就通过清晰接口调用评分函数,而非将其散落在 if-else 中,则未来替换为搜索、采样或博弈求解时将更加顺畅。又如,尽管目前对手信息估计较为简单,但若提前将公开信息提取与隐藏信息估计分离,后续引入更复杂的信念状态建模时,原有架构仍可复用。这类冗余并非浪费,而是对可演化性的投资。
与规则实现题不同,T3 的变化不仅来自外部需求,更源于我们对策略效果的持续调优。因此,代码不能仅满足当前能跑,更要做到后续易改。应将可复用的功能,如历史解析、合法动作判断、局面恢复,作为稳定基础设施,而将评分逻辑、艺伎权重、对手建模等易变内容独立封装。避免将策略写死为大量特例分支,尽量通过统一的评估与动作机制驱动决策,使新增评价指标时只需局部修改。同时,保留中间状态输出,让程序不仅能决策做什么,也能解释为什么,便于后续调试优化。
归根结底,要接受当前方案大概率不是最终方案这一现实。在实现时适度抽象,例如单独封装对手信息处理、统一响应与主动动作的评估框架,能让未来策略迭代时,核心结构依然稳固。对策略系统而言,真正有价值的,不是某一套具体的评分规则,而是一个支持策略持续演进的代码架构。
至于设计冗余,在实现时,可以适当多做一些抽象,例如把对手信息处理单独放一层,把响应动作和主动动作的处理逻辑都建立在同一套局面评估机制上。这样即使未来想替换评估方式,或者把启发式升级成更强的搜索算法,已有框架仍然能保留下来。对策略系统来说,真正有价值的不是某一版具体评分规则,而是一个允许策略不断迭代的代码结构。
软件度量
→ 📖 Q3.7(P) 请说明你们如何量度所实现的程序模块的有效性,例如:“如何说明我们的程序模块决策能力很强?”,尝试提出一些可能的定量分析方式或测试方式。
我们认为,要量度一个决策程序是否有效,不能只看“它能不能运行”,而应该从正确性、稳定性、效率和博弈表现这几个维度综合考察。对于 t3 这样的策略模块来说,所谓“决策能力很强”,并不等于它在某一局里恰好赢了,而是指它在大量对局、不同对手和不同先后手条件下,都能持续表现出较好的决策质量。
首先,最直接的量化指标是胜率。可以让程序与若干不同强度的基线对手进行重复对局,例如随机策略、简单贪心策略,或者其他实现版本的程序,然后统计总胜率、净胜场和平均得分差。如果程序在足够多场次下能够稳定战胜随机策略,并且相较于简单贪心策略仍然具有明显优势,那么至少可以说明它已经学会了利用游戏规则做出比“瞎选”更合理的决策。进一步地,还可以区分先手和后手分别统计胜率,因为花见小路中先后手会影响行动顺序和资源分配,如果一个程序只在先手时表现好,而在后手时明显失常,那么它的决策能力仍然是不平衡的。
其次,可以从稳定性角度进行度量。具体来说,就是在不同随机种子、不同牌堆顺序、不同测试批次下重复实验,观察结果波动是否较小。如果一个策略偶尔能打出很漂亮的结果,但在换一组测试数据后表现迅速下滑,那么它更可能只是“碰巧适配了某些局面”,而不是真正稳健。相反,如果在多组数据下都能维持相对稳定的胜率和较低的失误率,那么更能说明该程序确实掌握了较一般化的决策规律。
第三,可以从异常情况和规则正确性角度进行量化。一个策略程序即使思路再好,如果经常输出非法动作、重复使用行动类型、使用不存在于手牌中的牌,或者因为异常和超时直接判负,那么它在工程上仍然不能算有效。因此,除了胜率以外,还应当统计以下指标:
- 非法动作出现次数;
- 超时次数;
- 异常终止次数;
- 测试脚本中的格式错误次数。
如果这些指标不能稳定保持为零,那么即使程序胜率看起来不错,也不能说明它真正达到了可提交、可对战的质量要求。
第四,可以量度程序的决策效率。因为题目给出的约束是每一步最多 2 秒,因此不仅要看能不能在少量样例下跑通,还要统计:
- 单步平均决策耗时;
- 单步最大决策耗时;
- 整局总耗时;
- 在高压局面或复杂历史记录下的性能表现。
如果一个程序胜率不错,但经常逼近时限甚至偶发超时,那么它的有效性依然有限。相反,如果程序既能保持较高胜率,又有稳定的时间冗余,那就说明它在策略质量和运行效率之间取得了较好的平衡。
第五,可以从“局部决策质量”角度设计更细的测试。也就是说,不只是看最后赢没赢,而是专门构造一些关键局面,让程序在这些局面下做选择,再检查它是否做出了符合直觉或符合分析预期的决策。例如:
- 当某位高价值艺伎只差一步就能锁定时,程序是否会优先强化该位置;
- 当某些牌的边际收益已经很低时,程序是否会倾向于通过取舍进行资源止损;
- 当面对赠予和竞争时,程序是否会避免明显吃亏的选择;
- 当某个行动类型已经用过时,程序是否能正确避开非法动作。
这类测试虽然不像胜率那样直观,但更适合帮助我们分析程序到底“为什么强”或者“为什么弱”。
第六,可以做消融实验,也就是逐步关闭某些策略模块,观察程序表现变化。例如:
- 去掉艺伎重要度计算,只按静态分值选牌;
- 去掉最坏情况分析,只按理想情况评估赠予和竞争;
- 去掉对手公开信息估计,只使用当前手牌和场面;
- 去掉局面评分中的全局阈值因素,只看局部牌数差。
如果关闭某个模块后,胜率明显下降,就可以说明这个模块对整体策略有效性有较大贡献;反过来,如果某部分去掉后影响不大,也许说明它的设计还有优化空间。这种方法不仅能评估“整体强不强”,还能帮助我们定位“到底是哪一部分让程序变强”。
从我们当前工程的角度看,test.js 可以帮助确认程序是否能完成合法对局,benchmark.js 则可以用来做批量比赛、统计胜负结果和平均耗时。因此,如果要做一个相对完整的定量分析,我们会倾向于采用如下指标组合:
1. 对随机策略、贪心策略、镜像策略分别进行多轮对战,统计胜率;
2. 将先手和后手的表现分开统计;
3. 统计非法动作、异常和超时次数;
4. 统计平均决策耗时和最大决策耗时;
5. 对关键局面做专项测试;
6. 用消融实验分析各策略模块的实际贡献。
最后,我们还和别的组进行对打:

综合来看,我们认为“如何说明程序模块决策能力很强”这个问题,不能只靠单场胜负来回答,而应通过多轮对局的胜率、稳定性、合法性、运行效率以及模块贡献分析来共同说明。只有当一个程序在这些方面都表现较好时,才可以比较有说服力地认为它具备较强的决策能力。
总结
→ 📖 Q3.8(P) 请记录下目前的时间,并根据实际情况填写 附录A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
2026.4.2 10:00
| Personal Software Process Stages | 个人软件开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| PLANNING | 计划 | 20 | 15 |
| - Estimate | - 估计这个任务需要多少时间 | 20 | 15 |
| DEVELOPMENT | 开发 | 210 | 225 |
| - Analysis & Design Spec | - 需求分析 & 生成设计规格(确定要实现什么) | 20 | 25 |
| - Technical Background | - 了解技术背景(包括学习新技术) | 20 | 30 |
| - Coding Standard | - 代码规范 | 10 | 10 |
| - Design | - 具体设计(确定怎么实现) | 30 | 35 |
| - Coding | - 具体编码 | 90 | 85 |
| - Code Review | - 代码复审 | 15 | 15 |
| - Test Design | - 测试设计(确定怎么测,比如要测试哪些情景、设计哪些种类的测试用例) | 10 | 10 |
| - Test Implement | - 测试实现(设计/生成具体的测试用例、编码实现测试) | 15 | 15 |
| REPORTING | 报告 | 60 | 60 |
| - Quality Report | - 质量报告(评估设计、实现、测试的有效性) | 20 | 20 |
| - Size Measurement | - 计算工作量 | 10 | 10 |
| - Postmortem & Process Improvement Plan | - 事后总结和过程改进计划(总结过程中的问题和改进点) | 30 | 30 |
| TOTAL | 合计 | 290 | 300 |
→ 📖 Q3.9(I) 请写下本部分的心得体会。
这一部分的体验,与前两个章节截然不同。T1和T2的核心任务是将规则准确无误地落地,追求的是逻辑的严密与正确;而T3则是在规则稳固的基础上,进一步探索如何做出更优的决策。这也决定了T3没有唯一的标准答案,它更像是在有限的时间、算力与信息约束下,不断逼近一个更合理的策略空间。
在这个过程中,我最深刻的体会是,真正的挑战不在于让程序“能跑”,而在于定义何为“跑得好”。实现一个返回合法动作的函数并不困难;难点在于如何将局面评估、行动选择、对手响应预测以及不完全信息推理等因素有机整合,使每一步决策都具备可解释的合理性,而非随机选择。这让我意识到,策略问题的思维范式与普通功能实现截然不同——它更强调评估体系的构建、决策之间的权衡与近似求解的能力,而非单纯的逻辑闭环。
此外,我也深刻体会到了工程约束在实际开发中的决定性作用。理论上,我们可以构建更复杂的搜索算法或更精细的推理模型,但受限于时间上限、接口规范与实现成本,必须在“更聪明”与“更快、更稳定”之间做出权衡。这种在理论最优与工程可行之间寻找平衡点的过程,恰恰是真实软件工程的核心所在:最有价值的方案,往往不是理论上最强的那一个,而是最契合当前约束条件的那一个。
结对项目总结
结对过程回顾和反思
→ 📖 Q4.1(P) 提供两人在讨论的结对图像资料。

→ 📖 Q4.2(P) 回顾结对的过程,反思有哪些可以提升和改进的地方。
在整个结对过程中,从最初的分工有些模糊、节奏有些混乱,到后来的配合默契高效推进,期间经历了不少磨合,也留下了一些值得改进的地方,这些反思也让我们对协作有了更加具体的认知。和之前我们独立完成课程设计不同,结对编程的核心不是两个人各做各的,而是两个人共同解决一个问题。
在初期,通常会出现没有提前明确分工和沟通节奏,常常出现一个人埋头写代码,另一个人跟不上思路的情况,有时甚至会因为对需求的理解不一致,导致需要反复修改,浪费了不少时间。这是我们最需要改进的地方:前期需求拆解和分工规划不够细致,没有提前对齐每一个任务的预期和执行标准,合适的方法是先前约定好沟通的频率和方式,比如什么时候同步进度、什么时候讨论难点。
在遇到技术难点时,我们通常会陷入各自纠结的误区,没有及时切换角色、互补配合。比如其中一人对某个函数实现不熟悉,另一人没有及时耐心讲解,而是各自查看试错,导致问题解决效率偏低。总的来说,结对编程的改进方向,核心是做好前期规划、高效沟通和角色互补这些更高层次上的任务,这样才能真正发挥两个人的优势,避免内耗,提升协作效率。
→ 📖 Q4.3(I) 锐评一下你的搭档!并请至少列出三个优点和一个缺点。
1.逻辑思维非常清晰,面对复杂的需求和技术难点时,他总能快速拆解问题、梳理思路,把一个庞大的任务拆分成一个个可执行的小模块,然后逐一突破。。
2.耐心且善于沟通,这也是结对编程中非常重要的一点。在我对某个知识点不熟悉、或者对需求理解有偏差时,他不会不耐烦,而是会用通俗的语言反复讲解,直到我理解为止;遇到意见不一致的地方,他也不会固执己见,而是会认真倾听我的想法。
3.责任心强,执行力高。他总能按时甚至提前完成自己负责的任务,而且会主动检查代码质量,发现问题及时反馈、及时修改。
缺点,效率很高,速度很快,有时候可能会有一些小问题小疏忽。
对结对编程的理解
→ 📖 Q4.4(I) 说明结对编程的优缺点、你对结对编程的理解。
开始,我心里认为这种模式有点鸡肋有点扯淡,而且感觉有点莫名其妙,浪费时间,通过这次结对项目,我对结对编程有了更深刻具体的理解,它不是简单的两个人一起写代码,而是一种高效严谨的协作模式,有其独特的优点,也存在一些不可避免的缺点,关键在于如何合理运用、扬长避短,让协作的价值最大化。在我看来,结对编程的核心是互补监督和共同成长,它打破了独立编程的局限性,让两个人在协作中互相启发、互相约束,最终产出更优质的代码。
结对编程的优点非常突出,首先是能够有效减少代码bug,提升代码质量。独立编程时,我们很容易陷入自己的思维定式,忽略一些细节错误,而结对编程中,导航员可以实时关注代码的逻辑、语法和规范性,及时发现驾驶员的疏漏,避免错误被遗留到后续环节,大大降低了调试的难度和成本。结对编程还能够促进知识共享,实现共同成长。每个人的知识储备、技术能力和思维方式都有所不同,结对过程中,两个人可以互相学习、互相借鉴。比如我对某个编程语言的语法掌握不够熟练,搭档会耐心讲解;而我对策略设计的思路比较灵活,也能给搭档提供一些新的启发。这种双向的学习,让我们在完成项目的同时,也弥补了自己的不足,提升了自身的技术能力和协作能力。结对编程还能提升问题解决效率,避免陷入思维误区。面对复杂的技术难点时,一个人很容易陷入死胡同,而两个人一起讨论、一起头脑风暴,能够快速打开思路,找到解决问题的方法。在解决开放性的决策问题时,各自提出不同的思路,经过讨论融合,可以形成了更合理、更高效的策略,比一个人埋头思考节省了很多时间。
当然,结对编程也存在一些缺点。首先是可能出现效率内耗,尤其是在初期磨合阶段。如果两个人的思路不一致、沟通不顺畅,或者分工不明确,很容易出现互相拉扯、重复工作的情况,反而比独立编程效率更低。还可能限制个人思维的自由发挥。独立编程时,我们可以按照自己的思路自由探索,而结对编程中,需要不断和搭档沟通、协商,有时为了达成共识,可能会放弃自己的一些想法,一定程度上限制了创造性。
对于结对编程,它不是人数上的简单叠加,而是一场协作升级。它不仅是一种编程方式,更是一种沟通方式、学习方式。结对编程的核心不是两个人一起完成任务,而是通过协作,弥补个人的不足,发挥各自的优势,最终产出更优质、更可靠的代码,同时实现两个人的共同成长。在实际的软件工程中,结对编程能够有效提升团队的协作效率和代码质量,减少后期维护的成本,是一种非常有价值的协作模式。
代码实现提交
→ 📖 Q4.5(P) 请提供你们完成代码实现的代码仓库链接。
https://github.com/flowerinautumn/BUAASE2026-PairProgramming.git

浙公网安备 33010602011771号