[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

  1. 任务预计耗时与实际耗时(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
  1. 完成编程任务期间的过程记录

完成任务时,我们先阅读了项目说明、README 和作业要求,了解需要实现的功能、输入输出格式以及提交规范,并对整体耗时做了简单估计。由于开始之前对 Wasm、AssemblyScript 和《花见小路》都不熟悉,所以先查阅了相关资料,了解基本概念、项目代码结构和游戏规则。

在分析需求后,我们讨论了策略实现的大致方向,重点考虑了如何在合法行动范围内做出较优选择,以及如何设计响应阶段的判定逻辑。随后结合现有代码框架,确定了先保证功能正确、再逐步优化策略效果的实现思路。

编码时,我们先完成基础逻辑,保证程序能够正常运行并输出合法结果,再逐步补充局面评估和策略判断。过程中遇到的主要问题有:对游戏规则理解不够熟悉、部分 AssemblyScript 写法和常见 JavaScript/TypeScript 不完全一样、初版策略效果一般。针对这些问题,我们重新查看规则说明,参考已有代码接口,修改不合适的实现方式,并通过多次测试不断调整策略。

测试方面,我们主要做了合法性测试、基础局面测试,以及与随机策略或简单策略的对战测试,检查程序是否稳定、决策是否合理。完成测试后,我们又对代码进行了简单复审,检查命名、结构和接口是否符合要求,并总结了本次任务中遇到的问题和可以改进的地方。

→ 📖 Q1.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:

  1. 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
  2. 完成编程任务期间,依次做了什么(例如查阅了哪些资料、如何设计判定逻辑、如何设计测试样例、遇到了什么问题、如何解决)。
  1. 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
  1. 完成编程任务期间的过程记录

在完成本次编程任务时,我们先阅读了项目说明、README、题目要求和提交规范,明确了需要完成的功能、输入输出形式以及测试要求,并先对整个任务的大致耗时进行了估计。

由于在项目开始之前我们对 Wasm、AssemblyScript 以及桌游《花见小路》都不熟悉,因此先查阅了相关资料,了解 Wasm 和 AssemblyScript 的基本概念、项目已有代码的组织方式,以及《花见小路》的基本规则、行动类型和胜负判定方法。通过这一阶段的了解,我们对后续实现的大致方向有了初步认识。

在需求分析和设计阶段,我们重点考虑了两个问题:一是程序怎样保证输出合法行动,二是在合法行动的基础上怎样让策略尽可能更合理。为此,我们先梳理了不同阶段需要处理的情况,包括主动出牌阶段和响应阶段,然后结合当前局面、牌的分布情况以及不同选择可能带来的收益,设计了一个较为基础的判定思路。整体上采用的是先枚举合法操作,再根据局面做简单比较和选择的方式。

在编码实现过程中,我们先完成基础功能,保证程序能正常运行并通过基本测试;然后再逐步补充和调整策略逻辑。过程中遇到的主要问题有:对游戏规则理解不够准确,某些行动的处理细节容易混淆;AssemblyScript 与常见的 JavaScript/TypeScript 写法存在差异,部分习惯写法不能直接使用;初版策略虽然能运行,但效果不够稳定。针对这些问题,我们重新阅读规则说明,对照已有接口检查实现细节,并通过多次运行测试来定位问题,再对对应逻辑进行修改。

在测试方面,我们设计并执行了几类测试:首先是基础合法性测试,检查程序是否会输出不符合规则的操作;其次是若干典型局面测试,观察不同局面下程序的决策是否合理;最后还进行了与随机策略、简单策略的对战测试,用来大致评估当前策略的实际表现。通过这些测试,我们发现了一些评分和选择上的问题,并继续进行了调整。

任务完成后,我们又对代码进行了简单复审,检查命名、结构、注释和接口是否符合要求,同时回顾了整个实现过程。总体来看,这次任务中花费时间较多的部分主要是前期的规则理解和后期的测试调整,而不仅仅是编码本身。通过这次任务,我们对 Wasm/AssemblyScript 的基本使用方式、策略类程序的设计过程,以及测试在改进程序中的作用都有了更直接的认识。

设计

→ 📖 Q1.3(P) 请说明你们为这个判定模块设计了哪些中间量或辅助函数;如果没有额外设计,也请说明为什么认为直接实现已经足够清晰。

为了让判定模块更清晰,我们没有把所有规则和策略判断都直接写在主流程里,而是设计了一些中间量和辅助函数来拆分逻辑。因为这个模块不仅要判断当前有哪些合法操作,还要在多个合法操作里选择相对更优的一种,如果全部直接展开在一个函数中,代码会比较乱,也不方便后续修改和调试。

在中间量方面,我们主要关注了当前局面的几个信息。比如双方在各个艺伎上的得分或控制情况、当前手牌中不同点数牌的分布、某个行动执行后局面可能发生的变化、以及当前可选行动的集合。这些中间量可以帮助程序先整理出“现在是什么局面”,再去判断“接下来怎么选更合适”。

在辅助函数方面,我们主要把逻辑分成了几类。第一类是合法性相关函数,用来枚举当前所有可执行的操作,或者检查某个操作是否符合规则。这样可以避免在主流程里反复写同样的规则判断。第二类是评估相关函数,用来对某个行动或某个响应方案进行打分,比较它们对当前局面的影响。第三类是局面分析相关函数,用来统计某类牌的重要性、估计某个艺伎是否值得继续争夺,或者模拟执行某一步后的局面变化。

这样设计之后,主判定流程就比较直接:先得到所有合法操作,再调用评估函数对这些操作进行比较,最后选出最合适的结果返回。这样的写法比完全直接实现更清晰,也更方便后续优化。如果之后发现策略效果不够好,通常只需要修改评估函数或某些中间量的计算方式,而不需要把整个判定模块重写。

→ 📖 Q1.4(I) 请说明在这样一个规则判定类模块中,如何避免“漏判”“错判”或分支顺序错误等问题。

我觉得要避免这类模块出现漏判、错判或者分支顺序写错,最重要的是不要一上来就直接写代码,而是先把规则理清楚。因为这种题最麻烦的地方不在于代码能不能跑,而在于看起来能跑,但结果其实判错了。

应该先把所有情况列出来。比如有哪些正常情况,哪些是特殊情况,哪些属于不合法情况。只有先把这些情况整理清楚,写代码的时候才不容易漏掉。否则很容易写着写着只处理了自己一开始想到的几种情况,后面才发现还有别的分支没覆盖到。

还有写判断的时候顺序要固定。一般都是先判断是否合法,再判断属于哪一种情况,最后如果有多个合法选择,再去比较哪个好。这样会比较稳。如果一开始就把各种策略判断和规则判断混在一起写,很容易顺序一乱,前面某个分支就把后面的情况提前截走了,结果虽然程序没报错,但实际判定是错的。

另外,我觉得把不同功能拆开也很重要。比如“这个操作能不能做”是一类问题,“这些能做的操作里选哪个更好”是另一类问题,最好不要混在同一个大 if-else 里面。分开写的话,出问题时也更容易查,到底是规则判断错了,还是评分逻辑有问题。

边界情况要单独注意。像只有一种合法操作、没有某类牌、或者几个条件特别接近的时候,最容易出错。这些情况如果不专门想一下,代码里很容易直接漏掉。

最后还是要靠测试。不能只测最正常的情况,最好每一种分支都找一个例子去试一下,再测一些容易混淆的情况。这样比较容易发现有没有漏掉某个分支,或者某两个判断的顺序写反了。总的来说,就是先把规则列清楚,写的时候按固定顺序来,再把合法性判断和策略判断分开,最后靠测试去检查。这样基本就能少很多漏判和错判。

测试

→ 📖 Q1.5(P) 请说明你们设计了哪些测试用例,这些测试分别覆盖了哪一类规则或边界情况。

我们为 T1 的胜负判定模块设计了 13 个测试用例,主要覆盖了题目要求中的核心规则和若干边界情况。

  1. 按总分立即获胜:

    • 测试“我方总分达到或超过 11 分时返回 1”;
    • 测试“对手总分达到或超过 11 分时返回 -1”。 这一组用例覆盖了题目中的第一类立即胜利条件。
  2. 按倾心标记数量立即获胜:

    • 测试“我方持有至少 4 枚倾心标记且总分未到 11 分时,仍然直接获胜”;
    • 测试“对手持有至少 4 枚倾心标记时直接获胜”。 这一组覆盖了第二类立即胜利条件,也验证了“4 枚标记判胜”和“11 分判胜”是两条并列规则。
  3. 前两轮未分胜负时继续游戏:

    • 分别测试在第 1 轮和第 2 轮结束后,若双方都没有达到立即胜利条件,则函数应返回 0。 这组用例验证了“前两轮不做最终比较,而是继续下一轮”的规则。
  4. 第三轮按总分判定胜负:

    • 测试“第三轮结束后我方总分更高时返回 1”;
    • 测试“第三轮结束后对手总分更高时返回 -1”。 这一组覆盖了第三轮在未触发立即胜利条件时,优先按总分比较的规则。
  5. 第三轮总分相同,按最高档位倾心标记判定:

    • 测试“双方总分相同,但我方拥有 G 时获胜”;
    • 测试“双方总分相同,但对手拥有 F 而我方只有更低档位标记时,对手获胜”;
    • 测试“双方总分相同,最高非空档位落在 D/E,且该档位只由我方拥有时,我方获胜”。 这组用例主要验证了 G > F > D/E > A/B/C 的优先级顺序是否正确。
  6. 第三轮平局情况:

    • 测试“双方总分相同,且最高非空档位也无法区分时返回 2”;
    • 测试“第三轮结束时所有标记都仍为中立时返回 2”。 这组用例覆盖了题目要求中的平局判定,以及‘没有任何有效倾向信息’这一边界情况。

→ 📖 Q1.6(I) 请说明你对“先写测试再实现”与“先实现再补测试”两种方式的理解。

我认为“先写测试再实现”和“先实现再补测试”各有适用场景,但在这次 T1 这种规则明确、输入输出清晰的任务里,我更倾向于先写测试再实现。

先写测试的好处是,它能逼着我先把题目的规则顺序想清楚。比如这道题里,必须先判断是否达到 11 分、再判断是否达到 4 枚倾心标记、最后才是在第三轮比较总分和最高档位。如果没有先把这些情况整理成测试,我在实现时就很容易漏掉某一类情况,或者把分支顺序写错。测试相当于先把“题意”固定下来,再让代码去满足这些要求。

不过,先写测试也有一个前提:自己对规则的理解已经比较稳定。如果一开始连题意都没读透,测试本身也可能写错,后面还要返工。因此我觉得更合适的做法是:先整理规则,先写关键测试,再边实现边补充测试。这样既能保持方向正确,也不会因为一开始测试写得过细而影响推进速度。

总结

→ 📖 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) 请写下本部分的心得体会。

通过这一部分的实现,我最大的感受是,规则类题目看起来不复杂,但真正写的时候很容易在细节上出问题。尤其是一开始对游戏规则和判定顺序不够熟的时候,会觉得思路差不多就能写出来,但实际一做才发现,很多地方如果没有提前想清楚,就很容易漏掉某种情况,或者把判断顺序写错。

另外,我也感觉测试真的很重要。因为这种模块不像有些题那样一运行就能看出哪里错了,很多时候程序可以正常执行,但结果其实已经判错了。如果不专门去设计不同情况的测试,就很难发现这些问题。所以这次做下来,我觉得先把规则整理出来,再配合测试一点点检查,比一开始只顾着写代码更有效。

还有一个体会是,前期的理解工作不能省。虽然真正写代码的时间不算特别长,但在开始之前花时间去看题目、看规则、看已有代码结构,其实能减少后面很多返工。特别是这次还接触到了之前不熟悉的 Wasm 和 AssemblyScript,更让我感觉到,先把背景弄明白再动手会更稳一些。

总的来说,这一部分让我更明显地体会到,规则判定模块的重点不只是“实现出来”,而是“实现得对”。以后再做类似任务时,我会更注意先梳理规则、分清分支顺序,并且尽早补上测试,而不是等写完以后再统一检查。

Chapter.2 不祥之影

准备

→ 📖 Q2.1(P) 请记录下目前的时间。
2026.3.27 9:00

→ 📖 Q2.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:

  1. 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
  2. 完成编程任务期间,依次做了什么(比如查阅了什么资料,随后如何进行了开发,遇到了什么问题,又通过什么方式解决);
  1. 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
  1. 完成编程任务期间的过程记录

在完成这一部分任务时,我们先阅读了 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 的基础上新增了以下内容:

  1. 新增了对行动记录字符串的解析逻辑;
  2. 新增了双方区域牌数的统计数组;
  3. 新增了对四种行动(密约、取舍、赠予、竞争)的分别处理;
  4. 新增了根据双方场面牌数重新计算倾心标记状态的结算逻辑。

也就是说,T2 对 T1 的复用更多体现在“底层表示一致”和“拆分规则、分步实现”的设计思想上,而修改和扩展则主要集中在对完整一轮过程的恢复与结算上。

→ 📖 Q2.4(I) 请说明在编码实现时,可以采取哪些设计思想、考虑哪些设计冗余,来提高既存代码适应需求变更的能力。

我觉得为了提高既有代码适应需求变更的能力,最重要的一点把“稳定的东西”和“容易变化的东西”分开。像这次项目里,A~G 的牌面编号、长度为 7 的数组表示、1/0/-1 的标记含义,这些都是相对稳定的;而具体要不要解析 history、是否有 X、如何结算标记,则属于更容易变化的规则层。如果一开始就把这些内容混在一起写,后面需求一改,往往整段逻辑都要重写。

因此在编码时,我认为比较有效的设计思想包括:

  1. 统一数据表示:让多个任务共用一致的状态结构,这样后续功能扩展时不需要频繁改接口;
  2. 拆分辅助函数:把“字符转下标”“更新区域”“结算标记”这类步骤拆出来,避免主流程过于耦合;
  3. 分层组织逻辑:尽量把“输入解析”“状态更新”“结果计算”分开,这样以后即使输入格式变化,也不会影响后面的核心逻辑;
  4. 保留可扩展接口:例如即使当前任务只需要完整信息,也可以让某些函数保留处理未知信息或异常输入的空间。

我理解的“设计冗余”并不是无意义地多写代码,而是为未来可能的变化预留结构。例如,把每种行动单独处理,而不是写成一个巨大的条件分支;再比如,让状态计算和胜负判断彼此独立。这样一来,后面如果题目从完整信息扩展到不完全信息,或者从状态恢复扩展到策略决策,已有代码仍然能较大程度地复用。

头脑风暴环节

→ 📖 Q2.5(P) 头脑风暴环节:

我们终于快要开始让程序玩游戏了!请尝试分析:T2 中不带 X 的操作记录比起实际对局多出了多少信息?如果加上 X,也就是失去了这部分信息的话,如何处理对小轮结束后状态的估计?

T2 中不带 X 的操作记录,相比实际对局,最大的区别在于:它把原本属于隐藏信息的内容也完整公开了。因此,T2 并不是真正意义上的“从不完全信息中推断状态”,而更接近一个完整信息条件下的状态恢复问题。

具体来说,T2 比实际对局多出的信息主要有以下几类:

  1. 密约牌的信息被公开了

    在真实对局中,行动 1(密约)打出的牌在本轮结算前对对手是不可见的,只能记作类似 1X。而在 T2 中,输入的是完整一轮结束后的记录,因此这张牌已经被明确写出,例如 1D1C。这意味着我们不需要猜测暗置牌是什么。

  2. 取舍牌的信息也被公开了

    在真实对局中,行动 2(取舍)弃掉的两张牌对对手同样不可见,只能记作 2XX。但在 T2 中,这两张牌会被完整写出,例如 2GG2FF。因此我们不仅知道对方弃过牌,还知道具体弃掉了什么牌。

  3. 整轮结束后的信息是“确定的”

    由于密约和取舍的内容都不再隐藏,配合赠予和竞争本身就是公开行动,T2 的 history 实际上已经包含了恢复场面的全部必要信息。因此在 T2 中,小轮结束后的双方区域牌数以及新的倾心标记状态,理论上都可以唯一确定。

也就是说,T2 中不带 X 的记录,相当于把真实对局中的部分隐藏信息全部揭示出来了。从信息论角度看,它减少了不确定性,使得状态恢复从“估计问题”退化成了“计算问题”。

如果重新加入 X,也就是回到更贴近真实对局的输入形式,那么问题就会发生本质变化:我们不再能唯一确定小轮结束后的状态,而只能做估计。在这种情况下,可以考虑以下几种处理思路:

  1. 维护“可能状态集合”

已知的信息包括:

  • 每种牌在整副牌中的总数;
  • 我方手牌;
  • 所有公开行动中出现过的牌;
  • 对方做过哪些类型的行动;
  • 哪些牌已经确定进入双方区域、弃牌区或被选择过。

根据这些约束,可以枚举或构造所有与当前观测一致的可能状态。例如,对手打出 1X,就表示“这张牌来自其手牌,但具体是哪一种未知”;2XX 则表示有两张未知牌被移出本轮。这样就可以得到一组可能的隐藏牌分布与小轮末状态。

  1. 从“唯一结果”改为“概率分布”

如果可能状态过多,可以不再枚举所有局面,而是为每种未知牌建立一个概率估计。例如:

  • 某张 X 是高价值牌 G 的概率有多大;
  • 某个艺伎对应的隐藏牌最终更可能落在哪一方区域;
  • 小轮结束时某个倾心标记更可能偏向谁。

这样,最终的状态就不是一个确定数组,而更像是“每一项结果的概率分布”或“期望局面”。

  1. 在决策时考虑期望收益或最坏情况

如果程序后续要基于这个估计结果继续做决策,就不能只假设某一个最理想状态成立,而应当:

  • 计算在若干可能状态下动作的期望收益;
  • 或者采用更保守的最坏情况分析;
  • 也可以在两者之间折中,比如既看平均表现,也避免特别差的极端结果。

这实际上就把问题从“规则恢复”推进到了“不完全信息博弈”层面。

  1. 使用采样或模拟降低复杂度

如果严格枚举所有可能状态的代价太高,可以使用近似方法,例如:

  • 按约束随机生成若干组合法隐藏状态;
  • 在这些样本状态上模拟后续结算;
  • 统计小轮结束后各种结果出现的频率,作为对真实状态的近似估计。

这种方法虽然不能保证精确,但在 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) 请在完成任务的同时记录,并在完成任务后整理完善:

  1. 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
  2. 完成编程任务期间,依次做了什么(比如查阅了什么资料,随后如何进行了开发,遇到了什么问题,又通过什么方式解决);
  1. 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
  1. 完成编程任务期间的过程记录

在完成这一部分任务时,我们先阅读了 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,思路与赠予类似,只不过对象变成了“两组牌”。程序会枚举可能的分组方式,再分别模拟对手选择第一组或第二组后的结果,最后取较差的那个结果作为当前分组的评分。也就是说,我们在主动设计竞争时,追求的不是“存在一种选法对我有利”,而是“无论对手怎么选,我都不至于太亏”。这其实是一种比较典型的稳健优化思想。

第四,在“如何响应对手动作”上,我们没有另外写一套完全独立的规则,而是尽量复用同一套局面评估思想。当程序面对对手的赠予或竞争时,它会分别模拟不同选择后自己的收益变化,然后选择评分更高的那一种。这样做的优点是,主动出牌和被动响应使用的是同一套价值标准,整体决策逻辑更统一,也更容易维护。

从编程实现上看,我们大致把策略拆成了几个层次:

  1. 手牌统计与历史拆分,用于识别当前局面;
  2. 已使用行动的跟踪,用于限制本轮合法动作集合;
  3. 当前轮公开牌数恢复,用于建立局面基础状态;
  4. 艺伎重要度计算,用于反映局部争夺价值;
  5. 局面评分函数,用于比较不同动作后的收益;
  6. 合法动作枚举与逐个打分,用于在所有候选方案中选出当前最优动作;
  7. 对赠予和竞争的响应策略,用于在对手给出选项时做出较优选择。

从结果上看,这样的策略虽然还远远称不上理论最优,但它至少具备了几个比较重要的性质:

  • 它不是静态死规则,而是会根据当前局面变化动态决策;
  • 它在处理带有对手选择权的动作时,能够考虑最坏情况,而不是只看理想结果;
  • 它能够把局部牌面优势、倾心标记状态和整体胜利条件结合起来考虑,而不只是盯着某一位高分艺伎。

因此,我们对 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 中。再比如,虽然当前只做了比较粗略的对手信息估计,但如果一开始就把“公开信息提取”和“隐藏信息估计”分开,那么以后需要做更复杂的信念状态建模时,原有代码就还能继续用。这样的冗余并不是浪费,而是一种可演化性的投资。

同学b: 我认为 t3 这类问题和前面的规则实现题最大的不同,在于它的“变化”不仅来自需求本身,也来自我们对策略效果的不断调整。也就是说,即使题目要求不变,我们自己也很可能会反复修改策略。因此在这种情况下,代码不能只追求“现在能跑”,而应该尽量让它“之后容易改”。

具体来说,我觉得可以采用以下设计思想。第一,模块化,把可复用的功能和容易变化的策略拆开。像历史记录解析、合法动作判断、当前局面恢复,这些更偏基础设施,通常比较稳定;而行动评分、艺伎重要度计算、对手估计这类内容则更可能频繁调整。第二,避免把策略写死成大量特殊情况分支,而是尽量通过统一的评估函数、统一的动作枚举机制来驱动决策。这样后面即使要加入新的评价指标,也只需要修改局部函数,而不必重写整套决策流程。第三,尽量保留中间状态,让程序不仅能输出“选了什么动作”,还能在调试时看到“为什么这么选”,这样后续优化时会容易很多。

至于设计冗余,我觉得最重要的一点是要接受“当前方案大概率不是最终方案”。所以在实现时,可以适当多做一些抽象,例如把对手信息处理单独放一层,把响应动作和主动动作的处理逻辑都建立在同一套局面评估机制上。这样即使未来想替换评估方式,或者把启发式升级成更强的搜索算法,已有框架仍然能保留下来。对策略系统来说,真正有价值的不是某一版具体评分规则,而是一个允许策略不断迭代的代码结构。

软件度量

→ 📖 Q3.7(P) 请说明你们如何量度所实现的程序模块的有效性,例如:“如何说明我们的程序模块决策能力很强?”,尝试提出一些可能的定量分析方式或测试方式。

我们认为,要量度一个决策程序是否有效,不能只看“它能不能运行”,而应该从正确性、稳定性、效率和博弈表现这几个维度综合考察。对于 t3 这样的策略模块来说,所谓“决策能力很强”,并不等于它在某一局里恰好赢了,而是指它在大量对局、不同对手和不同先后手条件下,都能持续表现出较好的决策质量。

首先,最直接的量化指标是胜率。可以让程序与若干不同强度的基线对手进行重复对局,例如随机策略、简单贪心策略,或者其他实现版本的程序,然后统计总胜率、净胜场和平均得分差。如果程序在足够多场次下能够稳定战胜随机策略,并且相较于简单贪心策略仍然具有明显优势,那么至少可以说明它已经学会了利用游戏规则做出比“瞎选”更合理的决策。进一步地,还可以区分先手和后手分别统计胜率,因为花见小路中先后手会影响行动顺序和资源分配,如果一个程序只在先手时表现好,而在后手时明显失常,那么它的决策能力仍然是不平衡的。

其次,可以从稳定性角度进行度量。具体来说,就是在不同随机种子、不同牌堆顺序、不同测试批次下重复实验,观察结果波动是否较小。如果一个策略偶尔能打出很漂亮的结果,但在换一组测试数据后表现迅速下滑,那么它更可能只是“碰巧适配了某些局面”,而不是真正稳健。相反,如果在多组数据下都能维持相对稳定的胜率和较低的失误率,那么更能说明该程序确实掌握了较一般化的决策规律。

第三,可以从异常情况和规则正确性角度进行量化。一个策略程序即使思路再好,如果经常输出非法动作、重复使用行动类型、使用不存在于手牌中的牌,或者因为异常和超时直接判负,那么它在工程上仍然不能算有效。因此,除了胜率以外,还应当统计以下指标:

  • 非法动作出现次数;
  • 超时次数;
  • 异常终止次数;
  • 测试脚本中的格式错误次数。

如果这些指标不能稳定保持为零,那么即使程序胜率看起来不错,也不能说明它真正达到了可提交、可对战的质量要求。

第四,可以量度程序的决策效率。因为题目给出的约束是每一步最多 2 秒,因此不仅要看能不能在少量样例下跑通,还要统计:

  • 单步平均决策耗时;
  • 单步最大决策耗时;
  • 整局总耗时;
  • 在高压局面或复杂历史记录下的性能表现。

如果一个程序胜率不错,但经常逼近时限甚至偶发超时,那么它的有效性依然有限。相反,如果程序既能保持较高胜率,又有稳定的时间冗余,那就说明它在策略质量和运行效率之间取得了较好的平衡。

第五,可以从“局部决策质量”角度设计更细的测试。也就是说,不只是看最后赢没赢,而是专门构造一些关键局面,让程序在这些局面下做选择,再检查它是否做出了符合直觉或符合分析预期的决策。例如:

  • 当某位高价值艺伎只差一步就能锁定时,程序是否会优先强化该位置;
  • 当某些牌的边际收益已经很低时,程序是否会倾向于通过取舍进行资源止损;
  • 当面对赠予和竞争时,程序是否会避免明显吃亏的选择;
  • 当某个行动类型已经用过时,程序是否能正确避开非法动作。

这类测试虽然不像胜率那样直观,但更适合帮助我们分析程序到底“为什么强”或者“为什么弱”。

第六,可以做消融实验,也就是逐步关闭某些策略模块,观察程序表现变化。例如:

  • 去掉艺伎重要度计算,只按静态分值选牌;
  • 去掉最坏情况分析,只按理想情况评估赠予和竞争;
  • 去掉对手公开信息估计,只使用当前手牌和场面;
  • 去掉局面评分中的全局阈值因素,只看局部牌数差。

如果关闭某个模块后,胜率明显下降,就可以说明这个模块对整体策略有效性有较大贡献;反过来,如果某部分去掉后影响不大,也许说明它的设计还有优化空间。这种方法不仅能评估“整体强不强”,还能帮助我们定位“到底是哪一部分让程序变强”。

从我们当前工程的角度看,test.js 可以帮助确认程序是否能完成合法对局,benchmark.js 则可以用来做批量比赛、统计胜负结果和平均耗时。因此,如果要做一个相对完整的定量分析,我们会倾向于采用如下指标组合:

  1. 对随机策略、贪心策略、镜像策略分别进行多轮对战,统计胜率;
  2. 将先手和后手的表现分开统计;
  3. 统计非法动作、异常和超时次数;
  4. 统计平均决策耗时和最大决策耗时;
  5. 对关键局面做专项测试;
  6. 用消融实验分析各策略模块的实际贡献。

最后,我们还和别的组进行对打:

image-20260411093244506

综合来看,我们认为“如何说明程序模块决策能力很强”这个问题,不能只靠单场胜负来回答,而应通过多轮对局的胜率、稳定性、合法性、运行效率以及模块贡献分析来共同说明。只有当一个程序在这些方面都表现较好时,才可以比较有说服力地认为它具备较强的决策能力。

总结

→ 📖 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) 提供两人在讨论的结对图像资料。

47b0937e4b4f5962fb5af7ca5d2f7aaf

→ 📖 Q4.2(P) 回顾结对的过程,反思有哪些可以提升和改进的地方。
在整个结对过程中,从最初的分工有些模糊、节奏有些混乱,到后来的配合默契高效推进,期间经历了不少磨合,也留下了一些值得改进的地方,这些反思也让我们对协作有了更加具体的认知。和之前我们独立完成课程设计不同,结对编程的核心不是两个人各做各的,而是两个人共同解决一个问题。

在初期,通常会出现没有提前明确分工和沟通节奏,常常出现一个人埋头写代码,另一个人跟不上思路的情况,有时甚至会因为对需求的理解不一致,导致需要反复修改,浪费了不少时间。这是我们最需要改进的地方:前期需求拆解和分工规划不够细致,没有提前对齐每一个任务的预期和执行标准,合适的方法是先前约定好沟通的频率和方式,比如什么时候同步进度、什么时候讨论难点。

在遇到技术难点时,我们通常会陷入各自纠结的误区,没有及时切换角色、互补配合。比如其中一人对某个函数实现不熟悉,另一人没有及时耐心讲解,而是各自查看试错,导致问题解决效率偏低。总的来说,结对编程的改进方向,核心是做好前期规划、高效沟通和角色互补这些更高层次上的任务,这样才能真正发挥两个人的优势,避免内耗,提升协作效率。

→ 📖 Q4.3(I) 锐评一下你的搭档!并请至少列出三个优点和一个缺点。
我的搭档在这次结对编程中整体表现非常可靠,是一个很适合一起推进任务的人。

首先他执行力强。很多任务不是停留在讨论层面,而是能较快落到代码、测试和验证结果上,尤其是在 T1/T2/T3 的实现和调试过程中,能够持续推进问题解决。

第二个优点是测试意识比较好。除了关注代码能不能跑通,也会关注测试是否覆盖了关键场景,例如边界情况、提交测试、自写测试和多局对战验证,这对保证作业质量很有帮助。

优点三是沟通比较清楚。遇到问题时能把现象、原因和下一步处理方式说明白,例如在 T3 与其他组模块对接时,能区分是依赖问题、对方模块运行时崩溃,还是本方配置问题。

缺点是有时会比较依赖工具和 AI 辅助,导致一开始对部分实现细节的掌握不够深入,需要通过后续阅读代码、运行测试和复盘来补足理解。不过整体来看,这个问题在项目推进过程中已经有明显改善。

对结对编程的理解

→ 📖 Q4.4(I) 说明结对编程的优缺点、你对结对编程的理解。

在本次花见小路结对编程作业中,我对结对编程有了更加具体和实际的体会。我们在实现 T3 决策逻辑优化的过程中,经常需要围绕“如何设计评分函数”和“是否进行局面模拟”等关键问题进行反复讨论,这种实时交流使得一些原本容易忽略的细节能够在编码阶段就被发现。例如在优化 scoreActionevaluatePosition 时,一人提出思路,另一人会从边界情况或对局效果出发进行补充或质疑,从而让策略更加稳健,也减少了后期调试的成本。同时,在实现过程中通过不断切换“编码者”和“审阅者”的角色,我们能够更清晰地理解彼此的设计意图,也在无形中提升了代码的可读性和规范性。

不过,这次作业也让我感受到结对编程在效率上的一些问题。比如在一些实现细节较为简单的部分(如接口对接、基础逻辑补全)上,两个人同时参与会显得有些冗余,反而不如先由一人快速完成再统一 review 更高效。此外,在策略设计阶段,由于双方思路不完全一致,有时会在某些实现方式上花费较多时间讨论,虽然最终结果更优,但在时间紧张的情况下确实增加了沟通成本。特别是在 AssemblyScript 一些语言限制(如不支持闭包)带来的实现调整中,也需要反复确认修改方案,过程相对耗时。

总体来说,通过这次花见小路的结对作业,我更加明确地认识到结对编程的价值主要体现在复杂问题和关键决策阶段,它能够帮助我们减少低级错误、完善设计思路,并提升最终代码质量。而在具体实践中,也需要根据任务特点进行灵活调整,将“结对讨论”和“分工实现”结合起来,既保证质量,又兼顾开发效率。

代码实现提交

→ 📖 Q4.5(P) 请提供你们完成代码实现的代码仓库链接。
https://github.com/flowerinautumn/BUAASE2026-PairProgramming.git

posted @ 2026-04-11 09:44  windra_n  阅读(8)  评论(0)    收藏  举报