用矩阵数学解 Factorio 的品质概率
作者玩 Factorio 的方式是写矩阵数学代码来规划工厂。他把品质循环建成马尔可夫链、解出平稳方程,比较几种循环策略的期望产量,并做成在线计算器;他认为精确解而非无穷级数迭代是这个做法的新意。
中文
复制

我玩 Factorio 的方式还算正常:写矩阵运算代码来规划工厂。
不过这个待会儿再说。 先给个 TL;DR:直接去看我新做的在线计算工具。
Factorio 与品质机制入门
Factorio 基本奠定了工厂类游戏这个品类: 你扮演一个角色,采集资源并把它们组合起来,制作越来越复杂的物品, 而这些物品又反过来支撑更复杂的制作。 听起来很像 Minecraft 和许多其他生存游戏, 但工厂类游戏的不同之处在于它对自动化的侧重: 很快,大部分制作就不再由角色“手工”完成, 而是交给越来越多的机器, 再配合传送带之类的各种物流手段,把物品在机器之间 或任何需要的地方搬运。有些工厂游戏走得更远,干脆取消了玩家角色。
随着游戏内“科技”逐步解锁,Factorio 提供了许多提升产量的机制。 其中之一是插件:制作机器有(有限的)若干插槽, 可以装入不同种类的插件来影响其属性: 速度插件让机器运转更快,代价是耗能增加, 产能插件用同样的原料提高产出,代价是速度和能耗, 等等。
2024 年发布的 Space Age 扩展加入了新机制,其中包括品质: 现在每个物品和配方都有五个品质等级: ⚀ 普通、⚁ 精良、⚂ 稀有、⚃ 史诗、⚄ 传奇。 具体提升什么取决于物品,比如让制作机器更快, 或者让产能插件产能更高。 高品质物品可以直接用同品质的原料制作, 但想要提升品质,唯一的途径就是新增的品质插件。

品质插件本身也可以有品质。来源:Factorio wiki
插件影响品质提升的概率 。 对一次制作而言,第一次之后的每一级品质提升都是额外的 10% 概率。 我们可以根据输入品质,列出输出品质的概率表:

例如,一台有四个模块插槽的机器,最高品质概率是 24.8%:

从普通一步跳到传说只有 0.0248% 的概率。来源:Factorio wiki
从普通一步跳到传说只有 0.0248% 的概率。 来源:Factorio wiki
有些玩家不喜欢在一款大体上确定性的游戏里引入随机性,但重复次数够多,概率就会变成比例。
概率的设定是经过平衡的:即便要经过多道制作工序(每一道都可能触发品质跳级),想拿到高品质物品,就免不了同时造出一堆不想要的低品质物品。
为了避免仓库最终塞满、工厂被迫停摆,Space Age 还引入了回收机:一种新机器,能销毁任何物品,并(通常)返还其 25% 的原料。玩家因此可以设计出“升级回收”装置,让装配和回收带着品质模块循环往复,直到物品达到想要的品质——代价是消耗多得多的原料:
来源:Factorio 博客
工厂规划工具
有些电子游戏,有一部分是“玩”在游戏之外的。Blue Prince 就完全指望玩家自己把看到的一切详细记下来,却不提供游戏内记事本或类似工具。工厂类游戏很适合拿来搭大型电子表格做资源核算,但少数玩家认定电子表格不足以规划工厂,于是花上无数小时编写专用工具,把游戏里的大量数学复现出来,以便精确建模一条生产链。

Factoriolab 中的生产链示例
在工厂游戏里,这些全是可选项,完全可以凭感觉玩,缺什么就多造点什么。
但我喜欢提前规划: 每种机器需要多少台?预期产量是多少?瓶颈在哪里? 品质回收的循环特性让这件事格外棘手, 无论是靠猜,还是用现有工具去算。
矩阵运算
设想一下:
- 某些原料(比如铁板) 来自一条任意的生产链,其中可能用到品质模块。 任意一种原料都有概率落在各个品质档位: ⚀ 普通、⚁ 精良、⚂ 稀有、⚃ 史诗、⚄ 传说。
- 足够多的组装机,覆盖每一档配方 把所有原料做成某种产品(比如管道)。 这些机器装有品质模块,因此出现品质提升的概率是 10%。
来追踪一件物品可能的去向:
某一档位的产物,可以来自同档位或更低档位的原料。 这一结果的总概率,是得到它的各种(相互独立的)途径概率之和。 而每条途径的概率,则是某次品质跃迁的百分比几率 乘以对应档位原料原本出现的概率:
(百分号可理解为隐含除以 100, 因此“百分比”等同于乘法。)
这里百分比系数相对于 wiki 上的品质跃迁概率表看起来沿对角线转置了, 但这只是因为我们把产品等级竖直排列。 不妨把同一物品不同等级的概率归入行向量:
现在,我们的线性方程组可以写成一个方程,其中向量与一个转移矩阵相乘,该矩阵与 wiki 中的表格一致:
作为一种边界情况,品质概率为零意味着不会发生品级转换。 对应的转移矩阵是单位矩阵:
这看起来也许算不上多大进展, 但现在可以通过连续的矩阵乘法来计算多步过程。 例如以 7.5% 的品质概率开采铁矿石, 再以 5% 的品质概率将其冶炼成铁板,然后以 10% 的概率制作管道。 在没有生产力加成的情况下,我们得到这些最终产品的概率:
质量策略
虽然可以尽可能多地使用质量模块,并应对每个层级、无处不在、同时发生的问题,但这里我们聚焦于更小的自包含系统。
“赌博”:不回收的机会主义质量
最简单但也最低效的做法是:用普通品质的原料配合质量模块进行制造,且不回收任何东西。这可以在解锁回收机之前完成,但只适用于少量物品,例如制造几百个星际采集器,期望从中得到十几个罕见或稀有品质的,用于早期的太空船。
当工厂对普通品质物品有其他用途时,这种方法会有所改善。例如,如果在地面上放置数千个普通品质的太阳能板,用质量模块制造它们,在输出缓冲区填满之前,能为太空船带来更高品质的更好产出。
由于只有单一步骤且没有循环,这最容易计算:预期的产品层级分布就是转移矩阵的第一行,或者维基上概率表的第一行。
“洗涤”:纯回收循环
对大多数物品而言,回收机会逆转主要制造配方,并返还25%的原料。但有些物品没有制造配方(比如矿石),或者被认为不可逆(通常是熔炼和化学过程)。在这种情况下,回收机要么什么都不产出,要么有25%的概率产出同样的物品。如果回收机装有质量模块,这个过程可以提升质量。在循环中反复进行,最终所有物品要么被销毁,要么被提升,直到达到任何想要的品质层级。自我回收物品可以说并非常见情况,但让我们从这里开始,因为数学更简单。

示例设置:开采矿石(带质量模块),并“洗涤”它直到罕见或以上。
示例设置:开采矿石(带质量模块),并“洗涤”它直到罕见或以上。
假设系统中已注入某件物品,这里以采矿为例,用 表示由各品质等级概率组成的 5 分量行向量。
对该向量做变换后,新的概率之和可能小于 1。隐含的剩余情况是某个位置根本没有物品。例如,回收机有 75% 的概率什么都不产出,这可以用概率向量乘以 来表示。
按品质筛选同样可以用矩阵乘法表示。以提取稀有或更高品质为例:
一次循环迭代的综合效果是:先筛选保留,再按质量回收利用:
现在考虑一个物品可能被提取出来的几种情况:
- 它原本质量就足够高,可以立即被提取:概率向量
- 它恰好经过一次回收循环:
- 它恰好经过两次回收循环:
- 以此类推。
任何物品最终都会被提取或销毁, 但循环次数没有上限。 所有可能结果的组合构成一个无穷级数:
在有限时间内计算无穷和相当麻烦。尽管这个级数确实收敛到一个有限值——因为各项呈指数级变小——我们可以一直计算到各项小到足以近似为止,但在存在精确解的情况下,这样做并不令人满意。
长期平均吞吐量
关键洞察在于,某个物品已经循环了多少次并不重要,重要的是它当前处于哪个品质等级。在短时间尺度上,由于品质模块的随机效应,这会有所波动,但只要重复次数足够多,概率就会变成比率。
因此,与其考虑单个物品的概率,不如考虑足够长的时间内通过系统某一部分的物品平均速率,同样用品质等级的5分量向量表示。这些向量可以像之前一样乘以相同的转移矩阵。对于“洗涤”纯回收循环,相关向量为:
- 注入系统的物品,品质分布任意,此例中基于采矿机中的品质模块
- 物品
- 所有通过 的物品
- 按品质等级 的物品,此例中为稀有及以上
- 品质为 的物品 ,即未被提取的部分
我们将 和 视为固定参数,其他向量视为待求解的未知量。
系统会收敛到一个动态平衡,长期平均值满足以下方程:
我们可以重新排列并代入:
转置后得到的是列向量而非行向量,与经典约定保持一致:
介绍几个新名称:
现在,均衡状态已写成经典 形式的线性方程组, 可用高斯消元法解出 。 由 很容易算出 , 进而得到 (所需的回收机数量) 和 (系统的总产出率)。
如果把上面的示例配置扩大到每秒开采 100 矿石,参数会是:
而高斯消元法的解法是:
换算下来,提取速率大约是每分钟稀有 90 个、史诗 9 个、传说 1 个。
再看一个例子:
- 只注入普通品质的新物品,单位暂时随意取:
- 用当前最好的品质模块回收:
- 只提取传说品质的物品
解方程可得:
对于会回收自身的物品,“暴力”洗涤平均每产出 1 个传说品质产物,就要消耗 个普通品质输入。这与其他人算出的比例一致。所需的回收产能略低于新输入速率的 。
“升级回收”:合成 + 回收循环
对于回收后确实会返还原料的物品,我们可以把合成机器和回收机串联成一个循环:

将 construction robot 升级回收至传说品质的示例布局。
大多数合成配方需要多种原料,数量也各不相同,但这里我们把它抽象掉,直接以“一次合成所需的整套原料”作为计量单位。在这个例子里,每台合成 construction robot 的机器每秒消耗 4 个 electronic circuit 和 2 个 flying robot frame,但我们把它叫作“每秒 2 份原料”。
所以我们衡量的是原料和产物在长期内的平均速率,各自分五个品质等级;不过它们只出现在系统的不同部分,所以用 5 分量向量就够了,不必上升到 10 维数学。(嘿嘿,伏笔。)
示例布局中,普通品质的新原料由机器人送入蓝色的请求箱,传说品质的产物则被取出。一般情况下,我们可以设想任意品质分布的原料或产物在别处生产,再注入这个系统。同样,我们也可以决定取出任意品质等级的产物或原料(或两者都取)。(有时候某种原料比产物更值得拿到高品质,但把那个特定产物升级回收,产率可能比其他方法更好。)
那么我们要考虑的 5 分量行向量,对原料来说是:
- 注入系统,品质分布任意
- 原料
- ,按其品质等级,本例中为零
- 原料 用于合成新产品
对于产品:
- 向系统中注入 ,质量分布任意
- 产品
- 按质量等级划分的 ,本例中为传说级
- 产品
我们将 和 视为固定参数, 其余向量视为待求解的未知量。
同样,基于质量等级进行筛选的转移矩阵 是单位矩阵中互补的部分。 在本例中:
我们考虑一个制造过程 ,在本例中该值为 0%, 但如果使用某些制造机器或产能模块,该值可能会更高。 制造与回收的品质概率也可能不同 (例如使用产能模块制造时, 或取决于机器的模块插槽数量)。
简而言之,我们约定:
系统收敛至如下平衡状态:
我们可以重新排列并代入:
再介绍两个名字:
我们又一次把问题化成了标准的 形式,其中:
解得 后得到 , 将其代回原均衡方程,其余各项便均可求出。
在上述示例设定中,参数按任意单位缩放,具体为:
用高斯消元法求解:
实际运行中,这套系统的瓶颈在于普通品质产品的制作速度: 每分钟 360 个。 那就把所有东西乘以
总之,这套系统平均每分钟产出近 2 台传说级建筑机器人, 每分钟消耗约 309 套原料(309 个框架 + 618 个电路)。
在 Space Age 中,少数几种物品的产能加成可以通过可重复研究提升。 游戏将加成上限硬性限制为 +300%,因此“制造再回收” 最多只能返还最初投入的原料,绝不会更多。
产能研究的等级成本呈指数增长, 所以假设我们同时使用产能模块来达到上限, 也就是说制造机器里不放品质模块。 这一次,我们投入的是成品而不是原料。
我们的模型预测:
也许出乎意料的是,中间质量档位的产出率完全相同。
最高产能加成可以把普通品质的产品变成传说品质,且不损失任何资源,代价是需要大量机器和模块才能达到可观的吞吐量。
“太空赌场”:小行星再处理循环
在 Space Age 中,每个太空平台都是一座迷你工厂。它们不从地面开采矿石,而是收集小行星碎块并将其粉碎以获得资源。这些资源可以用于飞船自身的燃料和弹药,也可以供给太空中的生产链,最终产品再送回某颗行星的地面。
三种不同的碎块(金属质、碳质和氧化物)产出不同的资源,在太阳系不同区域的分布频率也各不相同。小行星碎块可以“再处理”,有一定概率得到另一种类型(或者什么也得不到)。

小行星再处理配方。来源:Factorio wiki
假设每种配方都有足够多的粉碎机,我们就可以用数学方式表示这个过程:将一个行向量乘以一个 3×3 的方阵转移矩阵:
再处理也接受品质模块,因此可以像“洗涤”那样循环使用。 粉碎传说级碎块会产出某些基础资源的传说级版本, 可用于制作其他传说级物品。
这次我们用传送带运输三种类型中任意一种的小行星碎块, 每种都有五个品质等级,共 15 种可能的物品。 我们用 15 维行向量和 15×15 转移矩阵来表示。 后者由九个 5×5 块组成的分块矩阵构建而成:
除了维度更高之外, 数学推导和洗涤时一样, 最终得到一个线性方程组 ,用来求解 ,其中:
小行星粉碎机有两个模块槽, 因此再处理所能达到的最高品质概率为 。 小行星收集器没有模块槽, 因此刚收集到的碎块始终是普通品质。 我们用 3 个分量构建 15 分量的 向量,分别对应每种小行星类型(金属质、碳质、氧化物)的普通品质产出率。 例如在 Nauvis 轨道:
假设我们只提取传说品质的氧化物碎块,其余全部再处理。 解此方程会得到两个互补的 15 分量行向量 和 ,我们可以将它们整理成 3×5 的表格
示例:在 Nauvis 轨道,提取传说品质的氧化物碎块,其余全部再处理:
| 待再处理 |
|---|
| 金属质 |
| 碳质 |
| 氧化物 |
| 已提取 |
| 金属质 |
| 碳质 |
| 氧化物 |
我们观察到:
- 随着品质等级提高,小行星类型的分布会迅速收敛到每种各占 。普通品质的新输入对进入系统的传说品质碎块的分布影响可以忽略不计。
- 提取单一类型的传说级小行星时,传说级产出约占总新输入的 。这比“洗涤”(即纯回收)的约 好得多,因为再处理只有 20% 的概率摧毁输入,而回收是 75%。
其他策略
还有其他获取高品质物品的方法,它们不太能归入上面的类别(特别提名“LDS Shuffle”),我相信人们还会想出更多。但如果某个循环的行为难以计算,我们可以用同样的思路:
- 把多种物品的吞吐量表示为向量
- 把线性变换(制作、回收……)表示为矩阵乘法
- 把动态平衡表示为矩阵方程
- 解这个方程,必要时用高斯消元
做一个交互式计算器工具
手工做矩阵运算显然又繁琐又容易出错,让计算机替我们做吧。
我出于习惯先用 Rust 起步。nalgebra 非常适合我们这里要做的矩阵运算。它内置了多个求解 线性方程组的求解器,但这些求解器基本都要求矩阵和向量分量的标量类型是浮点数 f32 或 f64,而基础库则更通用。
实际上浮点数完全够用,但它是固定精度的近似值,所以每一步计算都可能引入误差。既然可以,那拿到精确结果不是更好吗?矩阵规模和运算次数都是固定的,而且相对较小,所以我们不需要为速度优化代码。
我们用的每一个运算,归根结底都是加、减、乘、除。
所以只要所有参数都是有理数,
结果也一定是有理数。
num_rational 用一对(泛型)整数
精确表示有理数。
需要的话,我本可以引入某个无限精度 BigInt 库,
但内置的 i128 配上
溢出检查,
对 15×15 矩阵来说已经够用。
(i64 足以应付 5×5。)
到这里,我已经有一个能跑通上述全部数学计算的 Rust 库, 但靠改源码来调参数,用户体验实在谈不上好。 我更想要 Factoriolab 那样的东西。 而且要让别人也容易上手,它就该放在网上。
我的 Rust 代码可以编译成 WebAssembly,但那样一来,要么:
- 连整个 GUI 也用 Rust + wasm 来写。 可行,但工具链 还不太好用
- 用 JavaScript 或 TypeScript 写 GUI(享受成熟的工具生态), 再桥接到 wasm 做数学计算。 但参数太多,API 面积不小, 做这么多桥接可不轻松
于是我干脆用 TypeScript 把整个东西重写了一遍。 BigInt 是内置的, Factoriolab 已经有一个不错的开源 有理数库, 写一个通用的矩阵库也不太难。
至于在浏览器里做交互式 GUI, 我上次大量写前端的时候,jQuery 还是当红新事物。 我不想学 React,于是选定了:
合在一起,factoqual.grebedoc.dev 现在提供了一个 用于规划品质回收机的交互式 GUI。 源码发布在 codeberg.org/SimonSapin/factoqual。
致谢
Daniel Monteiro 关于品质数学的 博客文章给了我很大启发。 其他人也做过类似的工作, 包括 Konage 和 Factorio wiki 的贡献者。 但我相信,这里展示的通过求解平衡方程得到的精确解是新的, 而不是对无穷级数做迭代近似。