← 文章 / 科技资讯
Hacker News 7小时前 · 2026-10-01 03:25:14 · 2 阅读

Factorio 品质机制求解:矩阵运算与在线计算工具

我玩 Factorio 的方式比较特别:写矩阵运算代码来规划工厂布局。

不过这个后面再说。如果你想直接看结论(TL;DR),可以跳到我新做的在线计算工具。

Factorio 与 Quality 简介

Factorio 基本上开创了工厂建造类游戏这个品类:玩家采集资源,把它们组合成越来越复杂的物品,而这些物品又能解锁更高级的制造。听起来很像 Minecraft 和其他生存游戏,但工厂类游戏的独特之处在于自动化:很快,大部分制造就不再靠角色"手动"完成,而是由越来越多的机器代劳,配合传送带等各种物流方式在机器之间或任何需要的地方运输物品。有些工厂游戏甚至干脆取消了玩家角色。

随着游戏内"科技"的解锁,Factorio 提供了很多提升产能的手段。其中之一是模块:制造机器有(数量有限的)插槽,可以插入不同类型的模块来改变机器的属性——速度模块让机器运转更快,但耗电更多;产能模块在相同原料下产出更多,但会降低速度、增加耗电,等等。

2024 年发布的 Space Age 资料片加入了新机制,其中就有 Quality:每件物品和每个配方现在都有五个品质等级——⚀ 普通、⚁ 非常见、⚂ 稀有、⚃ 史诗、⚄ 传说。具体到不同物品,每个等级会提升相应属性,比如让制造机器更快,或让产能模块产得更多。高品质物品可以直接用同品质原料制造,但想提升品质,唯一的途径就是新增的品质模块。

品质模块自己也可以有品质。 来源:Factorio wiki

模块影响的是品质提升概率 $Q$。对一次制造而言,第一个等级之后每升一级,都有额外的 10% 概率。我们可以列出一张表,表示不同输入品质对应的输出品质概率:

来源:Factorio 维基

举个例子,在拥有四个模组插槽的机器中,质量提升的最大概率为 24.8%:

从普通品质直接跳到史诗品质的概率仅为 0.0248%。 来源:Factorio 维基

部分玩家不喜欢向原本主要是确定性机制的游戏中引入随机性,但随着重复次数增加,概率终将趋近于比率。

概率机制经过平衡设计,使得即便经过多个制造步骤(每步都可能发生品质跃升),获取高品质物品时也必然伴随产生大量低品质的废料。

为防止存储容量填满导致工厂停摆,“太空时代”版本引入了回收机: 这是一种新型机器,可销毁任意物品并(通常)返还 25% 的材料。 这使玩家能够设计“升级回收”装置, 通过品质模组在制造与回收之间循环,直到物品达到目标品质, 代价是消耗大量额外的原材料:

来源:Factorio 博客

工厂规划工具

某些电子游戏的玩法部分发生在游戏之外。 Blue Prince 完全期待玩家手动记录观察到的所有内容, 但并未提供游戏内笔记本等类似工具。 工厂类游戏特别适合制作大型电子表格来进行资源核算, 但少数玩家认为电子表格不足以进行工厂规划, 他们耗费数小时编程开发专用工具, 复现游戏内的大部分数学逻辑,以精确模拟生产链。

Factoriolab 中的示例生产链

在工厂类游戏中,使用这些工具纯属可选项,靠直觉游玩、看到短缺就扩建也完全可行。

不过我更喜欢提前规划:每种机器需要多少台?预期产出多少?瓶颈在哪里? Quality 升级改造循环往复的特性,让预测或借助现有工具计算都格外困难。

矩阵运算

设想一下:

  • 某些原料(例如铁板)来自任意生产链,其中可能涉及 quality 模块。 每种原料在各个 quality 等级上的概率各不相同: ⚀ 普通、⚁ 不常见、⚂ 稀有、⚃ 史诗、⚄ 传说。
  • 配备足够数量的组装机器来将所有原料加工成某种成品(例如管道), 机器覆盖每种配方等级。这些机器装有 quality 模块,quality 触发几率为 10%。

我们追踪单个物品的各种可能结果:

某等级的成品可以来自同级或更低的原料。 达到该结果的总概率是多种(独立)路径概率之和。 而每种路径的概率等于特定 quality 跳级几率乘以原料本身处于对应等级的概率:

p⚀=i⚀⋅90%p⚁=i⚀⋅9%+i⚁⋅90%p⚂=i⚀⋅0.9%+i⚁⋅9%+i⚂⋅90%p⚃=i⚀⋅0.09%+i⚁⋅0.9%+i⚂⋅9%+i⚃⋅90%p⚄=i⚀⋅0.01%+i⚁⋅0.1%+i⚂⋅1%+i⚃⋅10%+i⚄ \begin{align*} p_⚀ &= i_⚀ ⋅ 90\% \\ p_⚁ &= i_⚀ ⋅ 9\% &+ &i_⚁ ⋅ 90\% \\ p_⚂ &= i_⚀ ⋅ 0.9\% &+ &i_⚁ ⋅ 9\% &+ &i_⚂ ⋅ 90\% \\ p_⚃ &= i_⚀ ⋅ 0.09\% &+ &i_⚁ ⋅ 0.9\% &+ &i_⚂ ⋅ 9\% &+ &i_⚃ ⋅ 90\% \\ p_⚄ &= i_⚀ ⋅ 0.01\% &+ &i_⚁ ⋅ 0.1\% &+ &i_⚂ ⋅ 1\% &+ &i_⚃ ⋅ 10\% &+ i_⚄ \end{align*}

(百分号可视为隐含除以 100,因此“某百分比”等同于乘法。)

这里的百分比系数相对于 wiki 上的 quality 跳级概率表,沿对角线呈 转置 分布, 只是因为我们将成品等级按纵向排列。换个思路,把同一物品不同等级的概率归入行向量:

product=(p⚀p⚁p⚂p⚃p⚄)ingredient=(i⚀i⚁i⚂i⚃i⚄) \begin{align*} product &= \begin{pmatrix*} p_⚀ & p_⚁ & p_⚂ & p_⚃ & p_⚄ \end{pmatrix*} \\ ingredient &= \begin{pmatrix*} i_⚀ & i_⚁ & i_⚂ & i_⚃ & i_⚄ \end{pmatrix*} \\ \end{align*}

现在我们的线性方程组 可以写成单个方程:一个向量 乘以一个 转移矩阵, 这个矩阵正好对应 wiki 里的那张表:

products=ingredients⋅Tquality(10%)Tquality(q)=(1−q9q109q1009q1000q100001−q9q109q100q100001−q9q10q100001−qq00001) \begin{align*} {products} &= {ingredients} ⋅ T_{quality}(10\%) \\[1em] T_{quality}(q) &= \begin{pmatrix*} 1-q & \frac{9q}{10} & \frac{9q}{100} & \frac{9q}{1000} & \frac{q}{1000} \\[0.3em] 0 & 1-q & \frac{9q}{10} & \frac{9q}{100} & \frac{q}{100} \\[0.3em] 0 & 0 & 1-q & \frac{9q}{10} & \frac{q}{10} \\[0.3em] 0 & 0 & 0 & 1-q & q \\[0.3em] 0 & 0 & 0 & 0 & 1 \end{pmatrix*} \end{align*}

有一种边界情况:品质概率为零时不会发生品质升级, 对应的转移矩阵就是单位矩阵: Tquality(0%)=I5T_{quality}(0\%) = I_5

这看起来进步不大,但现在多步流程可以通过连续的矩阵乘法来计算了。 比如先用 7.5% 的品质概率开采铁矿石,再用 5% 的概率熔炼成铁板,最后以 10% 的概率制作管道。 在没有生产力加成的情况下,最终产品的概率分布如下:

pipe=(10000)⋅Tquality(7.5%)⋅Tquality(5%)⋅Tquality(10%)=(0.9250.06750.006750.0006750.000075)⋅Tquality(5%)⋅Tquality(10%)≈(0.878750.105750.0136130.0016650.000223)⋅Tquality(10%)≈(0.7908750.1742630.0296780.0044670.000719) \begin{align*} pipe &= \begin{pmatrix*} 1 & 0 & 0 & 0 & 0 \end{pmatrix*} ⋅ T_{quality}(7.5\%) ⋅ T_{quality}(5\%) ⋅ T_{quality}(10\%) \\ &= \begin{pmatrix*} 0.925 & 0.0675 & 0.00675 & 0.000675 & 0.000075 \end{pmatrix*} ⋅ T_{quality}(5\%) ⋅ T_{quality}(10\%) \\ &≈ \begin{pmatrix*} 0.87875 & 0.10575 & 0.013613 & 0.001665 & 0.000223 \end{pmatrix*} ⋅ T_{quality}(10\%) \\ &≈ \begin{pmatrix*} 0.790875 & 0.174263 & 0.029678 & 0.004467 & 0.000719 \end{pmatrix*} \end{align*}

品质策略

虽然你可以尽可能多地使用品质模块,采取那种全层级、全场景、一次性部署的策略,但本文侧重于构建更小、更自包含的系统。

“赌一把”:无回收的随机提升品质

最简便但也效果最差的策略是:使用普通品质的原料和品质模块进行生产,且完全不进行回收。 这种方法可以在解锁回收机之前使用,但只适用于少量物品。 例如,可以生产几百个“小行星收集器”,指望其中能出十几个不常见或稀有品质的,用于早期飞船。

如果工厂对普通品质物品有其他用途,效果会更好。 例如,当你在地面铺设成千上万个普通品质的太阳能板时,使用品质模块生产它们,能在输出缓冲区填满前,为飞船提供更多高品质部件。

由于只有一个步骤且没有循环,计算最简单: 最终产出的品质层级概率分布,就是 Wiki 上那个概率表(或 Tquality(q) 转移矩阵)的第一行。

“洗”:纯回收循环

对于大多数物品,回收机会反转主生产配方,并返还 25% 的原料。 但有些物品没有生产配方(如矿石),或者其加工过程被视为不可逆(通常指熔炼和化学反应过程)。 在这种情况下,回收机要么什么都不产出,要么有 25% 的概率返还该物品本身。 如果回收机装有品质模块,这个过程可以提升品质。 不断循环操作,最终所有物品要么被销毁,要么品质提升至你目标的任意层级。 虽然“自我回收”并非常见情况,但鉴于其数学原理更简单,我们先从这里讲起。

示例布局:使用品质模块开采矿石,并将其“洗”到稀有品质或以上。

假设系统中注入一个物品(在此案例中来自采矿),并定义 $fresh_{unit}$ 为表示各品质层级概率的 5 维行向量。

在对该向量进行变换后,新概率之和可能小于 1。 这意味着存在一种隐含情况:在特定位置根本没有物品。 例如,回收机有 75% 的概率什么都不产出,这可以用将概率向量乘以 $\frac{1}{4}$ 来表示。

基于质量的过滤同样可以用矩阵乘法来表示。 以提取稀有(rare)及以上品质为例:

\begin{align*} T_{filterKeep} &= \begin{pmatrix*} 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 \end{pmatrix*} \\ T_{filterExtract} &= \begin{pmatrix*} 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 \end{pmatrix*} = I_5 - T_{filterKeep} \end{align*}

循环一次迭代后的综合效果,先进行保留过滤,再执行带质量的回收:

\begin{align*} L &= T_{filterKeep} · \frac{1}{4} · T_{quality}(q) \end{align*}

接下来考虑物品被提取的所有可能路径:

  • 物品初始品质就足够高,可以直接被提取:概率向量为 \begin{equation*} fresh_{unit} · T_{filterExtract} = fresh_{unit} · L^0 · T_{filterExtract} \end{equation*}
  • 物品恰好经过回收循环一次: \begin{equation*} fresh_{unit} · T_{filterKeep} · L · T_{filterExtract} = fresh_{unit} · L^1 · T_{filterExtract} \end{equation*}
  • 物品恰好经过回收循环两次: \begin{equation*} fresh_{unit} · T_{filterKeep} · L · L · T_{filterExtract} = fresh_{unit} · L^2 · T_{filterExtract} \end{equation*}
  • 以此类推。

任何物品最终要么被提取,要么被销毁,但循环次数没有上限。 所有可能结果的组合是一个无穷级数:

\begin{equation*} extracted_{unit} = fresh_{unit} · \bigg(\sum_{i=0}^{+∞} L^i\bigg) · T_{filterExtract} \end{equation*}

无穷级数在有限时间内计算起来很不方便。 尽管由于各项按指数级缩小,该级数会收敛到一个有限值。 我们可以计算到各项足够小以得到近似解, 但当存在精确解时,这种做法终究令人不满足。

长期平均吞吐量

关键在于:某件物品已经循环过多少次并不重要,重要的是它当前处于哪个品质等级。短期内,由于品质模块的随机效果,这个数值会波动,但只要重复次数够多,概率就会变成比例。

所以,我们不再考虑单件物品的概率,而是考虑在足够长的时间段内,流经系统某一部分的物品平均速率,同样用 5 个分量的品质等级向量表示。这些向量可以和之前一样乘以转移矩阵。以“清洗”型纯回收循环为例,相关向量包括:

  • fresh:注入系统的全新物品,品质分布任意,本例中取决于采矿机上的品质模块
  • fromRecycling:回收后产出的物品
  • splitter:流经分流器的所有物品
  • extracted:按品质等级筛出的物品,本例中为稀有及以上
  • toRecycle:品质为 q、未被筛出、需要回收的物品

我们把 q 和 fresh 视为固定参数,其余向量作为待求的未知量。

系统会收敛到一个动态平衡状态,长期平均值满足以下方程组:

splitter=fresh+fromRecyclingsplitter=extracted+toRecycleextracted=splitter⋅TfilterExtracttoRecycle=splitter⋅TfilterKeepfromRecycling=toRecycle⋅14Tquality(q) \begin{align*} splitter &= fresh + fromRecycling \\ splitter &= extracted + toRecycle \\ extracted &= splitter ⋅ T_{filterExtract} \\ toRecycle &= splitter ⋅ T_{filterKeep} \\ fromRecycling &= toRecycle ⋅ \frac{1}{4} T_{quality}(q) \end{align*}

我们可以进行变形和代入:

splitter=fresh+fromRecyclingsplitter−fromRecycling=freshsplitter−toRecycle⋅14Tquality(q)=freshsplitter−splitter⋅TfilterKeep⋅14Tquality(q)=freshsplitter⋅(I5−TfilterKeep⋅14Tquality(q))=fresh \begin{align*} splitter = fresh + fromRecycling \\ splitter - fromRecycling = fresh \\ splitter - toRecycle ⋅ \frac{1}{4} T_{quality}(q) = fresh \\ splitter - splitter ⋅ T_{filterKeep} ⋅ \frac{1}{4} T_{quality}(q) = fresh \\ splitter ⋅ (I_5 - T_{filterKeep} ⋅ \frac{1}{4} T_{quality}(q)) = fresh \\ \end{align*}

对矩阵取转置,将行向量转换为列向量,以符合 经典约定:

(splitter⋅(I5−TfilterKeep⋅14Tquality(q)))T=freshT(I5−TfilterKeep⋅14Tquality(q))T⋅splitterT=freshT \begin{align*} (splitter ⋅ (I_5 - T_{filterKeep} ⋅ \frac{1}{4} T_{quality}(q)))^T &= fresh^T \\ (I_5 - T_{filterKeep} ⋅ \frac{1}{4} T_{quality}(q))^T ⋅ splitter^T &= fresh^T \end{align*}

引入几个新的记号:

A=(I5−TfilterKeep⋅14Tquality(q))Tx=splitterTb=freshT \begin{align*} A &= (I_5 - T_{filterKeep} ⋅ \frac{1}{4} T_{quality}(q))^T \\ x &= splitter^T \\ b &= fresh^T \end{align*}

现在,平衡态可表示为经典 $A \cdot x = b$ 形式的线性方程组, 我们可用 高斯消元法 求解 $x$。 由 $x$ 可以轻易算出 $splitter$, 进而得到 $toRecycle$(所需回收器数量) 和 $extracted$(系统总产出)。

若将上述示例配置调整为每秒开采 100 个矿石,参数将变为:

fresh=(9090.90.090.01)TfilterExtract=filter(≥rare)q=10% \begin{align*} fresh &= \begin{pmatrix*} 90 & 9 & 0.9 & 0.09 & 0.01 \end{pmatrix*} \\ T_{filterExtract} &= filter(≥ rare) \\ q &= 10\% \end{align*}

用高斯消元法求得的解为:

toRecycle≈(116.129114.9844000)extracted≈(001.49850.14990.0167) \begin{align*} toRecycle &≈ \begin{pmatrix*} 116.1291 & 14.9844 & 0 & 0 & 0 \end{pmatrix*} \\ extracted &≈ \begin{pmatrix*} 0 & 0 & 1.4985 & 0.1499 & 0.0167 \end{pmatrix*} \\ \end{align*}

换算成每分钟,产出速率接近:稀有品 90 个、史诗品 9 个、传说品 1 个。
再举一个例子:
  • 仅注入普通品质的新鲜物品,暂时以任意单位表示:

    fresh=(10000)fresh = \begin{pmatrix*} 1 & 0 & 0 & 0 & 0 \end{pmatrix*}

  • 使用最优品质模组进行回收:p>q = 24.8\%

  • 仅提取传说品质的物品

解方程得到:

toRecycle≈(1.2315280.084630.0142790.002410)extracted≈(00000.000366716) \begin{align*} toRecycle &≈ \begin{pmatrix*} 1.231528 & 0.08463 & 0.014279 & 0.00241 & 0 \end{pmatrix*} \\ extracted &≈ \begin{pmatrix*} 0 & 0 & 0 & 0 & 0.000366716 \end{pmatrix*} \\ \end{align*}

对于那些回收后仍变成自身的物品,这种“暴力”提纯方式平均需要消耗 1extracted⚄≈2726.91\frac{1}{extracted_⚄} ≈ 2726.91 个普通品质原料,才能产出 1 个传奇品质成品。 这一比例与其他人计算出的结果一致。 所需的回收容量略低于新鲜原料输入速率的 43\frac{4}{3} 。

“升级提纯”:制造与回收循环

对于回收确实能返还原料的物品, 我们可以将制造机和回收机串联,形成一个循环回路:

将构建机器人升级提纯至传奇品质的示例布局。

大多数制造配方需要多种不同数量的原料, 但在此我们将抽象看待,把“单次制造所需的原料集合” 作为测量的基本单位。 在这个例子中,每台制造构建机器人的机器每秒可消耗 4 个电子电路和 2 个飞行机器人框架, 但我们将此简称为“每秒 2 单位原料”。

因此,我们测量的是原料和成品的长期平均速率,它们各自分布在五个品质等级上。 但由于它们只出现在系统的不同部分,所以我们依然使用 5 分量向量, 无需升级到 10 维数学模型。 (这里有一个伏笔,心照不宣)

在这个示例布局中,普通品质的新鲜原料由机器人放入蓝色请求箱, 并提取传奇品质的成品。 在一般情形下,我们可以设想任何品质分布的原料或成品 都在系统外生产并注入其中。 同样地,我们可以决定提取任意品质等级的成品或原料(或者两者皆提取)。 (有时,高品質的原料可能比成品更有用, 但针对特定成品的升级提纯或许比其他方式产出率更高。)

因此,我们将考虑的 5 分量行向量,针对原料而言,包括:

  • 注入系统的新鲜原料 freshIfreshI,具有任意品质分布
  • 来自回收的原料 fromRecyclingfromRecycling
  • 总原料输入 totalItotalI
  • extractedIextractedI 按质量等级提取的原料,本例中没有
  • 用于 toCrafttoCraft 制造新产品的原料

对于产物:

  • freshPfreshP 注入系统的全新产物,质量分布任意
  • fromCraftingfromCrafting 制造产生的产物
  • totalPtotalP
  • extractedPextractedP 按质量等级提取的产物,本例中是传奇品质
  • toRecycletoRecycle 待回收的产物

我们把 freshIfreshI 和 freshPfreshP 视为固定参数,其余向量则是要求解的未知量。

同样,按质量等级过滤的转移矩阵是单位矩阵的互补部分。在本例中:

TfilterKeepingredients=I5TfilterExtractingredients=05TfilterKeepproducts=(1000001000001000001000000)TfilterExtractproducts=(0000000000000000000000001)=I5−TfilterKeepproducts \begin{align*} T^{ingredients}_{filterKeep} &= I_5 \\ T^{ingredients}_{filterExtract} &= 0_5 \\ T^{products}_{filterKeep} &= \begin{pmatrix*} 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 \end{pmatrix*} \\ T^{products}_{filterExtract} &= \begin{pmatrix*} 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 \end{pmatrix*} = I_5 - T^{products}_{filterKeep} \end{align*}

这里还考虑了生产率加成 productivityBonusproductivityBonus,本例中为 0%,但使用某些生产型组装机或生产效率模块后可以更高。制造和回收的品质概率也可能不同(例如使用生产效率模块制造时,或取决于机器的模块插槽数量)。

为简便起见,记:

Tcraft=(1+productivityBonus)⋅Tquality(qcrafting)Trecycle=14Tquality(qrecycling) \begin{align*} T_{craft} &= (1 + productivityBonus) ⋅ T_{quality}(q_{crafting}) \\ T_{recycle} &= \frac{1}{4} T_{quality}(q_{recycling}) \end{align*}

整个系统最终会收敛到如下平衡状态:

totalI=freshI+fromRecyclingtotalI=extractedI+toCraftextractedI=totalI⋅TfilterExtractingredientstoCraft=totalI⋅TfilterKeepingredientsfromCrafting=toCraft⋅TcrafttotalP=freshP+fromCraftingtotalP=extractedP+toRecycleextractedP=totalP⋅TfilterExtractproductstoRecycle=totalP⋅TfilterKeepproductsfromRecycling=toRecycle⋅Trecycle \begin{align*} totalI &= freshI + fromRecycling \\ totalI &= extractedI + toCraft \\ extractedI &= totalI ⋅ T^{ingredients}_{filterExtract} \\ toCraft &= totalI ⋅ T^{ingredients}_{filterKeep} \\ fromCrafting &= toCraft ⋅ T_{craft} \\[2em] totalP &= freshP + fromCrafting \\ totalP &= extractedP + toRecycle \\ extractedP &= totalP ⋅ T^{products}_{filterExtract} \\ toRecycle &= totalP ⋅ T^{products}_{filterKeep} \\ fromRecycling &= toRecycle ⋅ T_{recycle} \end{align*}

对其变形并代入可得:

totalP=freshP+fromCraftingtotalP=freshP+toCraft⋅TcrafttotalP=freshP+totalI⋅TfilterKeepingredients⋅TcrafttotalP=freshP+(freshI+fromRecycling)⋅TfilterKeepingredients⋅TcrafttotalP=freshP+(freshI+toRecycle⋅Trecycle)⋅TfilterKeepingredients⋅TcrafttotalP=freshP+(freshI+totalP⋅TfilterKeepproducts⋅Trecycle)⋅TfilterKeepingredients⋅Tcraft \begin{align*} totalP &= freshP + fromCrafting \\ totalP &= freshP + toCraft ⋅ T_{craft} \\ totalP &= freshP + totalI ⋅ T^{ingredients}_{filterKeep} ⋅ T_{craft} \\ totalP &= freshP + (freshI + fromRecycling) ⋅ T^{ingredients}_{filterKeep} ⋅ T_{craft} \\ totalP &= freshP + (freshI + toRecycle ⋅ T_{recycle}) ⋅ T^{ingredients}_{filterKeep} ⋅ T_{craft} \\ totalP &= freshP + (freshI + totalP ⋅ T^{products}_{filterKeep} ⋅ T_{recycle}) ⋅ T^{ingredients}_{filterKeep} ⋅ T_{craft} \end{align*}

再引入两个变量名:

TfilterRecycle=TfilterKeepproducts⋅TrecycleTfilterCraft=TfilterKeepingredients⋅TcrafttotalP=freshP+(freshI+totalP⋅TfilterRecycle)⋅TfilterCrafttotalP−totalP⋅TfilterRecycle⋅TfilterCraft=freshP+freshI⋅TfilterCrafttotalP⋅(I5−TfilterRecycle⋅TfilterCraft)=freshP+freshI⋅TfilterCraft(I5−TfilterRecycle⋅TfilterCraft)T⋅totalPT=(freshP+freshI⋅TfilterCraft)T \begin{align*} \begin{align*} T_{filterRecycle} &= T^{products}_{filterKeep} ⋅ T_{recycle} \\ T_{filterCraft} &= T^{ingredients}_{filterKeep} ⋅ T_{craft} \\[1em] totalP &= freshP + (freshI + totalP ⋅ T_{filterRecycle}) ⋅ T_{filterCraft} \\ \end{align*}\\ \begin{align*} totalP - totalP ⋅ T_{filterRecycle} ⋅ T_{filterCraft} &= freshP + freshI ⋅ T_{filterCraft} \\ totalP ⋅ (I_5 - T_{filterRecycle} ⋅ T_{filterCraft}) &= freshP + freshI ⋅ T_{filterCraft} \\ (I_5 - T_{filterRecycle} ⋅ T_{filterCraft})^T ⋅ totalP^T &= (freshP + freshI ⋅ T_{filterCraft})^T \end{align*} \end{align*}

我们再次将问题整理为标准 A·x=bA·x = b 形式,其中:

A=(I5−TfilterRecycle⋅TfilterCraft)Tx=totalPTb=(freshP+freshI⋅TfilterCraft)T \begin{align*} A &= (I_5 - T_{filterRecycle} ⋅ T_{filterCraft})^T \\ x &= totalP^T \\ b &= (freshP + freshI ⋅ T_{filterCraft})^T \end{align*}

解出 xx 即可得到 totalPtotalP,将其代入原始平衡方程,其余变量也就随之求得。

在上述示例配置中(暂时缩放为任意单位),参数如下:

freshIunit=(10000)freshP=(00000)TfilterExtractingredients=filter(nothing)TfilterExtractproducts=filter(legendary)productivityBonus=0%qcrafting=24.8%qrecycling=24.8% \begin{align*} freshI_{unit} &= \begin{pmatrix*} 1 & 0 & 0 & 0 & 0 \end{pmatrix*} \\ freshP &= \begin{pmatrix*} 0 & 0 & 0 & 0 & 0 \end{pmatrix*} \\ T^{ingredients}_{filterExtract} &= filter(nothing) \\ T^{products}_{filterExtract} &= filter(legendary) \\ productivityBonus &= 0\% \\ q_{crafting} &= 24.8\% \\ q_{recycling} &= 24.8\% \end{align*}

通过高斯消元法求解:

toCraftunit≈(1.1646550.1138360.0394040.0111330.002154)toRecycleunit≈(0.875820.3455550.0810350.0223070)extractedIunit=(00000)extractedPunit≈(00000.006464) \begin{align*} toCraft_{unit} &≈ \begin{pmatrix*} 1.164655 & 0.113836 & 0.039404 & 0.011133 & 0.002154 \end{pmatrix*} \\ toRecycle_{unit} &≈ \begin{pmatrix*} 0.87582 & 0.345555 & 0.081035 & 0.022307 & 0 \end{pmatrix*} \\ extractedI_{unit} &= \begin{pmatrix*} 0 & 0 & 0 & 0 & 0 \end{pmatrix*} \\ extractedP_{unit} &≈ \begin{pmatrix*} 0 & 0 & 0 & 0 & 0.006464 \end{pmatrix*} \end{align*}

实际运行中,这套系统的瓶颈在于普通品质产品的制造速度:每分钟 360 个。于是我们把所有数值乘以 360toCraftunit⚀\frac{360}{toCraft_{unit_⚀}}

freshIscaled≈(309.100000)toCraftscaled≈(36035.1812.173.440.66)toRecyclescaled≈(270.72106.8125.046.890)extractedPscaled≈(00001.99) \begin{align*} freshI_{scaled} &≈ \begin{pmatrix*} 309.10 & 0 & 0 & 0 & 0 \end{pmatrix*} \\ toCraft_{scaled} &≈ \begin{pmatrix*} 360 & 35.18 & 12.17 & 3.44 & 0.66 \end{pmatrix*} \\ toRecycle_{scaled} &≈ \begin{pmatrix*} 270.72 & 106.81 & 25.04 & 6.89 & 0 \end{pmatrix*} \\ extractedP_{scaled} &≈ \begin{pmatrix*} 0 & 0 & 0 & 0 & 1.99 \end{pmatrix*} \end{align*}

结论:这套系统平均每分钟产出约 2 台传奇品质建造机器人,同时每分钟消耗约 309 组原材料(309 个机框 + 618 块电路板)。


Space Age 中只有少数几件物品, 可以通过可重复的科技研究来提升生产力加成。 游戏对加成设有 +300% 的硬上限, 这样一来,制造后再回收,最多也只能拿回投入的原材料,绝不可能更多。

生产力科技每升一级成本都呈指数级增长, 所以我们假设改用生产力模块来达到上限, 也就是说制造机里不再放品质模块。 这次,我们注入的是成品,而不是原材料。

freshI=(0 0 0 0 0) freshP_unit=(1 0 0 0 0) T_filterExtract^ingredients=filter(nothing) T_filterExtract^products=filter(legendary) productivityBonus=+300% q_crafting=0% q_recycling=24.8% \begin{align*} freshI &= \begin{pmatrix*} 0 & 0 & 0 & 0 & 0 \end{pmatrix*} \\ freshP_{unit} &= \begin{pmatrix*} 1 & 0 & 0 & 0 & 0 \end{pmatrix*} \\ T^{ingredients}_{filterExtract} &= filter(nothing) \\ T^{products}_{filterExtract} &= filter(legendary) \\ productivityBonus &= +300\% \\ q_{crafting} &= 0\% \\ q_{recycling} &= 24.8\% \end{align*}

我们的模型预测:

toCraft_unit≈(0.758 0.907 0.907 0.907 0.25) toRecycle_unit≈(4.032 3.629 3.629 3.629 0) extractedI_unit=(0 0 0 0 0) extractedP_unit=(0 0 0 0 1) \begin{align*} toCraft_{unit} &≈ \begin{pmatrix*} 0.758 & 0.907 & 0.907 & 0.907 & 0.25 \end{pmatrix*} \\ toRecycle_{unit} &≈ \begin{pmatrix*} 4.032 & 3.629 & 3.629 & 3.629 & 0 \end{pmatrix*} \\ extractedI_{unit} &= \begin{pmatrix*} 0 & 0 & 0 & 0 & 0 \end{pmatrix*} \\ extractedP_{unit} &= \begin{pmatrix*} 0 & 0 & 0 & 0 & 1 \end{pmatrix*} \end{align*}

或许出人意料的是,中间质量等级的比率完全相同。

最大生产力加成允许将普通品质的产品转化为传奇品质且无任何资源损耗,但代价是需要大量机器和模块才能获得显著的吞吐量。

“太空赌场”:小行星再加工循环

在太空时代,每艘太空平台都是一个微型工厂。它们不再从地面开采矿石,而是收集小行星碎片并粉碎以获取资源。这些资源可用于太空船自身的燃料和弹药,或为太空生产链提供输入,最终将成品送回行星地面。

三种不同类型的碎片(金属、碳质和氧化物)产出不同的资源,且在不同区域的出现频率各异。小行星碎片可以“再加工”以一定概率获得不同类型的碎片(或什么都没有)。

小行星再加工配方。 来源:Factorio 维基

假设拥有足够多的粉碎机来应对每种配方,我们可以用数学方式将此过程表示为行向量乘以一个 3×3 的方阵转移矩阵:

$fromReprocessing=toReprocess⋅TreprocessingTreprocesing=(40%20%20%20%40%20%20%20%40%) \begin{align*} fromReprocessing &= toReprocess ⋅ T_{reprocessing} \\[1em] T_{reprocesing} &= \begin{pmatrix*} 40\% & 20\% & 20\% \\ 20\% & 40\% & 20\% \\ 20\% & 20\% & 40\% \end{pmatrix*} \end{align*}

重处理机也支持品质模组,因此可以像“清洗”一样用于循环。 粉碎传奇星块会产出一些基础资源的传奇版本,这些资源可用于制作其他传奇物品。

这次我们使用传送带运输三种类型、每种五个品质等级的星块碎块,共计 15 种可能的物品。 我们将用 15 分量行向量和 15×15 转移矩阵来表示这种状态。 后者构建为由九个 5×5 块组成的分块矩阵:

$Treprocessing15=(40%⋅I520%⋅I520%⋅I520%⋅I540%⋅I520%⋅I520%⋅I520%⋅I540%⋅I5)Tquality15(q)=(Tquality(q)000Tquality(q)000Tquality(q))TfilterKeep15=(TfilterKeepMetallic000TfilterKeepCarbonic000TfilterKeepOxide)total=fresh+fromReprocessingfromReprocessing=toReprocess⋅Treprocessing15⋅Tquality15(q)toReprocess=total⋅TfilterKeep15extracted=total⋅TfilterExtract15=total⋅(I15−TfilterKeep15) \begin{align*} T^{15}_{reprocessing} &= \begin{pmatrix*} 40\% ⋅ I_5 & 20\% ⋅ I_5 & 20\% ⋅ I_5 \\ 20\% ⋅ I_5 & 40\% ⋅ I_5 & 20\% ⋅ I_5 \\ 20\% ⋅ I_5 & 20\% ⋅ I_5 & 40\% ⋅ I_5 \end{pmatrix*} \\[2em] T^{15}_{quality}(q) &= \begin{pmatrix*} T_{quality}(q) & 0 & 0 \\ 0 & T_{quality}(q) & 0 \\ 0 & 0 & T_{quality}(q) \end{pmatrix*} \\[2em] T^{15}_{filterKeep} &= \begin{pmatrix*} T_{filterKeepMetallic} & 0 & 0 \\ 0 & T_{filterKeepCarbonic} & 0 \\ 0 & 0 & T_{filterKeepOxide} \end{pmatrix*} \\[2em] total &= fresh + fromReprocessing \\ fromReprocessing &= toReprocess ⋅ T^{15}_{reprocessing} ⋅ T^{15}_{quality}(q) \\ toReprocess &= total ⋅ T^{15}_{filterKeep} \\ extracted &= total ⋅ T^{15}_{filterExtract} = total ⋅ (I_{15} - T^{15}_{filterKeep}) \end{align*}

除了维度更高,数学推导与“清洗”过程相同。 最终我们得到一个线性方程组 $A \cdot x = b$,需求解出 $x$,其中:

A=(I15−TfilterKeep15⋅Treprocessing15⋅Tquality15(q))Tx=totalTb=freshT \begin{align*} A &= (I_{15} - T^{15}_{filterKeep} ⋅ T^{15}_{reprocessing} ⋅ T^{15}_{quality}(q))^T \\ x &= total^T \\ b &= fresh^T \\ \end{align*}

小行星破碎机有两个模块插槽,因此重加工时能达到的最高品质概率是 q=12.4%q = 12.4\%。小行星采集器没有模块插槽,所以新采集的碎块永远是普通品质。我们用三种小行星类型(金属、碳质、氧化物)各自的普通品质产出率,构建包含 15 个分量的 freshfresh 向量。例如在 Nauvis 轨道上: fresh⚀=(362616)fresh_⚀ = \begin{pmatrix*} \frac{3}{6} & \frac{2}{6} & \frac{1}{6} \end{pmatrix*}

假设我们只提取传奇品质的氧化物碎块,其余全部重加工。求解方程会得到两个互补的 15 分量行向量 toReprocesstoReprocess 和 extractedextracted,可以把它们整理成 3×5 的表格。

示例:在 Nauvis 轨道上,提取传奇品质氧化物碎块,其余全部重加工:

fresh⚀=(362616)q=12.4% \begin{align*} fresh_⚀ &= \begin{pmatrix*} \frac{3}{6} & \frac{2}{6} & \frac{1}{6} \end{pmatrix*} \\ q &= 12.4\% \end{align*}

待重加工
⚀⚁⚂⚃⚄
金属 1.316160.337910.133140.052860.0175
碳质 1.114090.332440.132450.052770.01748
氧化物 0.912020.326970.131750.052680
被提取
⚀⚁⚂⚃⚄
金属 00000
碳质 00000
氧化物 00000.01398

可以观察到:

  • 品质等级越高,三种小行星类型的分布就越快收敛到均等的 13\frac{1}{3}。普通品质新碎块的类型分布,对系统中传奇品质碎块的分布几乎没有影响。
  • 提取单一类型传奇小行星时,传奇产量约为总新鲜投入量的 $\frac{1}{71.5} \approx 1.4\%$。这远优于“洗涤”(即纯回收)的 $\frac{1}{2726}$ 左右,原因在于再加工仅以 20% 的概率销毁输入物,而回收则以 75% 的概率销毁。

其他策略

还有其他方式可以获得品质物品,它们不完全适合上述分类(特别提及 “LDS 洗牌法”),我相信大家还能想出更多。但如果某个循环使得行为计算变得复杂,我们可以使用相同的方法:

  • 将多种物品的吞吐量表示为向量
  • 将线性变换(制造、回收等)表示为矩阵乘法
  • 将动态平衡表示为矩阵方程
  • 求解该方程,必要时使用高斯消元法

制作交互式计算器工具

手动进行矩阵运算显然既繁琐又易错,让我们让计算机代劳。

出于习惯,我从 Rust 入手。nalgebra 非常适合我们这里用到的矩阵运算。它包含多个 $A \cdot x = b$ 线性方程组求解器,但这些求解器通常要求矩阵和向量组件的标量类型为浮点数 f32 或 f64,而基础库则更为通用。

实际上,浮点数就完全足够用了,但它是固定精度的近似值,因此计算的每一步都可能会引入误差。既然我们可以做到,何不追求精确结果呢?矩阵尺寸和运算次数都是固定的且相对较小,因此无需为速度优化代码。

我们使用的每个操作归根结底都是加法、减法、乘法或除法。因此,如果所有参数都是有理数,结果也将是有理数。num_rational 库将有理数表示为一对(泛型)整数,精确度很高。如果需要,我可以引入任意精度的 BigInt 库,但事实证明,内置的 i128 配合溢出检查对于 15×15 的矩阵已经足够了。(对于 5×5 矩阵,i64 就够用了。)

到这一步,我有了一个功能完整的 Rust 库,可以执行上述所有计算。但编辑源代码来调整参数并非良好的用户体验。我更希望它像 Factoriolab 那样友好。而且,为了便于他人使用,它真的应该部署在 Web 上。

我的 Rust 代码可以编译为 WebAssembly,但之后我需要二选一:

  • 同样用 Rust + wasm 构建整个 GUI。这可行,但目前的工具链体验还不佳
  • 用 JavaScript 或 TypeScript(借助成熟的工具链)构建 GUI,并通过 wasm 进行数学计算的桥接。但由于参数众多,API 接口面很大,做这么多桥接工作并不愉快

因此,我最终用 TypeScript 重写了整个项目。BigInt 是内置的,Factoriolab 已经有一个不错的开源rational 库,而且编写一个通用的矩阵库也不难。

至于在浏览器中构建交互式 GUI,上次我这么干的时候,jQuery 还是当时的大热门。我不太想学 React,于是最终选择了:

  • VanJS 提供极简的响应式功能
  • Vite 用于处理 TypeScript 及提供保存即重载的开发服务器
  • Grebedoc 用于静态文件托管

总之,factoqual.grebedoc.dev 现在提供了一个交互式界面,方便规划 Quality 升级机。源代码发布在 codeberg.org/SimonSapin/factoqual。

原始来源: Hacker News

评论 (0)