<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>自动化测试 on 蜂窝</title><link>https://yufenghuang.tech/tags/%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95/</link><description>Recent content in 自动化测试 on 蜂窝</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><managingEditor>yufenghuang009@gmail.com (Evan Wong)</managingEditor><webMaster>yufenghuang009@gmail.com (Evan Wong)</webMaster><copyright>EvanWong</copyright><lastBuildDate>Mon, 12 Oct 2026 10:10:00 +0800</lastBuildDate><atom:link href="https://yufenghuang.tech/tags/%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95/index.xml" rel="self" type="application/rss+xml"/><item><title>HeirCut 开发手记（八）：让机器把整局玩一遍</title><link>https://yufenghuang.tech/p/heircut-devlog-playthrough-automation/</link><pubDate>Mon, 12 Oct 2026 10:10:00 +0800</pubDate><author>yufenghuang009@gmail.com (Evan Wong)</author><guid>https://yufenghuang.tech/p/heircut-devlog-playthrough-automation/</guid><description>&lt;blockquote&gt;
&lt;p&gt;「HeirCut 开发手记」第八篇。游戏简介见&lt;a class="link" href="https://yufenghuang.tech/p/heircut-devlog-submit-guidance/" &gt;第一篇&lt;/a&gt;文末。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;《宫廷理发师》有三天、九组客人、五个结局。换词让剧情不断分叉：第一天国王怎么选，会影响第二天公主想不想逃；第三天的二公主，要用第一天从公主那里收来的词。&lt;/p&gt;
&lt;p&gt;这意味着改一处内容，可能在两天之后的某个结局上出问题。靠人从头玩一遍，一个结局就要很久，五个结局、每条分支都走到，几乎不可能每次改动都做。&lt;/p&gt;
&lt;p&gt;在做自动化之前，每次发布前全靠手动测：我先尽量把每个结局都通关一遍，确认能正常走完，再把最新版本发到开发群里，团队里的其他人也会主动去玩、去找问题。&lt;/p&gt;
&lt;p&gt;我们最后用了两样东西：一个在纯逻辑层把整局穷举一遍的&lt;strong&gt;推演器&lt;/strong&gt;，和一个在真实场景里从新游戏玩到结局的&lt;strong&gt;机器人&lt;/strong&gt;。前者回答「哪些内容理论上能到达」，后者回答「玩家真的能玩到」。&lt;/p&gt;
&lt;h2 id="推演器在纯逻辑层把整局穷举一遍"&gt;&lt;a href="#%e6%8e%a8%e6%bc%94%e5%99%a8%e5%9c%a8%e7%ba%af%e9%80%bb%e8%be%91%e5%b1%82%e6%8a%8a%e6%95%b4%e5%b1%80%e7%a9%b7%e4%b8%be%e4%b8%80%e9%81%8d" class="header-anchor"&gt;&lt;/a&gt;推演器：在纯逻辑层把整局穷举一遍
&lt;/h2&gt;&lt;p&gt;这件事能做，是因为剧情已经&lt;a class="link" href="https://yufenghuang.tech/p/heircut-devlog-encounter-refactor/" &gt;变成了纯逻辑&lt;/a&gt;：推进一组客人，就是不断给会话发命令。推演器直接用游戏里的同一套会话逻辑，不需要 Unity。&lt;/p&gt;
&lt;h3 id="怎么穷举"&gt;&lt;a href="#%e6%80%8e%e4%b9%88%e7%a9%b7%e4%b8%be" class="header-anchor"&gt;&lt;/a&gt;怎么穷举
&lt;/h3&gt;&lt;p&gt;推演器按天、按客人组往前走。在一组客人内部：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;状态&lt;/strong&gt;由两部分组成：会话的快照（在哪个阶段、哪段演出、哪些事实），加上玩家手里的东西（收集了哪些词、哪些词打过勾、手里和给出去的道具）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每一步&lt;/strong&gt;，列出当前阶段能被接受的所有命令：推进对话、把每个合法的词换进每个槽（外加一个「不会被接住的错词」）、递每一件道具、点每个热点、理发成功或失败、摇铃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;相同的状态只走一次&lt;/strong&gt;。一组客人结束时，按「玩家手里的东西」归并，同样的结果只保留最短的那条路，作为下一组的起点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整局穷举下来大约一分钟。&lt;/p&gt;
&lt;h3 id="它告诉我们什么"&gt;&lt;a href="#%e5%ae%83%e5%91%8a%e8%af%89%e6%88%91%e4%bb%ac%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;它告诉我们什么
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;五个结局是否都能到达&lt;/strong&gt;，以及每个结局最短的一条路线；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;哪些台词、哪些规则永远到不了&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一组覆盖路线&lt;/strong&gt;：每一轮选出「能新覆盖最多台词和规则」的一整局，重复到再也没有新增为止。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最近一次的结果是：316 句台词里 303 句可达，113 条规则全部可达，18 条路线就能把它们全部覆盖。到不了的 13 句都是需求分析失败时的反馈：其中 11 句在会话之外播放，推演器本来就看不到，机器人在实机里都见到了；另外 2 句在教程里，按设计就不会出现。&lt;/p&gt;
&lt;p&gt;有一个细节值得记一笔：&lt;strong&gt;有台词到不了时，推演器的退出码仍然是 0。&lt;/strong&gt; 因为「到不了」不一定是错误，可能是故意的教程分支。所以每次跑完都要拿新的清单和上一次的比，而不是只看它有没有报错。&lt;/p&gt;
&lt;p&gt;推演器也有一个建模上的简化：一句台词里所有能剪的词，它都当作被收集了。这样「收集了哪些词」总是一个超集，覆盖范围只会被高估、不会被低估——但这也意味着它不能回答「玩家漏剪了一个词会怎样」。&lt;/p&gt;
&lt;h2 id="机器人在真实场景里玩到结局"&gt;&lt;a href="#%e6%9c%ba%e5%99%a8%e4%ba%ba%e5%9c%a8%e7%9c%9f%e5%ae%9e%e5%9c%ba%e6%99%af%e9%87%8c%e7%8e%a9%e5%88%b0%e7%bb%93%e5%b1%80" class="header-anchor"&gt;&lt;/a&gt;机器人：在真实场景里玩到结局
&lt;/h2&gt;&lt;p&gt;推演器跑在逻辑层，看不见界面：词有没有显示出来、按钮被没被挡住、动画有没有卡住。这些只能在真实的游戏里验证。&lt;/p&gt;
&lt;p&gt;机器人就是一个在 Unity 里玩游戏的脚本。它的原则是&lt;strong&gt;走玩家走的路&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;点击和拖拽都走真实的界面射线检测。被遮住、没激活的东西，它和玩家一样点不到。&lt;/li&gt;
&lt;li&gt;推演器给出的路线&lt;strong&gt;只提供选择&lt;/strong&gt;：换哪个词、递哪件道具、理发成功还是失败。收集词、推进对话、翻笔记本、摇铃，都是它看着画面做出的反应。&lt;/li&gt;
&lt;li&gt;每一步之间隔一小段时间。连续 90 秒没有任何进展，就判失败，并把当时的现场状态一起记下来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了走到结局，它沿路还要做两件核对：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;台词对账&lt;/strong&gt;：推演器预计这条路线会出现的台词，是否全部出现了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;归档核对&lt;/strong&gt;：每收集一个词，检查它是否进了正确的笔记本页。这条核对&lt;strong&gt;刻意没有用游戏里决定归档的那个函数&lt;/strong&gt;——用同一个函数去检查它自己，两者一起错的时候，它也会说「对」。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;跑之前，机器人会把玩家的存档挪到一边，跑完再放回去。&lt;/p&gt;
&lt;p&gt;验收分成四类：五个结局各走一遍；覆盖路线全部走一遍；九组客人各自中途存档、退出、继续，核对回到的是不是同一句；笔记本回溯（包括跨天）之后重玩。&lt;/p&gt;
&lt;h3 id="成本很不均匀"&gt;&lt;a href="#%e6%88%90%e6%9c%ac%e5%be%88%e4%b8%8d%e5%9d%87%e5%8c%80" class="header-anchor"&gt;&lt;/a&gt;成本很不均匀
&lt;/h3&gt;&lt;p&gt;覆盖路线的收益极不均匀。前 4 条大约 47 分钟，就能覆盖 93% 的内容；剩下的十几条，每条只补几句台词或几条规则，全部跑完要三个半小时左右。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://yufenghuang.tech/p/heircut-devlog-playthrough-automation/bot-coverage.svg"
loading="lazy"
alt="一次跑满 19 条覆盖路线的机器人批次：第 4 条跑完时已经见到 314 句里的 301 句，用时 47 分钟；第 9 条之后不再出现新台词；全部跑完用了 3.6 小时"
&gt;&lt;/p&gt;
&lt;p&gt;所以日常只跑前几条，大改之后才跑全量；全量跑的时候也可以随时让它「跑完当前这一局就停」。&lt;/p&gt;
&lt;h2 id="它们抓到了什么"&gt;&lt;a href="#%e5%ae%83%e4%bb%ac%e6%8a%93%e5%88%b0%e4%ba%86%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;它们抓到了什么
&lt;/h2&gt;&lt;p&gt;自动化最直接的好处是省时间：批次排好就可以完全放着不管，回来看报告。而它抓到的，往往是人工测试很难碰到的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;台词变透明。&lt;/strong&gt; 卷轴「收起」的动画还没播完，下一句的「展开」就开始了，两段动画互相打架，结果卷轴处于「显示中」，却是完全透明的。只有在打开「跳过理发」、并且走到某一组客人的某一句时才必现，人工几乎不可能碰到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;词被放回了一页不存在的笔记本。&lt;/strong&gt; 第三天的二公主要复用第一天公主的需求词；把词从合成板撤回时，代码按「当天 + 当前客人」算页，算出了一页根本不存在的「第三天公主」。这是归档核对加上之后，第一次跑覆盖路线就抓到的。之前从没红过，是因为最常跑的那条路线正好不经过这段复用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败的理发播到一半时存档，读不回来。&lt;/strong&gt; 这是编辑器试玩后端的随机自测撞出来的：随机玩几百局，在各种时刻存档再恢复。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一批永远到不了的内容。&lt;/strong&gt; 推演器列出的「到不了」的台词和规则，拿去和策划逐条确认，删掉了几条早就作废的线索链和几句废弃台词，台词从 322 句减到 316 句，规则从 127 条减到 123 条，剩下的全部可达。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="两个工具互相纠错"&gt;&lt;a href="#%e4%b8%a4%e4%b8%aa%e5%b7%a5%e5%85%b7%e4%ba%92%e7%9b%b8%e7%ba%a0%e9%94%99" class="header-anchor"&gt;&lt;/a&gt;两个工具互相纠错
&lt;/h2&gt;&lt;p&gt;推演器和机器人对游戏各有一套理解，它们不一致的地方，往往就是问题所在——有时是游戏的问题，有时是工具自己的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;推演器排出了玩家走不通的路线。&lt;/strong&gt; 会话只按规则匹配命令，而「这个槽、这句台词现在在不在屏幕上」，是游戏里的桥在把关。推演器一开始没模拟这一层，于是排出了「去换一个根本没显示出来的槽」「新的一句还没答完就摇铃」这样的路线，机器人一跑就卡住。之后推演器补上了这些界面层的条件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;推演器把能到的内容判成了到不了。&lt;/strong&gt; 它要求「某条规则点名的那个词，必须是一个会打勾的候选」，可游戏里的桥只按「槽 + 词」来匹配，和打不打勾无关。修正之前，有 9 条规则、22 句台词被误判为不可达。&lt;/p&gt;
&lt;p&gt;推演器说「到得了」，和玩家「玩得到」，原本应该是同一件事。每次两边对不上，我们都要先问清楚：错的是游戏，还是工具对游戏的理解。&lt;/p&gt;
&lt;h2 id="机器人自己踩的坑"&gt;&lt;a href="#%e6%9c%ba%e5%99%a8%e4%ba%ba%e8%87%aa%e5%b7%b1%e8%b8%a9%e7%9a%84%e5%9d%91" class="header-anchor"&gt;&lt;/a&gt;机器人自己踩的坑
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编辑器失去焦点时，Unity 不会正常推进。&lt;/strong&gt; 同一份代码，一次在主菜单卡住、一次在第三天递道具时卡住，保持编辑器在前台就全部通过。这类失败看起来非常像游戏缺陷。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有人按了 Esc。&lt;/strong&gt; 按 Esc 会打开暂停菜单，机器人卡在某一组客人的理发上，这个「卡住」被当成偶发的游戏缺陷跟了一整天。现在机器人只要看到不是自己打开的菜单，就当场判失败，并写明原因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;探针把游戏弄坏了。&lt;/strong&gt; 为了观察状态，写了一个每帧读取管理器单例的探针。场景还没加载完时，读单例会新建一个空实例，真正的实例随后被销毁，机器人在第一天结束时报错。第一次失败的原因，是我们自己加的探针。之后编辑器工具里观察状态，一律用「查找已存在的对象」，不碰单例。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="还没解决的"&gt;&lt;a href="#%e8%bf%98%e6%b2%a1%e8%a7%a3%e5%86%b3%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;还没解决的
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机器人不看画面。&lt;/strong&gt; 它能确认流程走得通、台词都出现了，但看不出「这里的动画很别扭」「这个按钮太难找」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;理发是跳过的。&lt;/strong&gt; 要高分时，机器人打开「跳过理发」的辅助开关，所以剪发本身的手感不在它的验证范围内。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全量覆盖太贵&lt;/strong&gt;，不能每次改动都跑。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="回头看"&gt;&lt;a href="#%e5%9b%9e%e5%a4%b4%e7%9c%8b" class="header-anchor"&gt;&lt;/a&gt;回头看
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;整局验证能做，是因为剧情是数据。&lt;/strong&gt; 推演器直接复用游戏的会话逻辑，这件事在状态机时代做不到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两层验证要回答两个不同的问题。&lt;/strong&gt; 推演器回答「理论上能不能到」，机器人回答「玩家能不能玩到」；两者的分歧本身就是信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查不要用被检查的东西去检查。&lt;/strong&gt; 归档核对如果用了决定归档的那个函数，它永远会同意。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动化的失败，先排除自动化本身。&lt;/strong&gt; 失焦、外部按键、自己的探针——这些都长得很像游戏缺陷。&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>