自动测试 ChoiceScript 游戏
https://www.choiceofgames.com/make-your-own-games/testing-choicescript-games-automatically/
用 ChoiceScript 编写的互动小说测试起来可能非常困难,因为每个 *choice 都可能产生需要测试的游戏新变体。手动测试时,很难确定某些选择路径是否会导致游戏崩溃,或者故事的某些重要部分是否根本无法被触及。
为此,我们开发了两个工具,可以显著简化ChoiceScript游戏的调试过程。
Randomtest(随机测试)
随机测试工具会自动反复运行你的游戏,为每个选择做出随机决定,并生成一份“命中次数”报告,告诉你每行代码在随机游戏过程中被使用的次数。
Quicktest(快速测试)
快速测试:与随机运行代码不同,快速测试会尝试测试每个
*choice中的每个#option,以及每个*if语句的两个分支。为了快速完成测试,快速测试会“作弊”——跳过已经测试过的行。
Keyboard Shortcuts(键盘快捷键)
在手动玩游戏时,您可以使用键盘快捷键点击“下一步”按钮,这样您就可以通过敲击键盘快速将游戏从头玩到尾。
所有类型的测试都是必要的。快速测试可以发现一些随机测试无法发现的错误,反之亦然。遗憾的是,快速测试也可能会发现一些“虚假”错误——这些是代码风格上的问题,虽然可能引发问题,但实际上对真实玩家并无害处。当然,任何测试都无法真正替代手动测试!
键盘快捷键
ChoiceScript 中的“Next”按钮设有浏览器访问键。实际快捷键取决于您使用的是 Windows 还是 macOS 系统。
截至 2020 年 9 月,Chrome、Firefox 和 Safari 浏览器中“Next”按钮的快捷键设置如下:
Windows:
Alt-Shift-N用于“Next”,Alt-Shift-数字键用于选择选项macOS:
Control-Option-N用于“下一步”,Control-Option-数字键用于选择选项(注意此快捷键使用 Control 键^,而非 Command 键⌘)
这些快捷键允许您按住 Alt+Shift 或 Control+Option,然后反复按 N 键来快速推进游戏。操作时,您可能希望点击屏幕顶部的“设置”按钮并选择“页面间无动画”,以使游戏运行得更快一些。
运行测试
If you followed the ChoiceScript introduction, you’ll see two “Tests” links at the bottom of the page, for “Quicktest” and “Randomtest.” When you click the “Quicktest” link, Quicktest will run immediately. When you click the “Randomtest” link, it will ask you a few questions. (See Randomtest Options, below.) 如果你按照 ChoiceScript 入门指南操作,就会在页面底部看到两个"测试"链接,分别是"Quicktest(快速测试)"和"Randomtest(随机测试)"。点击"快速测试"链接会立即运行测试;点击"随机测试"链接则会弹出几个选项问题(详见下文"随机测试选项")。
你也可以通过双击 ChoiceScript 文件夹中的文件来运行测试(但查找和使用这些文件的操作比直接点击浏览器链接稍显复杂)。
在Windows系统中,你可以通过双击附带的 run-randomtest.bat 和 run-quicktest.bat 文件来运行测试(若 Windows SmartScreen 防护程序弹出警告,请点击"更多信息"后选择"仍要运行")。
在 Mac 系统中,你需要使用 randomtest.command 和 quicktest.command 文件(若出现"无法打开,因为开发者无法验证"的提示,请按住 Control 键点击文件,然后选择"打开")。
快速测试:解读错误信息
如果快速测试通过,控制台会显示“QUICKTEST PASSED”。如果未通过,则会打印出错误信息。
在这个例子中,我在 ChoiceScript 文件的第16行添加了一个错误的代码行 *oops,以上是打印出的错误信息。
如果快速测试(或 ChoiceScript)给出的错误信息让你无法理解,欢迎在我们的论坛上向我们提问。请务必不仅发布错误信息,还要提供导致问题的完整代码文件。如果你不想在群组中分享那么多代码,也可以将代码发送至 support@choiceofgames.com;我们会尽力为你提供帮助。
检测死代码
但即使快速测试通过,它仍可能报告一些未测试的行,例如:
如果快速测试显示“SOME LINES UNTESTED(某些行未测试)”,这意味着快速测试认为这些行完全无法访问。这些行是“死代码”,任何玩家都无法看到。
例如,这里有一种在未察觉的情况下引入死代码的方法。我们以这样的代码开始:
但后来我们意识到我们也想降低 strength 力量值。我们愚蠢地直接在末尾加了一行,就像这样:
那行 *set strength %-15 就是死代码;它永远无法被执行,因为它紧接在 *goto 之后。
当 Quicktest 发现死代码时,你应该修复这个问题,要么删除代码,要么修复导致代码失效的错误。在这个例子中,我们可能只需要把 *goto 那一行移到最后。
请注意,Quicktest 并不能保证找到所有死代码……由于 Quicktest “作弊”的方式,它有时能执行到正常人类玩家无法触及的代码。使用 Randomtest 来查找此系统中的其他一些死代码。
快速测试中的假错误
太长不看版:你可能需要将一些 *if 语句改为 *else 语句,才能使快速测试通过。
快速测试会像普通玩家一样自动运行代码,但当遇到 *choice 语句或 *if 语句时,快速测试会创建自身的多个副本并尝试运行它们。(这些副本会依次运行;例如,我们会先测试 *if 语句的 true 行,然后再测试 *else 行。)
为了节省时间,快速测试会“作弊”。如果快速测试的一个副本验证了某行代码,而之后另一个快速测试的副本又到达了同一行代码,那么第二个副本会退出,因为它假设之前的副本已经完成了该行代码的验证工作。
但 Quicktest 还有另一种“作弊”方式,它会测试 *if 语句的两侧,即使玩家实际上无法到达那些行。这可能导致 Quicktest 识别出“虚假”错误:即在实际游戏中根本不可能发生的漏洞。
例如,ChoiceScript 试图通过要求 *choice 语句中的每个 #option 都以 *goto 或 *finish 结尾来确保正确性。Quicktest 可以帮助你捕捉这类漏洞:
如果 ChoiceScript 允许这样的代码,它将制造一个难以察觉的漏洞;“即使你非常 naughty(淘气),你也仍然能得到出一款电子游戏”。相反,如果你编写这样的代码,ChoiceScript 会崩溃;Quicktest 可以自动检测到崩溃,让你轻松捕获这个漏洞。
你可以像这样修复代码:
But now suppose we included an *if statement in the middle of this *choice. Suppose we have a politics variable, which we set to either “democrat” or “republican”. Then we might write code like this:
但现在假设我们在这个*choice 中间加入了一个 *if 语句。假设我们有一个 politics (政治倾向)变量,我们将其设置为“democrat(民主党)”或“republican(共和党)”。那么我们可能会编写如下代码:
这段代码存在一个漏洞,但在现实生活中可能永远不会发生:如果 politics 既不是"democrat(民主党)"也不是"republican(共和党)"呢?
如果一位政治独立人士游玩示例3,游戏将会崩溃,并出现与示例1相同的错误;Quicktest 会自动检测到这种情况。
其原理如下:在第一个 *if 语句处,Quicktest 会创建自身的副本:它首先创建一个 politics = "democrat" 的副本,随后创建另一个 politics != "democrat" 的副本;这个非民主党副本又会自我复制,生成 politics != "republican" 的副本和政治立场≠"共和党"的副本。
在最终生成的副本中,Quicktest 测试了政治立场既非"democrat"也非"republican"的情况;由于该情况下不存在*goto 或 *finish 指令,Quicktest 会因错误而崩溃。
现在,在你的游戏中,可能实际上并不存在其他政党。但 Quicktest 无法确切知道这一点,所以即使这个 bug 在现实生活中发生的概率为 0%,Quicktest 仍会指出示例3存在缺陷。
你可以通过 *else 语句来修复示例3,这有助于 Quicktest 理解在这种情况下只有两种可能性:
在这里,Quicktest 只会创建两个副本:一个副本中 politics = "democrat",另一个副本中 poliics != "democrat"。然后 *else 语句确保了不存在其他可能性。
你也可以使用下面介绍的 *bug 命令来修复这些“虚假”的 Quicktes t失败。
*bug 命令
*bug 命令ChoiceScript 包含一个 *bug 命令,它会导致游戏以特定消息崩溃。
*bug 命令在与 Randomtest 配合使用时尤其有用,后者能提供触发 *bug 的确切步骤序列。
请注意,如果用户在正常游戏过程中实际遇到 *bug,游戏将停止并显示错误消息,因此您需要运行 Randomtest 以确保 *bug 行永远无法被执行。
您可能会惊讶地发现,Quicktest 会忽略 *bug 命令。这是由于 Quicktest 的“作弊”机制所必需的。Quicktest 总是会执行每个 *if 语句的两个分支,无论哪个分支实际为真。因此 Quicktest 必然会以某种方式执行 *bug 命令。当它执行时,那个偏离轨道的 Quicktest 副本只会停止运行,而不会报告错误。
这意味着你可以用 *bug 语句来修复上一节中的示例3,就像这样:
当既非民主党也非共和党的 Quicktest 副本遇到 *bug 行时,Quicktest 会停止并忽略该 *bug。当然,这并不能证明该 bug 在实际游戏中不会真正出现;如果游戏包含除"民主党"或"共和党"之外的其他政治立场,那么我们就需要运行 Randomtest 来自动报告 *bug 错误。
因此,更稳妥的做法可能是像上文示例 4 那样使用 *else 来修复 Quicktest 的"假bug",因为这能保证代码不会在实际用户使用时崩溃。
随机测试选项
Iterations(迭代次数)
启动 Randomtest 时,它会询问您希望运行多少次测试。我们建议运行 10,000 次或更多次以确保充分的测试覆盖率。
Starting seed number(起始种子数)
注意:Randomtest 并非真正随机。实际上,Randomtest 的输出是完全确定性的;如果你连续运行两次,两次会得到完全相同的结果。这让很多人感到意外。(具体来说,Randomtest 使用的是伪随机数生成器,并带有硬编码的“种子”值。)
这样做有一个很大的优势:如果 Randomtest 因错误而失败,你再次运行它时,它会以相同的错误再次失败。如果你修复了一个错误,并且 Randomtest 通过了,你可以确信你已经解决了那个问题。
因此,如果你想用 Randomtest 运行 30,000 次随机游戏流程,仅仅运行三次 Randomtest 是行不通的;你只会重复玩相同的 10,000 种变体。相反,你需要运行 30,000 次迭代,或者至少在每次运行时将“起始种子数”设置为“10000”或“20000”。
Avoid used options(避免使用过的选项)
在此模式下,当随机测试遇到之前见过的 *choice 时,它会统计每个选项被使用的次数,并选择测试次数最少的选项,仅在出现平局时随机选择。
避免使用过的选项随机性较低,但通常能更快地发现漏洞。一个缺点是,有些漏洞只有在连续多次选择同一选项时才会出现,而随机测试在避免使用过的选项时永远不会这样做。
Show full text during the game(游戏中显示完整文本)
游戏中显示完整文本:随机测试日志可能相当冗长,如果包含玩家看到的每一个字,日志会变得格外冗长。如果您需要更多细节来帮助查找棘手的漏洞,请开启“显示完整文本”功能。
Show choices selected during the game(游戏中显示已选选项)
如果您的随机测试日志确实非常冗长,可以隐藏随机测试所做的选择列表。
Highlight gender pronouns(高亮显示性别代词)
如果你的游戏包含可变的性别代词,很容易在输入时误将“
${he}”打成“he”。开启高亮显示性别代词并显示完整文本功能后,我们会用不同颜色突出显示代词——当代词是变量(如“${he}”)时以蓝色高亮,当代词是硬编码在文本中(如“he”)时则以红色高亮。
After the test, show how many times each line was used(测试后显示每行代码的使用次数)
测试后显示每行代码的使用次数:测试通过后,Randomtest 可以打印出每行代码被触发的“命中次数”。(详见下文说明。)
Automatically scroll to the bottom(自动滚动至页面底部)
在 Randomtest 运行期间自动将页面滚动到底部。
随机测试游戏日志
Randomtest 可以生成一份庞大的日志,记录其随机尝试的所有操作。
以下是基于 ChoiceScript 随附示例游戏生成的 Randomtest 日志首部分内容示例。
*****Seed 行表示 Randomtest 完成一次游戏流程并重新开始下一次流程的时间点。其他行则详细说明了 Randomtest 在游戏过程中做出的具体选择。
例如,行记录 startup *choice 55#3 (line 83) 表示在文件 startup.txt 的第 55 行有一个 *choice 命令。该记录的其余部分指明了 Randomtest 选择的选项及其行号。在此示例中,Randomtest 选择了第 83 行的选项 #3。在第二次游戏流程中,我们看到 startup *choice 55#1 (line 56) 的记录,这表明 Randomtest 改为选择了第 56 行的选项 #1。
理想情况下,randomtest-output.txt 文件的最后一行会显示"RANDOMTEST PASSED."。若未出现此提示,则文件中包含错误信息;更多详情请参阅前文"解读错误信息"部分。但请注意,您可以通过随机测试日志准确复现故障:只需在错误发生前,完全按照随机测试的操作序列("选项#1、#2、#3、#1、#2……")进行手动选择。通过手动复现随机测试发现的故障,往往能更直观地理解问题根源并实施修复。
随机测试命中次数
Randomtest 可以展示每行代码被使用的次数。每次一行代码被使用,我们称之为该行被"hit(命中)",而记录每行命中次数的报告则被称为"命中计数"。
若选择生成命中计数,这通常是输出结果中最耗时的部分。
例如,这是对 startup.txt 示例运行一万次后生成的命中计数样本:
随机测试运行了 10,000 次迭代,因此你可以看到引言和选项文本显示了整整 10,000 次。共有三个选项(“war(战争)”、“trade(贸易)”和“abdicate(退位)”),每个选项被选中的次数大约各占三分之一:随机测试选中“战争”3,418 次,“贸易”3,278 次,“退位”3,304 次。
在“战争”选项下有三个子选项;这些选项将 3,418 次“战争”选择大致均分为三份:1,133 次、1,132 次和 1,153 次。在“贸易”选项下只有两个子选项;这些选项将 3,279 次“贸易”选择平分为两半:1,601 次和 1,677 次。
如果命中次数报告显示某些代码行被命中了零次,这表明这些代码可能是"死"代码——即无论玩家做出何种选择都无法执行到的代码。然而,零命中次数的代码并不能保证一定是死代码——也可能只是极难触发。
如果发现难以触发的代码,你需要自行判断这是否意味着存在程序错误。例如,在传统的"选择路径"类书籍中,达成"好"结局往往非常困难;大多数结局都是糟糕的结局(例如死亡)。一方面,这或许是件好事,因为它能激励玩家重新尝试;另一方面,不断遭遇坏结局可能会令人沮丧。
再举一个例子,如果你的选项存在"正确"与"错误"之分(比如游戏包含大量解谜环节),随机测试可能会显示完全随机操作通关的概率极低。但这或许正是理想状态——倘若仅凭随机选择就能通关,你的谜题设计可能过于简单了!
解读命中次数报告时,请记住可将命中次数除以 10,000 来获得特定代码行的触发概率。若某行代码命中次数低于 100 次,则随机触发该行的概率不足 1%。若你认为该概率过低,可以考虑调整游戏平衡性,让更多玩家能够触及这段剧情分支。
强制随机测试做出特定选择
Randomtest 可能与真实玩家的行为大相径庭;有时,强制 Randomtest 做出特定选择,点击次数反而更具参考价值。
有一个特殊变量 choice_randomtest,仅当游戏在 Randomtest 模式下运行时其值为真。这让你可以编写如下代码:
Randomtest 不允许运行 *restore_checkpoint
*restore_checkpoint如果 Randomtest 运行了 *restore_checkpoint,它只会陷入循环,永远无法完成。
如果 Randomtest 随机遇到 *restore_checkpoint 命令,它将因错误而失败。你可以像这样阻止它运行 *restore_checkpoint:
有问题吗?
若对本文件有疑问,请在 ChoiceScript 论坛发帖咨询。
最后更新于