模拟试玩与人工玩家
Simulated Playtests & Artificial Players
简单的赌博游戏(21 点、轮盘)可以直接算胜率;复杂而带随机的游戏必须玩很多次才知道是否平衡。Machinations 工具提供人工玩家 (Artificial Player, AP):用简单脚本代替人点击节点,配合快速运行与批量运行收集上千局统计。Adams & Dormans 用它做了两个完整案例:给两人版《大富翁》加入房屋与房产税(动态摩擦)来遏制动态引擎;给 Will Wright 的假想 RTS SimWar 调参,使"乌龟流"与"速攻流"从 929:68 调到 500:500。核心纪律:人工玩家要做成始终坚持一种策略的漫画式角色;调机制,不调人工玩家;一次只改一处并先大幅改动。
人工玩家与脚本(第 8 章 Simulated Play Tests,印刷页约 p.171–174)
人工玩家在图中是一个写着 AP 的小方块,不与任何节点相连;有颜色、线宽、标签和激活模式(同其他节点,见 Machinations 图示语言),可以激活任何有名字的节点(不限于交互节点)。作者注明脚本远不如关卡编辑器的脚本语言强大,不需要会编程。
执行语义:AP 触发时从上到下逐行求值;直接命令立即执行;if(条件) 命令 在条件为真时执行;任何命令一旦执行,本次脚本即停止。因此两条直接命令连写时第二条永远不会执行,也不需要 else——一串 if 就是"第一个为真的分支"。下次触发再从头求值。对程序员来说,它相当于一个每次触发都从头扫描的优先级规则表 (Priority Rule List),类似行为树中无记忆的 Selector(见 状态模式与有限状态机)。
| 命令 | 语义 |
|---|---|
fire(node) | 触发标签匹配的节点(大小写敏感);无参数或无匹配时脚本终止、不触发 |
fireAll(n1,n2,…) | 同时触发全部列出节点 |
fireSequence(n1,n2,…) | 每次执行轮到下一个,循环;如 fireSequence(Produce, Produce, Upgrade, Produce, Consume) |
fireRandom(n1,n2,…) | 随机选一个;重复写名字提高权重,fireRandom(Produce, Produce, Consume, Upgrade) 为 50%/25%/25% |
stopDiagram(message) | 立即停止图(类似终止条件);批量运行时统计每条消息出现次数 |
endTurn() | 结束回合制图的当前回合 |
activate(ap) / deactivate() | 停用自己并激活另一个 AP / 停用自己 |
- 颜色编码的图中,AP 只能触发与自己同色的节点。回合制图中,AP 每回合有若干"行动数",多个行动按一秒间隔执行。
表 8.1 脚本条件中的运算符(转写自原书图片表)
| 运算符 | 含义 | 示例 |
|---|---|---|
== | 相等 | if(Resources == 1) fire(Consume) |
!= | 不等 | if(Upgrades != 0) fire(Upgrade) |
<= < >= > | 关系 | if(Resources <= 3) fire(Produce) |
+ - | 加减 | if(Upgrades + 2 < Resources - 1) fire(Produce) |
* / % | 乘除取模 | if(Upgrades * 2 > Resources / 3) fire(Upgrade);if(Resources % 4 == 2) fire(Upgrade) 在资源为 2、6、10… 时触发 |
&& | 逻辑与 | if(Resources > 4 && Upgrades < 2) fire(Upgrade) |
|| | 逻辑或 | if(Resources > 6 || Upgrades > 2) fire(Consume) |
特殊值
| 值 | 含义 |
|---|---|
random | 每次求值生成 [0,1) 随机实数;if(random < 0.25) fire(A) 即 25% 概率;if(random * 100 > Resources) fire(Produce) 使触发概率随资源增多线性下降,资源到 100 时永不触发 |
pregen0…pregen9 | 10 个 [0,1) 随机值,只在图启动时生成一次,整局不变——用来让同一脚本每局略有不同的"性格" |
actions | AP 已触发的次数 |
steps | 图已执行的时间步数 |
actionsOfCommand | 该 if 后命令已执行的次数 |
actionsPerStep | 本时间步内 AP 已触发的次数 |
例:if(Resources>4) fireRandom(Consume, Upgrade) 后接 fire(Produce)——先攒到 5 个资源,再随机消费或升级。
批量运行与数据收集(第 8 章 Collecting Data from Multiple Runs,印刷页约 p.174)
- 快速运行 (Quick Run):极快执行、不可交互;必须有能到达的终止条件,否则手动停止。
- 批量运行 (Multiple Runs):默认 100 次可改;单次超过 10,000 时间步未到终止条件自动停止;统计各终止条件到达次数(两人各有胜利条件即可直接得胜率)与平均用时(比较哪个脚本最快把经济推到目标状态)。
- 图表保存每次运行的数据,默认显示最近 25 次(当前运行高亮),可翻页、清除、导出 CSV 到表格或统计软件。
- 本书前几章给出的胜率数字就是这样从上千次模拟中得来的。
这本质上是对游戏经济的蒙特卡洛模拟 (Monte Carlo Simulation):胜率估计的标准误约为 ,、 时约 1.6 个百分点,所以下文表格中相差几十局的行之间才有意义(这是本 wiki 的补充,不是原书内容;随机源见 伪随机数生成)。
设计人工玩家策略(第 8 章 Designing Artificial Player Strategies,印刷页约 p.174–175)
- AP 不能代替真人,也写不出聪明的 AI;目的是测试简单策略以观察机制如何运转。最好把 AP 设计成漫画式角色:无论后果如何都坚持一种策略。例如想知道 RTS 最佳兵种组合,就写几个分别造不同组合的 AP 互打。AP 也可设为被动/自动,运行中点击切换,方便快速切换对手。
- 写对 AP 策略常要试好几次,这本身就是探索设计的好办法;理想情况是能找到很多有效策略。若能写出一个总是打败其他 AP(或你自己)的 AP,多半找到了优势策略,应修改机制降低其效果。
- 去除所有随机以暴露优势策略(随机会掩盖产生优势策略的模式,见 概率与期望值):百分比随机用均值分数代替(20% 概率的源→固定产率 0.2,即每 10 步 2 个);多骰子分布不均匀,建议用均值
脚本中的 fireRandom() 换成顺序版本,random 条件换成基于 steps 的周期条件,如 if(random < 0.3) fireRandom(A, B, B, C) → if(steps % 10 < 3) fireSequential(A, B, B, C)。连接标签和脚本都去随机后,每次运行结果相同,适合判断一种策略是否真的优于另一种。
- 链接人工玩家:长脚本可拆给多个 AP,一个 AP 触发另一个(被设为交互的)AP,如 builder、attacker,再用第三个 AP
fireRandom(builder, attacker)随机选择。 - 警告:如果你得花大量时间才能写出能控制住经济的 AP,通常说明经济本身有问题。AP 不应聪明到能赢下一个对玩家不公平的游戏——它们是为了暴露问题而非掩盖问题。调你的机制,不要调你的人工玩家 (Tune your mechanics, not your artificial players)。
案例一:两人版《大富翁》(第 8 章 Playing with Monopoly,印刷页约 p.175–187)
模型与前几章不同:两人对战,收租/付租由两个在玩家间转移金钱的门实现——每块地产给对方每时间步 4% 概率付 1 单位钱;可用地产只有 20 块(Available 池),买完即止。两个 AP 的脚本都是:
if((random * 10 < 1) && (Money > 4 + steps * 0.04)) fire(Buy)
即每步 10% 机会买地,但要保留随时间增长的最低存款(初始 >4)——因为对方地产越多,连续付租的可能越大,储备要越来越多。
| 实验 | 设置 | 观察 |
|---|---|---|
| 基线(图 8.5/8.6) | 两个相同 AP、带随机 | 前约 90 步双方金钱大致平稳,之后逐渐上升——典型的动态引擎正反馈(见 机制设计模式);趋势线为作者手绘示意 |
| 去随机(图 8.7/8.8) | steps % 10 < 1 代替 random * 10 < 1 | 趋势非常清晰;两条曲线完全重合 |
| 运气差(图 8.9) | 红方 steps % 20 < 1、0.02 | 红方买地机会减半;金钱曲线起初差别不大,但地产数才是真正的实力指标 |
| 随机化经过起点(图 8.10) | 产率恢复为 ,即每 4–8 回合得 2 | 影响不大,不改变主趋势——经过起点对游戏影响不显著 |
| 随机化收租(图 8.11) | — | 影响更大,但不破坏总趋势(有时红方钱少而买地更少) |
| 随机化买地机会(图 8.12) | 蓝 random * 10 < 1、红 random * 20 < 1 | 影响最大:改变地产分布,进而改变金钱分布 |
| 全部随机(图 8.13) | 红方仍只有一半买地机会 | 双方命运更多变,但红方很少赢 |
租金与收入的平衡:多数运行没有胜者——只要经过起点的收入能弥补房租,游戏就无限进行,因为模型缺少真实游戏中的租金通胀:房屋与旅馆。加入"投资房屋"机制(图 8.14),房屋提高收到大额租金的机会;AP 脚本第二行:
if((random * 10 < 1) && (Money > 4 + steps * 0.04)) fire(Buy)
if((Property > Houses / 5) && Money > 6 + steps * 0.04)) fire(Invest)
去随机、提高起点收入、一方投资房屋一方不投资:不投资时双方一样稳定增长(图 8.15 紫线),投资方(蓝)获胜。经过起点再也无法抵消上涨的租金:双方都投资、起点收入 时 1,000 次非确定运行约 75% 有胜者(双方机会均等),25% 拖到工具强行停止;起点收入 时拖延概率降到 0;高收入并把租金通胀效果加倍时,进入均衡的概率降到 2%。
加入动态摩擦(房产税):现在地产的力量又太强——地产多于对手就是最强的胜负指标,动态引擎过于主导。附录 B 给出解法:用动态摩擦遏制正反馈。加一个每 6 时间步从玩家 Money 池抽钱的泄,抽取量由 Houses 与 Property 池通过状态连接调节(图中税率为每栋房 6%、每块地 30%;真实桌游里也有不同形式的房产税)。
flowchart LR
Go[/"经过起点 2/(3+D5) 自动"/] --> M((Money))
M -->|3| Buy>"[AP] 买地"]
Avail((Available 20)) --> Buy
Buy --> P((Property))
M --> Inv>"[AP] 投资"]
Inv --> H((Houses))
P -.->|"+ 对方付租概率"| Rent{"收租门"}
H -.->|"+ 租金"| Rent
Rent --> M
M -->|"每 6 步"| Tax[\"房产税 泄"/]
P -.->|"+0.3"| Tax
H -.->|"+0.06"| Tax表 8.2 不同房产税率的效果(1,000 次模拟;蓝方买 14 块、红方买 6 块;双方都在最早机会买地和房)
| 税率 地产/房屋 | 蓝胜 | 红胜 | 无胜者 |
|---|---|---|---|
| 无税 | 632 | 152 | 216 |
| 0.1 / 0.02 | 557 | 314 | 129 |
| 0.2 / 0.04 | 472 | 503 | 25 |
| 0.3 / 0.06 | 456 | 542 | 2 |
税率上升时蓝方的优势下降,超过某点后地产反而成了负担。设置得当的税确实起到动态摩擦的作用。作者指出 AP 都在最早机会买地买房,有税时这未必是最佳策略,保守一些可能更好;税也减少了"无胜者"结局——摩擦有助于防止僵局,这也是理想的特性。
案例二:平衡 SimWar(第 8 章 Balancing SimWar,印刷页约 p.187–194)
SimWar 是 Will Wright 在 GDC 2003 上提出的假想极简战争游戏(据作者所知从未被实际制作),只有三种单位,都用工厂产出的资源建造:
| 单位 | 成本 | 规则 |
|---|---|---|
| 工厂 | 5 | 产出资源;工厂越多资源越多 |
| 防御单位 | 1 | 不能移动 |
| 进攻单位 | 2 | 唯一可移动单位;与敌方防御单位相遇时各有 50% 概率消灭对方 |
Wright 认为这个极简 RTS 仍有有趣的选择和同类游戏的动态行为:三者构成石头剪刀布——造工厂胜过造防御、造防御胜过造进攻、造进攻胜过造工厂;还有短期与长期的取舍,以及让人联想到"乌龟流 (Turtling)"与"速攻流 (Rushing)"的高风险高回报策略。乌龟流先建防御与产能、挡住进攻,再用产能优势组织大波进攻压倒对方;速攻流趁对手还没站稳就早早进攻,一般被视为高风险高回报,更考验技术(需要很高的 APM)。
建模(结构同 常见类型的经济机制 中的 RTS 示例):
flowchart LR
U(("未采资源 自动池")) -->|"0.25 × 工厂数"| R((Resources))
R -->|5| BF>"[点击] BuildF ≥5 才拉取"]
BF --> F((Factories))
F -.->|"+0.25 产率"| U
R -->|1| BD>"[点击] BuildD 蓝"]
R -->|2| BO>"[点击] BuildO 绿"]
BD --> Def(("Defense 蓝+绿"))
BO --> Def
Def -->|"只拉绿色"| Atk(("[点击] Attack"))
Atk --> X1[\"概率消灭敌方防御"/]
Atk --> X2[\"敌方无防御时 概率摧毁工厂"/]- 生产部分是典型的动态引擎:工厂越多产出越快、又能造更多工厂;但资源总量有限,只造工厂未必最好。注意结构要求开局至少有 5 资源或 1 座工厂,否则永远无法开始生产。
- 颜色编码:防御单位(蓝)与进攻单位(绿)都进入 Defense 池,点击 Attack 池只拉走绿色资源,所以只有进攻单位能发起攻击。
- 战斗:每时间步,一方每个进攻单位有机会消灭对方一个防御单位,防御单位同样有机会消灭进攻单位;进攻单位摧毁工厂的泄只在对方没有防御单位时生效。两人版完全对称(左蓝绿、右红橙)。
- 实力的任意定义:每座工厂 5 分、防御单位 1 分、后备进攻单位 1 分、正在进攻的单位 2 分。两个相同策略的 AP 打出看似势均力敌的曲线,但策略相同时不能据此断定平衡。
策略脚本:
// random turtle(配套网站版本,用 pregen 让每局略有不同)
if(Defense <= 3 + pregen0 * 3) do(BuyD)
if(Factories <= 2 + pregen1 * 3) do(BuyF)
if(Defense > 6 + pregen2 * 3 && random < 0.2) do(Attack)
if(Resources > pregen3 * 4) do(BuyO)
// turtle:先 4 防御 4 工厂,共 10 个以上单位才进攻
if(Defense < 4) fire(BuyD)
if(Factories < 4) fire(BuyF)
if(Defense > 9 && random < 0.2) fire(Attack)
if(Resources > 3) fire(BuyO)
// rush:2 防御 1 工厂即开始造进攻单位,起初快速出波,随时间攒更大的波
if(Defense < 3) fire(BuyD)
if(Factories < 2) fire(BuyF)
if(Defense > 5 + steps * 0.05 && random<0.2) fire(Attack)
if(Resources > 1) fire(BuyO)
除了进攻时机的随机,这两个脚本策略非常一致,最适合判断哪种更有效。1,000 次模拟显示乌龟流远远占优,约 92% 获胜;速攻方的胜利很大一部分来自乌龟方耗尽资源(这不常发生)。配套网站上的黑色 AP 不是对手,只在僵局时停止游戏。
表 8.3 调整生产成本的效果(每行 1,000 次自动运行,每行之间手动改图)
| 调整 | 乌龟胜 | 速攻胜 | 平局/超时 | 平均用时 |
|---|---|---|---|---|
| 无调整 | 929 | 68 | 3 | 70.97 |
| 防御成本 1.5 | 890 | 105 | 5 | 74.27 |
| 防御成本 2.0 | 660 | 337 | 3 | 77.88 |
| 防御成本 2.5 | 515 | 480 | 5 | 74.04 |
| 工厂成本 6 | 844 | 154 | 2 | 78.81 |
| 工厂成本 7 | 792 | 204 | 4 | 88.07 |
| 工厂成本 8 | 710 | 278 | 12 | 98.53 |
| 工厂成本 9 | 568 | 401 | 31 | 107.87 |
| 工厂成本 10 | 455 | 509 | 36 | 107.61 |
| 进攻成本 1.8 | 914 | 83 | 3 | 67.77 |
| 进攻成本 1.6 | 888 | 112 | 0 | 63.86 |
| 进攻成本 1.4 | 802 | 198 | 0 | 58.31 |
| 进攻成本 1.2 | 653 | 347 | 0 | 52.33 |
| 进攻成本 1.0 | 506 | 494 | 0 | 48.33 |
作者结论:提高防御成本对乌龟/速攻平衡影响不大,只有防御单位贵过进攻单位、成为很差的选择时乌龟才开始更常输——平衡主要取决于生产与进攻单位之间的成本关系,与进攻/防御之间关系不大。提高工厂成本起初延长平均时长(建产能更慢),但很高的工厂成本偏向赢得更快的速攻流,到高成本时后一效应压过前者,时长趋于稳定。
表 8.4 其他调整的效果
| 调整 | 乌龟胜 | 速攻胜 | 平局/超时 | 平均用时 |
|---|---|---|---|---|
| 无调整 | 929 | 68 | 3 | 70.97 |
| 产率 0.20 | 847 | 152 | 1 | 88.99 |
| 产率 0.15 | 750 | 248 | 2 | 124.34 |
| 产率 0.10 | 396 | 565 | 39 | 208.56 |
| 进攻火力 30% | 919 | 81 | 0 | 65.22 |
| 进攻火力 35% | 863 | 137 | 0 | 59.43 |
| 进攻火力 40% | 811 | 189 | 0 | 56.55 |
| 进攻火力 45% | 755 | 245 | 0 | 56.07 |
| 进攻火力 50% | 627 | 373 | 0 | 51.95 |
| 初始资源 4 | 883 | 114 | 3 | 76.43 |
| 初始资源 3 | 885 | 114 | 1 | 79.26 |
| 初始资源 2 | 877 | 122 | 1 | 84.95 |
| 初始资源 1 | 855 | 144 | 1 | 89.51 |
| 初始资源 0 | 797 | 200 | 3 | 98.73 |
| 可用资源 110 | 937 | 63 | 0 | 69.85 |
| 可用资源 120 | 949 | 51 | 0 | 69.43 |
| 可用资源 130 | 945 | 55 | 0 | 71.11 |
| 可用资源 200 | 970 | 30 | 0 | 71.18 |
| 可用资源 90 | 911 | 84 | 5 | 73.12 |
| 可用资源 80 | 860 | 125 | 15 | 80.82 |
| 可用资源 70 | 839 | 134 | 27 | 85.96 |
(转写自原书图片表;所有行三项之和均为 1,000。可用资源越多越偏向乌龟,越少越偏向速攻且僵局增多。)
最佳平衡大概要组合多种调整:作者兼顾平均时长,选择产率 0.20、工厂成本 7、进攻火力 35%,两种策略恰好各胜 500 次,平均 83.02 时间步。
"一次只改一处,并夸大改动":同时改两处就分不清各自的效果;先做很大的改动,确认它确实有效且方向正确,之后总可以退回到原值与新值的中间。这与 Schell 引用 Sid Meier 的"加倍与减半"同出一辙(见 游戏平衡)。
从模型到游戏(第 8 章 From Model to Game,印刷页约 p.194)
平衡好 Machinations 图并不保证游戏自动平衡:图是抽象视角、缺乏细节,真实游戏的表现会略有不同;设计越接近图,图上的平衡越能直接迁移。Machinations 无法考虑人类玩家的特殊行为(如虚张声势)或战争游戏中的战略机动。但只要图的结构与机制结构一致,某些效应会相似——例如"工厂与进攻单位的相对成本对乌龟/速攻平衡影响很大"这一发现能指导完整实现的平衡;在图上试玩,也能预先认出完整游戏试玩时会涌现的玩法模式。
程序员视角:把它搬进自己的引擎
- Machinations 工具基于 Flash 已停用(见 Machinations 图示语言),但方法可直接移植:把经济写成无渲染、可固定时间步推进的纯逻辑(见 游戏循环与更新方法),用可设种子的 PRNG,以命令行批量跑上千局并输出 CSV。
- "AP 脚本=优先级规则表"可直接用作单元测试中的机器人策略;"去随机"对应固定种子或用期望值替换随机分布,使回归测试可复现。
- 作者的警告同样适用于自动化测试:机器人策略越来越复杂才能"通关"时,问题在设计而非机器人。
原书问题
- 表 8.1 乘除行的说明有误:示例
if(Upgrades * 2 > Resources / 3)的说明写成"升级数乘 2 大于升级数除以 3",按表达式应为"大于资源数除以 3"。 - 房屋投资脚本括号不配对,且与说明不符:
if((Property > Houses / 5) && Money > 6 + steps * 0.04))多一个右括号(左 3 右 4);说明称"地产数超过房屋数的五倍时投资",但条件Property > Houses / 5等价于"房屋数少于地产数的五倍",即每块地最多约 5 栋房(与《大富翁》每地 4 栋房后升旅馆的规则相近)。按说明应写作Property > Houses * 5。 - 命令名不一致:命令表介绍的是
fireSequence(),去随机一节却写fireSequential();random turtle 脚本用do(BuyD),而命令表只有fire()系列,节点名在建模部分又叫BuildF/BuildD/BuildO,脚本中为BuyF/BuyD/BuyO。以 Machinations 工具实际命令为准时应使用fire...(本页按原文照录)。 - 与表 8.3 不符的结论:"只有防御单位贵过进攻单位时乌龟才开始更常输"——表中防御成本 2.5(已高于进攻单位成本 2)时仍是乌龟 515 胜、速攻 480 胜,乌龟并未更常输,只是接近持平。另"工厂成本到 8 及以上时平均用时稳定",表中 8→9 仍从 98.53 增至 107.87,稳定发生在 9 以后。
- "乌龟流约 92% 获胜"与表 8.3 的 929/1000(约 93%)略有出入,属取整问题。
- random turtle 脚本"先建 4–6 个防御单位":条件
Defense <= 3 + pregen0*3在 Defense 达到 前持续建造,即 4–6 个(pregen0 接近 1 时可到 6),与原文一致;工厂"3–5 座"同理。
与 Schell 的对照
| 问题 | Schell(游戏测试、游戏平衡) | Adams & Dormans(本章) |
|---|---|---|
| 平衡靠什么 | 真人试玩+数学模型+直觉,"记录模型,边调游戏边调模型" | 先在可执行的抽象模型上用人工玩家跑上千局,再用真人验证 |
| 调参步长 | 加倍与减半(Sid Meier) | 一次只改一处,先大幅改动,再取中间值 |
| 局限 | 强调真人才能告诉你体验 | 承认 AP 不能代替真人,模型无法表现虚张声势与战略机动 |
两者并不冲突:A&D 的模拟只回答"经济结构是否偏向某策略",Schell 的试玩回答"玩家感觉如何"。
出处
- Game_Mechanics_Advanced_Game_Design_-_Ernest_Adams.pdf
- Ernest Adams & Joris Dormans《Game Mechanics: Advanced Game Design》第 1 版(New Riders, 2012, ISBN 978-0-321-82027-3)第 8 章 Simulating and Balancing Games(PDF p.246–278,印刷页约 p.171–195)