→ 📖 Q0.0(P) 【你可以在结对结束后补充】如果你的代码仓库包含 AIGC 的部分,列举使用的工具、模型和使用范围。若未使用则填写:本组提交的全部代码不包含AI补全或生成的部分。
使用 codex 实现了一个用于测评的随机出牌算法,并搭建了能够随机生成牌局的测评程序;最后一版的程序实现,是我们在已有代码的基础上,通过将优化方向告知 codex,让其生成的一版改进版。
→ 📖 Q0.1(P) 请记录下目前的时间。
2026年4月7日 20:27
→ 📖 Q0.2(I) 【你可以在结对结束后另行补充。】作为本项目的调查:
请如实标注在开始项目之前对 Wasm 的熟悉程度分级,可以的话请细化具体的情况。(分别回答两人各自的情况)
I. 没有听说过;
II. 仅限于听说过相关名词;
III. 听说过,且有一定了解;
IV. 听说过,且使用 Wasm 实际进行过开发(即便是玩具项目的开发)。
答:I. 没有听说过
请如实标注在开始项目之前对桌游花见小路的熟悉程度分级,可以的话请细化具体的情况。(分别回答两人各自的情况)
I. 不了解玩法和规则;
II. 听说过,且有一定了解;
答:I. 不了解玩法和规则
→ 📖 Q0.3(P) 请记录下目前的时间。
2026年4月7日 20:47
→ 📖 Q1.1(P) 请记录下目前的时间。
2026年4月7日 20:47
→ 📖 Q1.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
- 完成编程任务期间,依次做了什么(例如查阅了哪些资料、如何设计判定逻辑、如何设计测试样例、遇到了什么问题、如何解决)。
任务耗时估计
完成任务期间依次做了什么:
- 阅读题目要求,了解具体设计需求。
- 学习Rust基础语法,教程参考了rust语言圣经。
- 设计结算的判定逻辑。
- 具体编码实现。
- 设计测试用例,使用工具链编译,然后使用课题组提供的test.js进行测试。
- 总结与反思,回答后续的问题。
→ 📖 Q1.3(P) 请说明你们为这个判定模块设计了哪些中间量或辅助函数;如果没有额外设计,也请说明为什么认为直接实现已经足够清晰。
辅助函数:
fn get_score(board: &[i8], player: i8) -> i32- 作用:获取某个玩家总共获得的分数
- 输入:表示倾心状态的数组切片board,玩家player
- 输出:玩家player获得的分数
fn get_count(board: &[i8], player: i8) -> i32- 作用:获取某个玩家总共获得的标记数
- 输入:表示倾心状态的数组切片board,玩家player
- 输出:玩家player获得的标记数
中间变量:
- my_s:自己的分数
- op_s:对手的分数
- my_c:自己的标记数
- op_c:对手的标记数
- my_has_de:自己是否拥有DE等级的标记的标示
- op_has_de:对手是否拥有DE等级的标记的标示
- my_has_abc:自己是否拥有ABC等级的标记的标示
- op_has_abc:对手是否拥有ABC等级的标记的标示
→ 📖 Q1.4(I) 请说明在这样一个规则判定类模块中,如何避免“漏判”“错判”或分支顺序错误等问题。
我的做法呢,就是把所有可能的输出值(1、-1、0、2)和触发它们的条件先全都列在纸上,做成一张决策表,再对照决策表来一一实现。这样的话就能在编码前就发现之前漏掉的分支。
对于分支顺序,关键点是"立即胜利条件优先于第三小轮判定"——如果先判第三小轮再判立即胜利,就会导致在第一、二轮出现分数 ≥11 时错误地返回 0。我还专门设计了一个测试用例(board=[1,1,1,1,1,0,0], round=1, 预期返回 1)来验证这一顺序没有写错。
另外 Rust 的编译器强制要求 match 穷尽所有分支,这也可以在语言的层面上帮助我们避免漏掉分支。
→ 📖 Q1.5(P) 请说明你们设计了哪些测试用例,这些测试分别覆盖了哪一类规则或边界情况。
我们设计的测试用例如下:
- 一方分值达到 11 分而获胜
- 输入:board=[0, 0, 0, 0, -1, -1, -1],round=2
- 输出:-1
- 一方获得至少 4 枚倾心标记而获胜
- 输入:board=[1, 1, 1, 1, 0, -1, 0],round=1
- 输出:1
- 前两小轮结束时尚未满足胜利条件,应返回 0
- 输入:board=[1, 1, 1, 0, 0, -1, 0],round=1
- 输出:0
- 第三小轮结束时,总分不同,由总分高者获胜
- 输入:board=[1, 1, 1, 0, 0, -1, 0],round=3
- 输出:1
- 第三小轮结束时,总分相同,由最高档位倾心标记判定胜负
- 输入:board=[1, 1, -1, 1, 0, 0, -1],round=3
- 输出:-1
- 第三小轮结束时平局,应返回 2
- 输入:board=[1, -1, 0, 0, 0, 0, 0],round=3
- 输出:2
→ 📖 Q1.6(I) 请说明你对“先写测试再实现”与“先实现再补测试”两种方式的理解。
这次作业我们采用的是先实现再补测试的一个方式。对于规则比较简单、边界条件可以一次性枚举出来的模块,这种方式效率比较高,测试就主要是验证的一个角色。
但我认为对于相对复杂点儿的规则,先写测试更能防止遗漏。在测试驱动开发(TDD)的模式下,先把"期望的行为"用测试用例写清楚之后再去实现这种方式可以在编码阶段就及时地发现规则理解的偏差。例如本题的档位决胜规则如果先写成 6 个测试用例,就可以逼迫自己在开始写代码前把每种情形都想清楚,而不是等写完代码再去想有没有可能漏了什么边界情况。
→ 📖 Q1.7(P) 请记录下目前的时间,并根据实际情况填写 附录A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
2026年4月7日 22:30
→ 📖 Q1.8(I) 请写下本部分的心得体会。
T1 是整个项目中规模最小的模块,然而呢,它却让我意识到"把规则翻译成代码"并不是一件简单的事情。花见小路的胜负规则有多个层次,分支顺序一旦写错就会出现奇怪的判定结果。通过这个模块,我体会到了提前梳理决策树、将逻辑分层实现的重要性。
选择 Rust 实现后,最大的感受是 Rust 的 match 表达式非常适合这种规则判定场景——穷尽模式匹配让编译器帮你检查是否遗漏了分支,&[i8] 切片类型直接对接 wasm-bindgen 的 Int8Array 参数,类型安全在编译期就可以得到有效的保障。
→ 📖 Q2.1(P) 请记录下目前的时间。
2026年4月8日 15:30
→ 📖 Q2.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
- 完成编程任务期间,依次做了什么(比如查阅了什么资料,随后如何进行了开发,遇到了什么问题,又通过什么方式解决);
任务耗时估计
完成任务期间依次做了什么:
- 阅读题目要求,了解具体设计需求。
- 设计代码逻辑。
- 执行具体编码任务。
- 设计测试用例。
→ 📖 Q2.3(P) 请说明针对该任务,你们对 🧑💻 T1 中已实现的代码进行了哪些复用和修改。
T2的任务要求和T1重复度较低,我们没有使用T1中已经实现的代码,而是完全重新编写了代码。
→ 📖 Q2.4(I) 请说明在编码实现时,可以采取哪些设计思想、考虑哪些设计冗余,来提高既存代码适应需求变更的能力。
从这次 T2 的开发来看,我认为以下几点可以有效提升代码对需求变更的适应性:
单一职责:将"解析 token"和"分配牌"的逻辑分开来。如果今后行动格式有变化,那就只需修改 token 解析部分,而不影响分配逻辑。
数据驱动而非硬编码:角色分值 [2,2,2,3,3,4,5] 放在一个常量数组 SCORES 中,而不是在代码各处散布奇奇怪怪的 magic number。同时 Rust 的 const 保证编译期还可以保证如果规则改变了,那就只需要修改一处即可。
利用类型系统:Rust 的所有权和借用机制在编译期就能防止多处代码意外修改同一数据,比动态语言中"谁改了我的数组"的运行时 bug 更容易排查。
→ 📖 Q2.5(P) 头脑风暴环节:
我们终于快要开始让程序玩游戏了!请尝试分析:T2 中不带 X 的操作记录比起实际对局多出了多少信息?如果加上 X,也就是失去了这部分信息的话,如何处理对小轮结束后状态的估计?
T2 比实际对局多出的信息:
实际对局中,玩家对对手的密约(1X)和取舍(2XX)的具体牌面是保密的。T2 中这些行动全部公开(如 1D、2GG),因此 T2 的输入比真实对局多出了以下两类信息:
- 对手密约的具体牌型(1 张)
- 对手取舍的具体牌型(2 张)
每小轮最多多出 3 张牌的信息(密约+取舍)。
如果有 X(信息不完整),如何估计结束后状态:
当存在 1X 和 2XX 时,我们无法直接还原双方的确切持牌数,但可以通过以下方式估计:
- 候选集推断:已知总牌堆(A×2, B×2, ..., G×5,共 21 张,减去暗置的 1 张 = 20 张),减去所有已知牌面,剩余即为未知牌的候选集。
- 枚举可能性:对
1X和2XX列出所有候选牌的组合,对每种组合计算可能的得分标记结果,取概率最高或期望最优的状态作为估计。 - 悲观决策:如果以最悲观的情况进行决策,可以对所有可能的 X 作最坏估计(假设对手密约了对我方最不利的牌),以此进行保守决策。
→ 📖 Q2.6(P) 请记录下目前的时间,并根据实际情况填写 附录 A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
2026年4月8日 17:20
→ 📖 Q2.7(I) 请写下本部分的心得体会。
T2 算是三个任务里工程量中等的,其主要难点在于字符串解析。Rust 中将 &str 转为 &[u8] 字节切片后逐字节扫描的方式非常高效——无需正则表达式,无需分配新字符串,所有操作都可以直接在原始字节数组上完成。这种写法的缺点是可读性不如高级语言的 split/slice,但其性能优势在 Wasm 场景下颇有价值。
另外一个收获是:在动手写代码前先手工模拟一遍测试用例非常值得。我花了十几分钟手算预期输出,发现自己对竞争(4)行动的分组理解有误(误以为选出的组是剩余而非被选),通过队友提醒然后自己手算了一遍纠正后才开始编码,避免了走弯路。
→ 📖 Q3.1(P) 请记录下目前的时间。
2026年4月8日 18:30
→ 📖 Q3.2(P) 请在完成任务的同时记录,并在完成任务后整理完善:
- 浏览任务要求,参照 附录A:基于 PSP 2.1 修改的 PSP 表格,估计任务预计耗时;
- 完成编程任务期间,依次做了什么(比如查阅了什么资料,随后如何进行了开发,遇到了什么问题,又通过什么方式解决);
任务耗时估计
完成任务期间依次做了什么:
- 阅读题目要求,了解具体设计需求。
- 讨论要设计什么样的行动策略
- 解耦代码设计,探讨需要设计哪些功能的函数。
- 具体编码实现。
- 设计测试用例。
- 根据测试结果优化代码。
→ 📖 Q3.3(P) 头脑风暴环节:
假设提供更充裕的时间和资源,这个游戏中你能找到的最优策略有可能是什么形式的?进行调研并总结分析。你还可以在任务结束后试着实现(不计分)。
调研后认为,花见小路属于不完美信息双人零和博弈,我们考虑的一种最优策略如下:
对每个可能的行动,通过大量随机对局模拟估计胜率,选择期望胜率最高的行动,我们调查后了解到这个算法被称为蒙特卡洛树搜索(MCTS)。
在此基础上,还可以通过以下辅助策略降低计算量并且提高胜率:
- 对相似的选择进行剪枝,降低计算量。
- 设计简单的打分函数,仅对高质量的动作进行深度模拟。
→ 📖 Q3.4(P) 请说明针对该任务,你们采取了哪些策略来优化决策。具体而言,怎么选择行动类型?选牌如何更优?如何编程实现。
我们采用了基于状态重建的贪心决策逻辑,具体的思路如下:
- 状态重建:复用了T2的
calc_current_state()先把历史回放成一个GameState。其中my_cards记录我方已落场牌,op_cards[0..7)记录对手已知落场牌,op_cards[7]记录对手场上的未知牌数,known_removed/unknown_removed记录弃牌信息,used_actions记录我方已经用过的四种行动,need_response表示当前是否正处在响应别人赠予/竞争的回合。 - 牌面价值评估:
Evaluator为每个场景计算imp(对自己的重要度)和o_imp(对对手的重要度)。价值主要由三部分决定:牌本身的分值SCORES、当前board的归属状态、以及我方可控制总量与对手场上数量的差值。直观上,正在争夺、只差 0 或 1 张的艺伎权重最高;已经明显领先或明显落后的艺伎权重会下降。 - 行动类型选择:基于对场面剩余牌的分布,把当前所有合法的
1/2/3/4动作全生成出来,再逐个算期望分数。1偏向保留高价值牌,2偏向弃掉低价值牌,3会假设对手拿走其视角下最想要的那张牌,4会枚举所有 2+2 分组并比较我方保留与对手获得的价值差。 - 响应选择:如果当前需要响应别人的
3或4。评分时同时考虑“这张牌/这组牌对我有多重要”和“拿走它后对手少赚多少”,对应代码里的RESPONSE_ALPHA和RESPONSE_BETA。
→ 📖 Q3.5(P) 请说明针对该任务,你们对 🧑💻 T2 中已实现的代码进行了哪些复用和修改。
T3 没有直接调用 T2 的源码接口,但延续了 T2 的“按历史回放动作”思路,并在此基础上做了 T3 所需的改造。
- T2 处理的是“一整小轮已经结束”的完整公开历史,而 T3 处理的是“当前时刻的部分历史”。因此这里新增了
need_response、used_actions和pending_offer相关逻辑,用来判断当前究竟是该主动出牌还是该响应别人。 - T2 默认信息是完整的,T3 则必须面对
1X、2XX。因此新增了op_cards[7]、known_removed、unknown_removed等结构,把“隐藏信息推断”变成决策的一部分。 - T2 的目标是复原结束状态;T3 的目标是输出当前最优动作。所以在回放逻辑之外,又增加了
Evaluator、候选动作生成、行为打分和响应策略等模块。 - T2 中设计了针对最终局面进行结算的函数
settle(),但在 T3 中我们没有涉及模拟游戏直到结算的逻辑,因此 T3 中舍弃了这个函数。
→ 📖 Q3.6(I) 请说明在编码实现时,可以采取哪些设计思想、考虑哪些设计冗余,来提高既存代码适应需求变更的能力。
我觉得这版实现里最值得保留的设计思想有以下几条:
- 状态层和策略层分离:
calc_current_state()只负责把历史转成GameState;真正的决策在Evaluator、build_op_scenarios()和choose_*系列函数中完成。之后如果想换成搜索算法,还能复用整套状态层。 - 把隐藏信息显式建模:没有把所有“不确定”揉成一个模糊变量,而是区分成“对手场上的未知牌”“弃掉的未知牌”“已知弃牌”。这样以后无论是改推断方法还是接入更多观测信息,都只需要改局部逻辑。
- 枚举与评分分离:
generate_*_candidates()只负责列出合法动作,score_*只负责定义好坏,choose_*只负责比较期望值。这样做便于单独调试,也便于未来直接加入剪枝、缓存或搜索。 - 参数集中管理:
RESPONSE_ALPHA、RESPONSE_BETA、KEEP_BONUS这种常量集中地定义后,后续调参不会再牵连函数结构。如果还要继续迭代,可以再把board_factor和contest_factor也进一步配置化。 - 保持决策确定性:平分时按字典序选动作,这看起来是个小细节,但它能让对战回放和 AB 测试更稳定,排查问题时也更容易复现。
→ 📖 Q3.7(P) 请说明你们如何量度所实现的程序模块的有效性,例如:“如何说明我们的程序模块决策能力很强?”,尝试提出一些可能的定量分析方式或测试方式。
我们使用 ai 快速实现了一个简单的随机出牌的 T3 算法,并且修改了课程组提供的test.js文件,实现了一个随机生成牌局进行对战的简单测评程序,我们通过让自己的程序和随机出牌算法进行对战的方式来度量我们程序的有效性。
我们模拟了10000场对战,以减少随机性,实验结果表明:
- 在我们实现了基于模拟对局情况的贪心算法后,这个算法对战随机算法的胜率大概是 65 : 35。
- 我们在此基础上添加了对场上未知牌的处理后,胜率来到了70 : 30。
这说明我们设计的程序已经稍微具备了一定的决策能力,并且后续的程序模块的添加也具有一定的作用,
→ 📖 Q3.8(P) 请记录下目前的时间,并根据实际情况填写 附录A:基于 PSP 2.1 修改的 PSP 表格 的“实际耗时”栏目。
2026年4月8日 23:00
→ 📖 Q3.9(I) 请写下本部分的心得体会。
T3 这版给我最大的体会是:在不完美信息博弈里,先把“我们到底知道什么、不知道什么”描述清楚,比一上来设计复杂策略更重要。真正难的不是写 1/2/3/4 四种行动的代码,而是把 1X、2XX、未响应的赠予/竞争以及剩余牌数统一进同一套状态模型里。
把 GameState、OppScenario、ScenarioEval 分开之后,整个程序的层次一下子清楚了很多:先回放历史,再枚举隐藏场景,最后做期望决策。这个分层也让我更确信,后面如果要继续增强,完全可以在现有结构上往信息集搜索或轻量 MCTS 继续推进,而不用再推倒重来。
另外,Rust 在这类数组计数和场景枚举任务里也确实很合适。定长数组让状态表达足够直接,编译期检查也能尽早暴露问题。后续如果继续优化,我会选择优先保留这套“状态重建 + 场景枚举 + 期望评分”的框架,再在外层增加限深搜索。
→ 📖 Q4.1(P) 提供两人在讨论的结对图像资料。

→ 📖 Q4.2(P) 回顾结对的过程,反思有哪些可以提升和改进的地方。
回顾整个结对过程,有以下几点值得改进:
- 沟通节奏:在部分环节(尤其是 T2 的字符串解析设计)我们各自理解了一会儿再讨论,导致两人分别在脑子里形成了不同的方案,后来合并时花了额外时间统一。如果遇到稍复杂的逻辑就立刻一起在纸上画出来,效率会更高。
- 驾驶员/导航员分工:有时导航员的建议没有被充分采纳就直接跳过了,这违背了结对编程的初衷。应该在继续之前明确确认两人都理解并同意当前方案。
- commit 频率:实际编码期间有几次实现了一段功能后忘记 commit,导致某次代码跑不通时回滚麻烦。应当养成"每完成一个可测试的小功能就 commit"的习惯。
→ 📖 Q4.3(I) 锐评一下你的搭档!并请至少列出三个优点和一个缺点。
优点:
- 逻辑思维清晰,能快速厘清规则,为我讲解我难以理解的一些条件
- 责任心强,有担当,每次讨论前都认真预习了相关文档,独揽了代码commit和事件记录的工作。
- 有强大的创新性,能提出很多独创性的想法和策略,让人眼前一亮。
也不能叫做缺点吧,只是横看成岭侧成峰。对自己的要求过于严格,追求完美、纯洁性和独创性,但出发点绝对是向着更高、更强、更卓越。
→ 📖 Q4.4(I) 说明结对编程的优缺点、你对结对编程的理解。
优点:
- 错误发现率高,"两双眼睛"的效果显著
- 两人讨论能快速碰撞出更好的设计方案,避免一个人钻死胡同
- 互相督促,工作状态更集中,不容易分心
缺点:
- 时间协调成本高,两人必须同时有时间
- 对"导航员"的要求较高,如果导航员没有跟上思路,沟通会产生阻碍
- 一些简单机械的任务由两人完成效率反而不如单人
对于本次结对编程,我的体验是蛮好的,针对方向性的问题,我们可以共同确定前进的方向,提出很多自己的想法,能够有效降低走弯路的概率,形成互补。而到了细节层面,我们即通过这次合作给彼此提供了一个学习rust的好机会同时相互交流了对AI的使用经验和感受,为之后让AI为我们提供更加行之有效的辅助打下了良好的根基。总体而言,结对编程更适合逻辑复杂、容易出错的模块,这可以让这把大砍刀发挥更大的威力。
→ 📖 Q4.5(P) 请提供你们完成代码实现的代码仓库链接。
7fenfen/BUAASE2026-PairPogramming
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/259938.html