跳到主要内容

游戏数值设计

RPG 公式与成长曲线、放置的指数驯服、消除的棋盘结构与字母概率的定价

周日 8月 16 2026
19691 字 · 68 分钟

休闲小游戏看着轻,数值和算法却一点不含糊:RPG 的战斗公式与成长曲线、放置游戏要驯服的指数曲线、消除游戏先选好的棋盘数学结构、单词游戏给字母概率的定价,底层是同一门手艺。本文把站内几组合计二十来个组件拆开,看数值设计如何在不同品类里落地。

RPG 数值:公式、曲线与闸门

每个论点配一个实验台。动手版本:战斗回合模拟器、武器伤害计算、怪物图鉴、技能树、法术书、背包、卡组构筑、DND 角色卡。

伤害公式三流派

第一处分岔:防御属性怎么进公式。

减法流派最直观:伤害 = 攻击 − 防御。

战斗回合模拟器用的就是这一派。每次攻击先掷命中,然后结算:

const variance = Math.floor(Math.random() * 3); // 0..2 的浮动
const real = Math.max(1, att.atk - Math.floor(Math.random() * (att.def + 1))) + variance;

防御减伤不是固定值,是 00 到 def\mathrm{def} 之间均匀取整。期望减 def/2\mathrm{def}/2 点,方差随 (def+1)2/12(\mathrm{def}+1)^2/12 增长。

算笔账:勇者攻 12 打兽人防 3,期望 12−1.5+1=11.512 - 1.5 + 1 = 11.5,乘 90% 命中得 10.35。反过来兽人打勇者,10−2+1=910 - 2 + 1 = 9 乘 80% 得 7.2。

双方血量 50 对 40,纸面推算勇者约 4 回合击倒、兽人约 7 回合。模拟器跑出来的胜率和这个一致。

max⁡(1,⋅)\max(1,\cdot) 保底让减法永远打不死人也永远打得动人。防御堆过攻击,伤害不归零,压到 1-3 点。

但减法有个著名失衡:每点防御固定减 0.5 伤,相对减伤率却加速膨胀。从 10 压到 9 是 10% 减免,从 2 压到 1 是 50%。后期防御边际收益指数上升,必须用「防御收益递减条款」补洞。

乘法流派用除法把减免率关进笼子:dmg=atk×KK+def\mathrm{dmg} = \mathrm{atk} \times \frac{K}{K + \mathrm{def}}。

每点防御绝对收益严格递减,永不归零伤害。KK 是曲率旋钮。

主流网游几乎全是这一派。乘法保证防御线性投入换来递减但可预测的生存提升,数值模型永远可解。

第三派把防御做成命中机制。D&D 的 AC 不减伤,只决定 d20 加修正能不能过线。防御每一分都变成「完全免疫本次攻击」的概率,伤害期望 = 命中率 × 骰伤期望。

三派心理体验完全不同:减法像挤压、乘法像稀释、AC 像躲避。

骰子流派的价值不止于复古。武器伤害计算把 NdS+B\mathrm{NdS} + B 完全参数化:骰子数、面数、固定加成、暴击倍率,三个按钮分别做单次掷骰、解析期望和千次模拟。

期望伤害是教科书公式:

E=N⋅S+12+BE = N \cdot \frac{S+1}{2} + B

真正区分设计水平的是方差:Var=N(S2−1)/12\mathrm{Var} = N(S^2-1)/12。同样期望 10.5,3d63\mathrm{d}6 标准差约 2.9,固定伤害是零,单发大骰子长尾宽得多。

暴击在源码里是 10% 概率把骰子和乘以倍率再加固定值,期望是 E=D(0.9+0.1c)+BE = D(0.9 + 0.1c) + B。暴击倍率抬乘数、固定加成抬底线,堆叠路径不同,这就是「暴击流」与「面板流」在数学上永远不等价的原因。

千次模拟框给出平均伤害、击杀数与 DPS。对照解析期望就能看到蒙特卡洛的收敛噪声。

读源码有个细节:模拟里失手已记零摊进平均伤害分母,结果面板的 DPS 又乘了一次命中率,等于把失手惩罚算了两遍。另外「目标护甲」输入框在结算代码里从未被引用,是预留接口。

数值系统的三个口径(单发、期望、模拟统计)必须对齐定义,否则面板自相矛盾。

命中、CR 与攻防天平

命中率是战斗数值里最反直觉的属性。

直觉上「少 10% 命中就是少 10% 输出」,但一次 miss 的挫败远大于伤害少一成,行为经济学叫损失厌恶的战斗版。

模拟器把双方命中率默认成 90% 对 80% 而不是 100%,恰好让这个现象可复现。把勇者命中调到 100% 再调回 90%,胜率变化不大,但战报里那几行「伤害 0」的观感完全不同。

成熟做法有两种:要么像乘法流派把命中率藏进命中超额,要么像 XCOM 在底层偷偷给连败加概率,把几何分布的冷 streak 人为截短。

敌人那侧的数值载体是 CR(挑战等级)。

怪物图鉴收了十只怪,从 CR 0.25、7 点血的哥布林到 CR 21 的巫妖,每只带六维属性与攻防面板,可以按人形/野兽/亡灵/龙/巨人/异怪筛选。

表里藏着一个反直觉事实:CR 21 的巫妖血量(135)低于 CR 10 的青年红龙(178)。

如果难度由血量承载,这个倒挂就是设计事故。但打开六维就能看到补偿:巫妖智力 20 对红龙 14,AC 与敏捷也更高,难度装进了施法与控制能力里。

CR 系统的核心:挑战等级不是任何单一属性的函数,而是「这队冒险者解决它要消耗多少资源」的综合估计。血量、护甲、输出、控制各承载一部分。

哥布林 CR 0.25 却是经典开局怪,因为这个体系允许四只 CR 0.25 堆出一场 CR 1 的遭遇战。怪物可以靠数量换等级,难度预算是可分解的。

六维属性在这里的角色也得说一下。

图鉴里属性是 3 到 20 的「属性值」,战斗公式吃的是「修正值」,两者靠一条挖矿式映射连接:mod=⌊(score−10)/2⌋\mathrm{mod} = \lfloor (\mathrm{score}-10)/2 \rfloor。

10 是凡人基准,每过 2 点属性才换来 1 点修正。10 与 11 没区别,18 对 10 是 +4 对 +0。

这条非线性映射是整个 D&D 式数值体系的地基。属性值给玩家一个「越大越好」的直觉界面,修正值才是公式里的真实汇率。「每 2 点一档」让偶数属性成为永久购点目标。

红龙力量 23 折算 +6,巫妖智力 20 折算 +5,数字都不大。这个体系宁可在属性表上显得华丽,也不让任何单项修正值跑出两位数。

成长曲线

成长系统的全部分歧浓缩在一个问题:等级每升一级,你强多少?

DND 角色卡给了三份标准答案。

生命值是线性的:HP=base+conMod×level\mathrm{HP} = \mathrm{base} + \mathrm{conMod} \times \mathrm{level}。职业基础值(战士 10、法师 6、牧师 8)加体质修正乘等级,每级增量恒定。

一级与二十级之间是六十点体质项的线性展开,职业间那 4 点血的基础差会被成长项稀释到几乎不可见。

熟练加值是阶梯函数:2+⌊(level−1)/4⌋2 + \lfloor (\mathrm{level}-1)/4 \rfloor。在 5、9、13、17 级各跳 1 点,二十级里总共只涨 4 点。这是「有界精度」哲学:数字必须保持小,让 d20 的随机性永远有话语权。

护甲更是常数级的:AC=10+dexMod\mathrm{AC} = 10 + \mathrm{dexMod},与等级无关。

三条曲线组合出一个刻意的设计立场:角色的绝对数值几乎不膨胀,膨胀的是能力清单。代价是攻击加值随熟练度缓涨而 AC 不涨,命中会随等级悄悄爬坡,需要用魔法物品把 AC 补回去。

线性是保守解,指数是放置类游戏的引擎。

指数成长的特点是每级增幅按比例放大,玩家前期每次升级都体验到「质变」,代价在第 73 次乘方之后才显形:数值冲出浮点精度、所有内容被一两刀秒杀。这条曲线的收尾处理(prestige 重置、软上限)才是放置游戏真正的设计战场。

幂函数(HP∝levelk\mathrm{HP} \propto \mathrm{level}^{k},kk 取 1.5 到 2)是两者折中:比线性陡、比指数收敛。怪物血量随等级走幂律时,「打几刀死」的体感才能跨等级稳定。

曲线的选择最终泄漏到「边际收益」这个统一尺度上。

拿血量公式算两笔账:体质 16(修正 +3)的战士,一级 10+3=1310 + 3 = 13,二十级 10+60=7010 + 60 = 70。同体质法师一级 6+3=96 + 3 = 9,二十级 66。

开局 44% 的血量差到满级只剩 6%。线性成长项随等级稀释了职业基础差,这是「职业个性在后期消失」的数学成因,也是后期靠装备与技能重开差异的动机。

反过来如果血量走指数,职业差按比例保留,但二十级战士与十级战士之间隔着一整个数量级,内容设计再也无法用「同一张遭遇表」覆盖。

每种曲线都在回答两个问题:玩家每级获得多少绝对提升,以及不同起点的玩家以什么速率趋同或分化。前者是节奏问题,后者是平衡问题,一条曲线同时定两者。

三种曲线没有高下,只有与内容消耗节奏的匹配问题。指数曲线的失控与收束,下一节「放置游戏」里拆得很细。

技能树最短路

技能树的树不大,九个节点四层。根节点 Core 分出战斗与魔法两系,战斗系下面是劈砍与守御,魔法系下面是火焰与冰霜。第三层有两个终点:旋风斩要求劈砍与守御同时点亮,陨石术只要求火焰。

总点数预算 5,未满足前置的节点锁死,点过的节点可以再点一次退款。

这个小树其实是一张有向无环图上的约束优化题。每个节点是一个任务,前置是 AND 依赖,预算是最多五步。

「点出旋风斩」的最短路径是 Core → 战斗 → 劈砍 → 守御 → 旋风斩,恰好五点吃满预算。「点出陨石术」只要 Core → 魔法 → 火焰 → 陨石,四点,剩下一格还能顺手点亮冰霜。

同一个预算下两条构筑的存在性、互斥性,都是图上可达集的纯计算。

天赋设计的老三样问题在这里都能演习。

其一,深度换强度还是广度换强度。旋风斩路线五点全押换一个终点,陨石路线四点还剩一格机动。深路线的终点必须显著强于浅路线的组合,否则没人走深。

其二,退款规则。这个实现里退款不检查下游。先点亮旋风斩再退掉劈砍,旋风斩依然亮着,依赖图出现悬空。真实游戏的通用规则是「退点必须级联回收下游」,源码里没有,正好当作反例教材。

其三,重置成本。免费无限重置的天赋树会退化成查表,正式作品要么收重置道具、要么用「洗点保留等级但收金币」给试错定价。

法术位的另一套闸门

法术书从另一个维度展示资源闸门。二十个法术按环阶 0 到 4 分层,按八个学派可筛,每个条目带施法时间与描述。

环阶是纵向的获取闸门。几环法术位只能装几环法术,法术位本身是一次性的(长休回满)。施法者的输出预算从「无冷却随便丢」变成「每天几发高环」的配给制。

法术位是少数会跨战斗结算的攻击资源,这让法师的强与弱都发生在决策层而不是面板层。

施法时间那一栏同样有内容。动作、附赠动作、反应三类把「何时能施法」也编码成了资源。反应尤其精妙,它消费的是「轮不到你的时刻」,等于在回合制里开辟了一条免费的第二动作经济。

学派过滤则是纯分类学。塑能、幻术、咒法、防护、惑控、死灵、变化、预言八大学派对机制几乎不做硬约束,却承担了构筑期的检索与叙事标签。一个玩家说「我玩幻术师」,这套分类就替他完成了半个角色设定。

魔法飞弹的「必中」描述值得单独看。它把命中检定直接改写成自动命中、伤害只保留骰子部分,这是设计师主动切除 RNG 最恶劣体验(对着空地丢高环法术)的标准手术。

背包、掉落与资源闸门

背包给了 20 个槽位(5×4 网格)、15 种道具、6 个类别。「添加」按钮从还没放进包里的道具池均匀抽一件塞进第一个空格,同一件道具不能重复存在,装满会明确报错。选中道具可以装备(移出网格进入装备栏)或移除。

这看似是个仓库管理玩具,但槽位制背包是 RPG 里最重要的一项隐性经济。格子是货币,捡起任何东西都要付「占用空间」的租金,于是「扔什么」成了比「捡什么」更频繁的决策。

背包设计三大流派:槽位制(本组件,Diablo 一系)、格子拼接制(道具占不同尺寸形状,Resident Evil 与《背包英雄》)、重量制(按总重封顶)。三种流派的成本形状完全不同:槽位制把成本离散成 0/1,拼接制把成本做成二维几何问题,重量制把成本压成一维标量。

为什么血瓶的描述是「恢复 2d4+22\mathrm{d}4+2 点生命」而不是一个固定数?期望 2×2.5+2=72 \times 2.5 + 2 = 7 点,标准差约 1.6。治疗也要掷骰,和伤害共用同一套骰子代数,只是方向相反。

掉落表是这类系统的上游。

工业界的实现是一棵加权树:先掷「掉不掉」(掉率),再在掉落子表里按权重抽条目,稀有度分层后逐层下潜。

朴素加权随机有个著名的体验问题:概率 1% 的物品,期望 100 次出货,但几何分布的长尾意味着约 5% 的玩家 300 次还空手。于是有了 PRD(伪随机分布,每次失败后提升下次概率)与保底(硬计数器)两套驯化手段,让长尾在玩家感知里被截平。保底硬计数器的完整期望账,在棋牌桌上的概率课的抽卡一节里算过。

站内这个背包的「添加」因为做了去重,实际上是不放回均匀抽样。每按一次,剩余池子等概率缩一格,十五次之内必然集齐全图鉴。这与掉落表的有放回加权抽样是两种不同的随机结构:前者「集齐时间」有硬上限,后者永不保证。

收集系统选哪种,取决于你想卖「保底的安全感」还是「欧皇的运气」。

卡组构筑的概率结构

卡组构筑是八个组件里随机结构最精巧的一个。规则是标准「爬塔」式:起手卡组十张,五张攻击(打 6)、四张格挡(挡 5)、一张强化(力量 +2)。每回合抽 5 张、有 3 点能量、每张牌费 1。打出的牌进弃牌堆,抽牌堆抽空时把弃牌堆洗回来。回合结束敌人打 9,格挡吸收伤害后清零,力量永久保留。玩家 30 血,敌人 48 血。

先算第一张超几何账:起手五张里见到那张唯一强化的概率恰好是 5/10=1/25/10 = 1/2。

再算纯进攻线:每回合三张攻击打 18,敌人 48 血需要两回合半,第三回合第二刀终结。期间玩家挨两次 9 点剩 12 血,赢得很紧。

混防线:每回合一张格挡加两张攻击,格挡把 9 压到 4 点穿透,输出每回合 12,四回合斩杀,终局剩 18 血。防守线的血量优势来自格挡的「4 换 5」汇率与提前一回合的额外输出窗口,代价是多耗一回合。

这两条线就是卡组数值设计的全部雏形。能量是把「每回合三张」的上限焊死的资源闸门,任何超过 3 费的收益都必须来自牌面以外的机制。

最后看循环结构里的稳态概率。

抽牌堆与弃牌堆在洗牌边界上不断互换。源码里的抽牌函数先从抽牌堆顶弹牌,弹空了就把弃牌堆整堆洗回。除了开局几回合,任意一张牌在任意一个抽牌事件里被抽到的概率趋近于「手牌数 ÷ 卡组总数」,十张卡抽五张就是约 1/21/2。

这个稳态值解释了构筑游戏最核心的一条设计公理:卡组越小、单卡质量越均匀,抽牌的方差越小。也解释了为什么加牌的代价远大于直觉——每加一张牌,所有关键牌的上手率同步被稀释。以及为什么「抽牌」本身是最强的牌面效果之一,它把稳态概率分子直接做大。

同理,永久增益(力量)比等价面板伤害珍贵。它每多 +1,卡组里五张攻击牌同时升值,是复利而不是单利。

概率在这些系统里的角色不是「骰一个输赢」,而是把玩家的每一次构筑决策翻译成可以精算的期望与方差。这一套把抽牌概率拆到超几何分布的功夫,与棋牌桌上的概率课里算 outs 和底池赔率用的是同一本账。

放置游戏:驯服指数的四条曲线

放置类(idle / incremental)游戏的全部系统都围绕一个矛盾展开:产出必须增长才爽,但指数增长一旦失控就会摧毁游戏。所以这个品类的数值设计就是一门「驯服指数」的手艺——用封顶、价格增长律、瓶颈与重置,把一条本该爆炸的曲线按进可玩的节奏。本站四个组件恰好覆盖这门手艺的四个侧面:一个点击器、一个钓鱼场、一只电子宠物、一家创业公司。

动手版本:点击放置、放置钓鱼、电子宠物、创业模拟。

点击为什么只值 1

点击器的核心交互是一颗饼干。每点一下 +1,永久如此,没有任何机制让点击升值。

围绕它的产线有四条,定价和产能是教科书级的一组数字。光标 15 币、产出 1/秒。奶奶 100 币、5/秒。农场 1100 币、40/秒。工厂 12000 币、250/秒。

先看一个反事实:假如点击收益随资产增值(比如等于产线总和的 1%),会发生什么?

最优策略永远是点击。点击不吃价格增长律、不需要攒钱、即时到账,产线反而成了效率更低的选择,整个购买系统瞬间作废。

手工收益恒为 1 是刻意的封顶。它的数学意义是手工产出是常数,自动产出随资产线性增长,点击占总产出的比例趋近于零。玩家的注意力被从「手」推向「产线」。

但点击也不能取消。开局产线为零,点击是唯一的启动资金来源。一秒钟点五次攒到第一个光标的 15 币,是整个游戏的「第零步」。

每次点击还伴随两个反馈。点击位置浮出一个 +1 小字,顶栏的总点击计数永远在走。

这两个细节没有数值意义,却回答了放置游戏最重要的体验问题:在产出还不存在的开局,玩家凭什么留下来。凭每一次输入都有即时、可见、不可剥夺的回报。

产线定价本身也有一条暗线。算回本时间(价格 ÷ 产能),光标 15 秒、奶奶 20 秒、农场 27.5 秒、工厂 48 秒。

越贵的产线回本越慢,但单件产能高一个数量级。后来者用「更差的性价比」换「更强的单位冲击力」。

这让买最新产线始终是一件有仪式感的大事,而不是无脑滚动点击购买列表。同时由于回本时间只是温和变差(15 秒到 48 秒,近三个数量级的价格差只换来三倍的时间差),攒钱买大的永远不算亏,只是节奏更慢。

回本时间从 15 秒温和拉长到 48 秒,玩家从频繁小额购买逐渐过渡到攒钱等大件,节奏感就藏在这条缓坡里。

1.15 律

同一款商品可以无限购买,但每买一件,下一件涨价 15%。cost=⌊base×1.15n⌋\mathrm{cost} = \lfloor \mathrm{base} \times 1.15^{n} \rfloor,nn 是已持有数量。

这是整个品类最重要的一条曲线。

数字先建立直觉。1.1513≈6.21.15^{13} \approx 6.2,1.1530≈661.15^{30} \approx 66,1.1550≈10831.15^{50} \approx 1083。

也就是说买 50 个光标,最后一个的价钱是第一个的一千倍。

而产出端,nn 件光标合计 rate×n\mathrm{rate} \times n,线性。成本端,第 nn 件的价格,指数。

每一件的回本时间按 1.15 的因子拉长,迟早超过玩家的耐心阈值。玩家撞墙了。

撞墙不是事故,是设计。墙的右边必须放着别的东西:更高产的产线、尚未解锁的升级,或者一次重置。

价格增长律的真正作用是给线性产出制造一个指数级的阻力场,逼玩家不断切换目标。

为什么系数偏偏是 1.15?太低(比如 1.05)价格形同虚设,玩家滚雪球畅通无阻,中后期无聊到死。太高(比如 1.5)三四件就撞死在墙上,攒钱节奏变成煎熬。

1.15 让单个商品在「值得买十几次」的位置精确衰减。Cookie Clicker 原版用的也是这个值,它几乎成了放置游戏的行业默认常数。

实现层面有两个细节。

其一,价格在购买后立即更新(count 自增再重算 floor(base × 1.15^count)),没有缓冲期。

其二,购买按钮的可用态与高亮态随 cookies ≥ cost 实时切换。玩家永远盯着「还差多少」的那个数字,价格曲线的张力被原样搬进了 UI。

这类按帧刷新的 affordability 反馈是增量游戏的标准界面语言。数值单调上涨,按钮在「差一点」与「买得起」之间反复横跳,本身就是一种奖励节奏器。

买入顺序

四条产线共用一种货币,「下一笔钱买谁」就成了一道持续变化的题。

严格的答案要比较每件商品的边际产出与到达其价格所需的时间。实战中的玩家则分裂成两个流派。

流水线式:有钱就买当前买得起的最便宜一件,保持购买频率。

储蓄式:忍住不动,攒到下一件大件。

1.15 律决定了储蓄式在长期更优,理由藏在价格曲线的「年龄」里。便宜的商品你买过太多次,它的价格指数已经跑出了很高的幂次(第 30 个光标要 15 × 66 ≈ 990 币,比一台崭新的奶奶贵十倍),而大件的价格曲线还年轻、幂次为零。

老练玩家的核心技能就是持续判断哪条价格曲线还没被自己抬起来。这个判断在原版 Cookie Clicker 里演化成一整套科技树优先级,但它的种子已经写在这四个数字和 1.15 这个系数里了。

第二条曲线

放置钓鱼把「产出是随机数」这件事做成了核心循环。

六种鱼,价值分五档。沙丁鱼 5、鲤鱼 8、鲈鱼 15、鳟鱼 20、三文鱼 45、黄金鱼 150,稀有度一至四级。

抽取权重不是查表而是公式:weight=rod rarity−1\mathrm{weight} = \mathrm{rod}^{\,\mathrm{rarity}-1},其中 rod 是鱼竿等级。

这条权重公式的行为值得算三组数。

1 级竿:所有权重为 1,均匀分布,单次抛竿期望 (5+8+15+20+45+150)/6=40.5(5+8+15+20+45+150)/6 = 40.5。

5 级竿:权重变成 1,1,5,5,25,1251, 1, 5, 5, 25, 125,黄金鱼概率 125/162≈77%125/162 \approx 77\%,期望升到约 124。

10 级竿:权重 1,1,10,10,100,10001, 1, 10, 10, 100, 1000,黄金鱼 89%89\%,期望约 138。

看出来了:期望值在收敛。

分布被最高稀有度吞噬,期望越逼近黄金鱼的价值上限 150。

把竿从 5 级升到 10 级,各次升级的价签从 327 涨到 2147,合计约 5200 金币。换来的期望却只从约 124 涨到约 138(+11%)。

升级的边际收益递减不是靠什么防膨胀机制实现的,而是这条权重公式自带的结构。当分布收缩到只剩最稀有项,再压榨也只能逼近它的面值。

数值上的收益递减设计,最优雅的形态是让公式自己去做。

鱼竿还是双用途升级。它同时抬升手动收益(稀有度权重)和挂机收益。自动钓鱼机的秒产出是 2×台数×竿等级2 \times \text{台数} \times \text{竿等级}。

前期手玩抛竿看运气,后期躺平收账。两种玩法的过渡全靠这一个变量自然完成,不需要任何「解锁挂机模式」的按钮。

手动与挂机的力量对比在开局是悬殊的。零台自动机时,抛竿全靠手,1 级竿期望 40.5 一次。哪怕只以每秒一次的手速点击,手动收入也是挂机的二十倍以上。

但手动收入有两个天花板。它不随时间累积(人总要睡觉),且单次抛竿上限被期望收敛锁死在 150 附近。挂机收入则随台数与竿等级相乘无限爬坡。

于是玩家的收入结构必然经历一次换挡。统计栏把金币、捕获数、每秒产出并排陈列,玩家能亲眼看着主导项从「最近捕获」的惊吓与惊喜,切换到「每秒」那一格里平稳跳动的数字。

好的放置游戏都提供这种可观察的换挡时刻。它是「我的策略生效了」的直观证明。

商店里并存着两条价格曲线。鱼竿 50×1.6级−150 \times 1.6^{\text{级}-1},温和指数,允许一路升到十几级。自动钓鱼机 200×2台数200 \times 2^{\text{台数}},每买一台翻倍。第四台 1600、第六台 6400、第九台 51200,数量天然封顶在个位数。

两种增长律在同一间商店里,是在给玩家两种不同的天花板预期。竿是「长线慢慢磨」的资产,自动机是「够用就好」的资产。

而收入结构 2×auto×rod2 \times \text{auto} \times \text{rod} 是乘法。两个变量互相放大,钓一台机器、升一级竿都会重估另一边的价值。

点击器的总产出 ∑rate×count\sum \mathrm{rate} \times \text{count} 是加法引擎,钓鱼的 auto×rod\text{auto} \times \text{rod} 是乘法引擎。加法引擎膨胀慢、节奏稳,乘法引擎后期爆发猛、但每一步都要重新算账。后者正是放置游戏后期「突然起飞」快感的来源。

时间怎么结算

产出游戏里「一秒」是什么,决定了整个游戏能不能被作弊。

点击器的实现是行业标准答案。setInterval 每 100ms 跳一次,但结算用的是真实流逝时间。每次 tick 先算 dt=(now−lastTick)/1000\mathrm{dt} = (\mathrm{now} - \mathrm{lastTick})/1000,再累加 cps×dt\mathrm{cps} \times \mathrm{dt}。

按墙钟结算,而不是按 tick 次数结算。

这层看似多余的区别恰恰是防刷的第一道闸门。浏览器会把后台标签页的定时器节流到一秒一次甚至更低,如果按次数结算,切出去听个歌收入就暴跌,定时器频率成了收入函数的一部分。按 dt 结算,节流只影响结算频率、不影响结算总量,无论 interval 怎么抖,单位时间的入账恒等于 cps。

定时器与事件循环这套浏览器基本功在之前的 JavaScript 笔记里梳理过,这里只看数值后果。时间成了唯一的记账单位,任何试图「加速 tick」的手段都自动失效。

电子宠物是这条时间轴的反方向。它不是产出曲线,是消耗曲线。

每 2 秒一个 tick,饥饿与心情各 −1。任一项跌破 20 时健康每 tick −2,两项都健康则 +1 缓慢回血。喂食 +25 饥饿,玩耍 +25 心情但 −5 饥饿,治疗 +20 健康。

算一下生存期。满值 80 起步,60 个 tick(两分钟)后跌到 20 开始掉血,健康 100 掉空再要 50 个 tick(100 秒)。纯放养不到四分钟死亡。

玩家的操作窗口是一个以分钟计的维护循环,这与点击器「产出永续」的时间观恰好相反。点击器结算「离线你赚了多少」,宠物结算「离开你欠了多少」。

但注意实现里的一个克制。定时器挂在组件内部,组件销毁即停表。离开页面,宠物暂停,而不是死亡。

这是刻意不做离线结算的设计决策。真按 2 秒一 tick 对离线时间补算,睡一觉回来必是尸体,情感设计上不可接受。「暂停」是对玩家温和的作弊,用技术上的不诚实换取情感上的公平。

数值设计与情感设计的冲突,在这里以牺牲模拟真实性收场。

真正要做离线收益时,通用模式是三件套。存档(资产与时间戳)、补发(回来时按 elapsed × cps 一次性入账)、封顶(补发时长设上限,比如 8 小时)。

封顶那一步不可省。没有它,一个月不上线等于白得一个月产出,曲线的整个中段被跳过,节奏崩塌。

这四个组件都没做存档,刷新即清零,所以它们的「离线」止步于「标签页留在后台」这一层级。对网页小游戏而言是合理的取舍,但把封顶补发记在脑子里,下次自己做就会知道差哪几块。

定时器清理有两种等价写法:点击器在 onDestroy 里手动清 interval,钓鱼则在 $effect 里返回清理函数,选一种用到底即可。

三个组件的 tick 刻度并排看也有讲究。

点击器 100ms。快到足以让浮出的 +1 与手指同步。

钓鱼 1s。收入是整数 2×auto×rod2 \times \text{auto} \times \text{rod},按次入账、不做 dt 补偿。

宠物 2000ms。慢到「每 tick 减一」能被肉眼察觉,制造出一点「盯着仪表读数」的焦虑。

tick 频率是表现需求与结算需求的折中,但两者的可分性恰恰由结算方式决定。dt 结算的点击器可以任意改 tick 而收入不变(频率只影响画面平滑度),整数按次结算的钓鱼则把频率焊死进了数值。1000ms 改成 500ms,挂机收入直接翻倍。

先决定收入模型,再决定 tick。顺序反了就会把数值表耦死在定时器参数上。

瓶颈与事件

创业模拟把「指数产出」做成了一道考题。

状态五个数。现金 50000,月收入 MRR 1200,月增长率 20%,月支出 4500,月份计数器。

每按一次「下一月」:MRR←MRR×1.2\mathrm{MRR} \leftarrow \mathrm{MRR} \times 1.2,cash←cash+MRR−burn\mathrm{cash} \leftarrow \mathrm{cash} + \mathrm{MRR} - \mathrm{burn}。盈利判定 MRR>burn\mathrm{MRR} > \mathrm{burn} 即胜利,cash<0\mathrm{cash} < 0 即破产。

先解无事件的基准情形。MRR 从 1200 追 4500,需要八个月(1200×1.28≈51601200 \times 1.2^8 \approx 5160 才反超)。前七个月净流出累计约 12900,现金最低点约 37100。安全垫厚实,纯指数模型下几乎不可能输。

所以真正的对手是每月 30% 概率触发的随机事件,且三种事件各自精确作用于模型的三个参数。

40% 的融资事件(入账 15000–35000 现金,增长率 +4 个百分点)。放大指数的曲率。

30% 的客户流失(MRR 折损 20%)。折断指数,ln⁡(0.8)/ln⁡(1.2)≈1.2\ln(0.8)/\ln(1.2) \approx 1.2,等于倒退一个多月的复利。

30% 的招聘(月支出 +400,外加 2500 一次性成本)。抬高那条决定胜负的瓶颈线。

这个结构里最值得学的是 burn 为什么固定。

固定的支线与指数的收入线之间的剪刀差,就是游戏全部的戏剧性。前期 MRR 在线下方挣扎,交叉点之后差额指数扩大。

若 burn 也跟着指数涨,玩家永远无法盈利,游戏退化为无限拖延。若 burn 会下降,瓶颈消失,张力同样消失。

保留一条不动的线,指数才有意义。

胜负判定用的是两种不同的量。

盈利是流量判定(MRR > burn,看每月差额)。破产是存量判定(cash < 0,看累计家底)。

两者之间存在一段最真实的窗口期。流量已经转正、存量却仍在出血,账上还有钱但每月在亏。理论上随时回本,实际上随时断裂。

真实世界的现金流分析里,「还剩几个月的跑道(runway)」说的就是这个差值。一款二百行的模拟组件把它保留了下来,判定条件写得干净,语义却没有打折。

时间观上也构成一组对照。

点击器与钓鱼的时间自动流逝(100ms 与 1s 的 tick),创业模拟的时间由玩家手动推进。「下一月」按钮让时间流速本身成了资源,按得越勤游戏越快,但每个月的结算都是一次决策心跳:事件骰子、现金流、距离盈利还差几个月。

离散回合制把指数增长的冲击从「每秒涓流」放大成「每月一次的跳变」。同一个 1.2n1.2^n,手感完全不同。

最后是重置循环(prestige),放置游戏的终极驯服手段。

这四个组件都没有实现完整的 prestige,但两个「朴素重置」恰好标出了它的两个端点。

电子宠物的复活把血量、饥饿、心情复位,年龄计数却不清零。年龄是唯一跨越死亡的累计量,只是暂时没有兑换出口。

创业模拟的重新开始则是无保留全量复位。50000、1200、20%、4500,一切归零。

prestige 的通用公式夹在两者之间。第 k+1k+1 轮的初始条件 = f(第 k 轮的成就)f(\text{第 } k \text{ 轮的成就}),ff 就是保留下来的乘数。

数学上它把单条指数曲线切成多段,每段从更高的起点重新爬同一条曲线。玩家反复体验的恰好是指数增长最陡、最爽的前中段,而「什么时候重置最划算」又衍生出独立的最优化玩法。

对这套组件而言,最小改造路径也很清楚。点击器加一行 localStorage 存下累计产出,重开时按它折算初始倍率,一个最简 prestige 就成立了。

prestige 的设计难点全在 ff 的定价上。

乘数太小,重置没有仪式感,玩家感觉白干。太大,等于送了一台时间机器,后续曲线被瞬间碾平。

通行标尺是「每轮重置让总进度速度大约翻倍」,并把重置的推荐时点安排在玩家刚刚撞墙的位置。价格增长律造出来的那面墙,本身就是重置功能最好的推销员。

还有一个容易被忽略的细节:保留的必须是乘数而不是资产。

送玩家一座工厂,它的产出仍受产线叠加的线性约束、价格律照样抬价。乘数则直接作用于总产出、不受任何价格律管辖。只有它能重启指数。

换个说法,prestige 贩卖的不是资源,是曲线本身的斜率。玩家真正用成就购买的东西,是下一次循环里的时间。

消除游戏:一个动词的四种数学结构

换到消除类游戏,骨架反而出奇地统一:六款游戏共用同一个集合论动作——从有限集合中删去满足某个谓词的子集,然后让剩下的部分愈合。谓词怎么定义、子集怎么找、删除之后世界如何塌陷与生长,每一种不同的回答都会长成一个独立的物种,本站这六款正好分属四种数学结构。

动手版本:三消、宝石迷阵、泡泡龙、祖玛、色彩洪流、图片拼图。

消除的四种数学动作

把消除类游戏排进一个二维矩阵,两个坐标轴分别是「消除的谓词是什么」与「棋盘是静态的还是动态的」。

谓词一栏有三种典型答案:同色且相邻够多(三消家族)、连通且够不到支撑(泡泡龙的悬空判定)、位置正确(拼图的归位)。

棋盘一栏有静态与动态之分:三消与泡泡龙的棋盘只在你造成消除时变化,祖玛的棋盘本身每帧都在移动。

两轴组合出本章要走的四种动作:

  1. 交换—检测—清除:玩家只做局部交换,检测与清除交给系统,愈合靠重力补给。三消与宝石迷阵属于此列。
  2. 发射—吸附—清除:玩家把新元素注入棋盘,用新增诱发满足谓词的子集。泡泡龙在静态网格上做这件事,祖玛在动态链条上做。
  3. 扩张—占领:不删任何元素,让一个连通区域吃掉相邻的同色格子。这是消除的逆运算,色彩洪流的全部内容。
  4. 置换—复原:消除的对象不是棋子而是「错位」这个抽象物,图片拼图把删除操作换成了对换操作。

四种动作里有三种能找到一个单调测度:色彩洪流的领地面积只增不减,障碍模式的剩余目标物与宝石迷阵的覆盖层数只减不增,泡泡龙 Boss 模式的血量只降不升。

玩家的每次有效操作都让测度朝终局方向移动。

消除类游戏之所以耐玩,恰恰在于设计者如何用概率(随机补给)、几何(发射角度)、时钟(匀速推进的链条)在这层单调性上覆盖出非单调的观感。分数会涨会跌的错觉、险象环生的错觉、来运气的错觉。

下面逐个拆。

交换式三消

本站三消的经典模式是教科书式的交换三消:8×8 棋盘、6 种颜色、交换相邻方块、三个以上同色连成直线即消除。

就这几行规则,但每一行背后都藏着一个必须做对的算法决策。

开局棋盘是一场拒绝采样

如果开局直接给每格独立均匀地掷颜色,三连会成批地自己冒出来。

算一笔账:横方向每行有 6 个「起点在 c、覆盖 c/c+1/c+2」的三连窗口,8 行共 48 个,纵方向同样 48 个,合计 96 个窗口。每个窗口三格同色的概率是 6×(1/6)3=1/366 \times (1/6)^3 = 1/36。

于是整张随机棋盘上三连窗口的期望约为 96/36≈2.796/36 \approx 2.7 个。开局即送两三次免费消除,节奏被破坏,玩家的第一次交换也失去了意义。

源码 makeBoard 的对策是拒绝采样:逐格填色,新颜色若与左边两格相同、或与上边两格相同,就重新掷一次。

这个约束只回看长度为 2 的历史,每个格子被拒绝的概率约 2/36≈5%2/36 \approx 5\%,重掷是例外而非常态,采样必然快速终止。

产出的棋盘「看起来完全随机」,却严格不含任何现成三连。随机性与可玩性在这里是一对需要显式调和的量,而不是同义词。

匹配检测

匹配检测本身就是一遍游程编码:对每一行从左到右扫描,相邻同色则游程加一,遇到断点且游程不小于三,把整段坐标加入集合。列方向再来一遍。

用集合做去重,L 形与 T 形交叉处的格子天然只留一份。

整张棋盘一次检测是线性的,这是三消引擎能跑在低配设备上的前提。

交换合法性用的是最朴素也最正确的办法:把交换放在棋盘副本上模拟,检测副本,无匹配则交换无效。

玩家的感知是「这步不行,方块弹回去」,机器的语义是「这次交换没有产生任何满足谓词的子集」。

没有启发式、没有特判,模拟一次的成本与一次真实检测相同。

级联、重力与补给概率

消除后的世界如何愈合,决定了三消的手感上限。

源码的 resolve 是一个循环:检测匹配、清除、重力下落、再检测,直到检测为空。

第 kk 轮清除的每格得分乘以 kk。级联乘数,界面上那个「连击」徽章显示的就是它。

重力这一步藏着三消最有趣的概率设计。

每列把非空方块压实,顶部缺口补上独立的均匀随机颜色。注意补给不经过开局那套拒绝采样。

于是刚才那笔「随机区域上期望 2.7 个三连」的算术,恰好适用于补进来的格子附近。连锁不是运气彩蛋,而是均匀补给的统计必然。

设计者故意在开局拒绝随机三连、在过程中放任随机三连,两者叠加出「玩家只需触发第一次消除,后面由概率接管」的免费奖励结构。

补给只填缺口、其余棋盘保留记忆,这又让每次消除后的局面带着历史。玩家的排列工作在级联中持续增值。

死局检测

交换式棋盘有一个静态游戏没有的风险:没有任何交换能产生匹配的死局。

源码的对策分两层。

第一层是检测:穷举全部相邻交换。横向 7×87 \times 8 加纵向 8×78 \times 7 共 112 对,每对在副本上模拟一次完整匹配检测,任何一个成功即判定有解,成本毫秒级。

第二层是补救:无解则洗牌,经典模式整盘重发(仍然走拒绝采样),至多重试 50 次。50 次之后仍有死局才宣告游戏结束。

障碍模式还有第三层讲究:洗牌只重掷颜色、保留障碍布局,免得玩家辛苦敲出来的残局被连障碍一起重置。

这套机制的设计含义是:死局被系统吸收,玩家永远面对有解的棋盘,除非极小概率地连洗 50 次都洗不出来。

诚实性由尝试上限担保,绝望被转移成了数学上的罕见事件。

三消页还提供障碍模式:5 个石块(需两次相邻消除,先裂后碎)、4 个锁定块(一次相邻消除解锁)、12 个果冻(在其上完成消除即清),30 步内清空全部障碍获胜。

它在工程上最有意思的细节是重力。石块占据格子后,一列被切成数段,方块只在段内下落,每段独立压实补齐。

障碍物在这里是结构性的:它们改变棋盘的拓扑。

宝石迷阵的覆盖物

宝石迷阵与三消共用同一台引擎:8×8、6 色、无效交换弹回、开局拒绝现成三连(整盘重掷直到干净)。

它的障碍模式示范了同一引擎上另一种障碍哲学。障碍物不占据格子,而是作为覆盖层叠在宝石上,把目标函数从「得分」改写成「清除覆盖格」。

覆盖物的投放由三个随机数门槛划出:约 14% 的格子是冰封,两层覆盖。再往下约 12% 是双层果冻、18% 是单层果冻。合计约四成半的格子带覆盖物。

每一步的消除波及某个覆盖格时剥掉一层,果冻每层 20 分、冰封每层 30 分,层数归零记为清除。

进度条显示清除比例,30 步预算内清完全部即通关。

冰封的锁定规则是这套经济学的枢纽:两层未破的冰封格不可作为交换的一方。玩家不能把它换进换出,只能让相邻交换拼出的三连把它裹进去挨打。

第一层被剥掉后锁定解除,第二层照常。

于是每一步的价值公式被改写了:一次不触及任何覆盖物的消除,在障碍模式里等于净亏一步。

做个预算算术:期望总层数为 64×(0.14×2+0.12×2+0.18×1)≈44.864 \times (0.14 \times 2 + 0.12 \times 2 + 0.18 \times 1) \approx 44.8 层,30 步预算意味着平均每步要剥约 1.5 层才够本。

级联从三消里的「额外奖励」升格成了「必需品」。

两个障碍模式对照着看最见设计分寸:三消的障碍占据格子、切断重力、改变棋盘拓扑。宝石迷阵的障碍不占格子、只在目标函数上施压。

前者改变的是「能做什么」,后者改变的是「该做什么」。

另一个工程差异是死局:宝石迷阵的源码里没有死局检测,局面是否可解全凭重力补给的随机重排。

同样的随机补给,在得分目标下是奖励,在清除目标下必须被玩家规划。

这就是目标函数改写玩法的最小完整案例。

泡泡龙

错列网格与六邻域

泡泡龙不用方格棋盘。

本站泡泡龙的网格是 12 行、偶数行 11 列、奇数行 10 列、整体错开半个直径的错列网格。六边形密堆的矩形近似。

错列的意义在邻接关系:每个泡泡有 6 个邻居(左右各一、斜上斜下各二),比方格的 4 邻域多出两条斜向通路。

同色团的形状因此更圆润,切断支撑的战术也更立体。

源码的 neighbors 函数按行号的奇偶性枚举六个方向,整局游戏的全部连通性计算,同色检测、悬空判定,都建立在这张邻接表上。

初始局面是前 6 行、每格 85% 概率有泡、6 种颜色。

那 15% 的空洞不是装饰:吸附需要落点,初始的稀疏给瞄准留出容错,也让「哪里该补、哪里该断」的开局规划成为可能。

发射

发射端是一段连续几何。

鼠标瞄准用 atan2 求发射角,方向键每次微调 0.12 弧度,空格或点击发射。发射角被钳位在水平线以上至少 0.15 弧度(约 8.6 度)。贴着底边平射被禁止,每一发都被强制参与构筑网格,而不是在底部扫射。

弹速是每帧 7 像素,撞左右墙时水平速度取反。镜面反射把一次瞄准变成一段折线,借墙反弹打背面是技巧而非漏洞。

命中判定是距离平方与 0.7D20.7D^2 的比较(DD 为泡直径):等效判定半径约 0.84D0.84D,比一个泡略小。

轻微重叠仍算命中,边界手感偏宽容。这类「判定半径略小于视觉半径」的取巧在弹幕与射击游戏里是通用手法,误差留在玩家有利的一侧。

弹速、链条推进速度这类「每帧常数」意味着游戏节奏绑定在渲染帧率上。60Hz 与 144Hz 的屏幕上弹道速度差一倍多。

帧循环与时间步的取舍,此前写游戏循环时已经展开过。泡泡龙属于「可变时间步裸奔」的活标本:物理简单到不需要固定步长,代价只是不同刷新率的手感漂移,对这类休闲玩法是笔划算的交易。

吸附与两次泛洪

弹丸越过顶部或撞上已有泡泡时发生吸附:把连续坐标四舍五入到最近的格位。目标格被占则落入第一个空邻居,连空邻居都找不到才判负。

离散化只发生在命中瞬间,飞行全程是连续的。「连续瞄准、离散落地」是所有泡泡龙类游戏的共同骨架。

吸附之后是两次泛洪,谓词完全不同。

第一次泛洪以「颜色相同」为谓词,从落点扩散出同色连通块,规模不小于三则整团清除,每泡 10 分。

第二次泛洪以「可达顶行」为谓词:从顶行所有泡泡出发做连通性检查,凡是够不到顶部的悬空泡整体坠落,每泡 20 分。

两次泛洪的价差是精确的设计语言:剪断支撑让 8 个泡泡坠落,比老老实实消 3 个同色泡泡赚得多。

游戏在奖励「剪桥」而非「剪多」,高手的瞄准因此总是冲着结构关键点去。

败局线是最底行:任何泡泡进入即负。

Boss 模式把这条线的压迫显式化。Boss 血量 100,每消一泡打 5 点、每坠一泡打 2 点。Boss 每 9 秒行动一次,压下一整行与投掷不可消除的灰色石泡两种手段交替,清空棋盘它就把前四行重新填满八成。

原版的失败是「自己的塔塌了」,Boss 战的失败是「对手在加码」。同一套网格,压力来源从内生的堆叠变成了外生的时钟。

祖玛

祖玛对消除谱系的贡献是第二个坐标轴:棋盘本身动起来。

本站祖玛把静态网格换成一条单向传送带:一串彩珠以每帧 0.4 像素的速度向左端的洞口匀速推进,链尾触洞即败。

棋盘不再有「布局」,只有「倒计时」:链条长度乘以间距再除以速度,就是留给玩家的总时间,且随着消除推进变长、随着疏忽变短。

压力从几何量变成了时间量。

开局 6 珠、5 种颜色。生成时有个温和的随机约束:新珠与前珠同色时有 60% 的概率被换成邻色。

不是硬性禁止而是概率抑制,比起三消的严格拒绝采样宽松得多。偶尔留一两个现成两连,给玩家一点即拆的甜头。

发射端与泡泡龙同构:鼠标瞄准、点击射出、每帧 8 像素、命中判定用 0.8×SP20.8 \times \text{SP}^2 的距离平方阈值。

插入则按弹丸与被击珠的相对横坐标决定插在前面还是后面。

插入后的连锁语义与泡泡龙截然不同,这是祖玛和泡泡龙的核心设计差异。

泡泡龙的连锁是物理连锁:消除后上方泡泡失去支撑而坠落,连锁沿着重力方向传播。

祖玛的连锁是代数连锁:消除后左右两段直接合拢,代码递归检查新邻接的两个颜色。若恰好同色且够三,再来一轮。

前者依赖网格拓扑,后者只依赖序列结构。前者的连锁需要玩家铺垫「下方埋着同色团」,后者的连锁只需要链条里恰好隔着重复合段。

两种连锁给玩家的预判对象完全不同:一个看空间,一个看序列。

节奏设计上,传送带的匀速是刻意的慢。60Hz 下每秒约 24 像素,一条 30 像素间隔的珠链每秒走不到一格。

胜负因此几乎不取决于反应速度,而取决于决策密度:每一发都要在链上挑插入点,考虑当前珠色、下一珠色、链上已有的相邻同色段,搜索空间是链长而不是棋盘格数。

三消练的是发现模式,泡泡龙练的是几何规划,祖玛练的是倒计时下的序列决策。同一个「消除」,三种认知负荷。

色彩洪流与图片拼图

色彩洪流

色彩洪流把「消除」整个翻了过来:不是删掉元素让世界塌陷,而是让一个区域生长直到吞下整个世界。

本站色彩洪流的棋盘是 14×14、6 色随机铺设。左上角格子的连通同色区域是玩家的领地,每次操作选一种颜色,领地把所有与它相邻的该色格子吸收进来并整体换色(教科书式的泛洪填充),25 步内把全盘染成同色即胜。

选择当前领地已有的颜色是无效操作,源码直接忽略、不扣步数。连「浪费」都被规则精确地定义了。

这类泛洪填色游戏(Flood-It 一族)最少要多少步?有一个干净的下界可推。

领地每一步最多向外扩一层邻接。吸收的前提是相邻,而新吸收的格子换色后不再传导,所以领地的最大曼哈顿距离每步至多加一。

棋盘对角格 (13,13)(13,13) 与原点的曼哈顿距离是 13+13=2613+13=26。设开局领地已达到的最大距离为 d0d_0。除非左上角恰好有同色邻居(按 6 色算概率约三成),d0d_0 几乎总是 0。

则通关至少需要 26−d026 - d_0 步。

对照源码里写死的 25 步上限:25<26−d025 < 26 - d_0 在 d0=0d_0 = 0 时严格成立。

也就是说,在当前参数下,即使每一步都完美,胜利判定在数学上也不可达。即便开局白捡一个同色邻居,也需要 25 步步步都让最远边界恰好前进一格、一步不浪费。

这不是操作或策略问题,是预算问题。把步数上限提到 26 以上,游戏才从「必败局」变回「困难局」。

读源码有时会读出这种连作者都未必自知的数学事实,这是把规则写成代码的意外收获。

再往上一层是复杂性:泛洪填色的最少步数问题在一般化的棋盘与色数下是 NP-难的,不存在可用的闭式公式。

玩家实际依赖的贪心策略,每步吸收最多的边界格,在最坏情况下会偏离最优。

14×14 的棋盘把这种「贪心不最优」压缩进了有限的体感里:选色不能只看眼前吸收量,还要看新边界给下一步留下了什么颜色的接口。

图片拼图

图片拼图在谱系的另一端:没有随机、没有同色、没有消除,只有一副被打乱的置换要复原。

本站图片拼图的实现里藏着两处数学。

第一处是切图。

源码没有真的把图片切成 N2N^2 块。每块用 CSS 背景定位从原图上「看」到自己那一格:background-size 设为 N×100%N \times 100\%,background-position 取 cN−1×100%\frac{c}{N-1} \times 100\% 与 rN−1×100%\frac{r}{N-1} \times 100\%。

百分比定位的语义是把图片的 p%p\% 处对齐容器的 p%p\% 处。当图片恰好是容器宽度的 NN 倍时,第 cc 块(从零数)恰好对齐 p=cN−1p = \frac{c}{N-1}。

一条公式免掉了一次真正的裁剪与 N2N^2 次图片对象,难度选项 3×3 到 6×6 之间切换时连原图都不用重传。

第二处是打乱与可解性。

打乱用 Fisher–Yates 洗 N2N^2 块,唯一的额外约束是「不能洗出已解状态」。若恰好洗成恒等排列,就把前两块对调。

玩法是任意两块对换。

这里有一个和十五数码的关键区别:十五数码的每次移动是受限置换,全部可达状态只占一半,奇偶性是一堵墙。而「任意对换」生成整个对称群,任何打乱都可复原,不存在无解局。

拼图玩家的困难永远是「怎么排」而不是「能不能排」。

竞速模式给这套机制装上时钟:N2N^2 块配 N2×4N^2 \times 4 秒。每块 4 秒的线性预算,超时判负,最快纪录存进本地。

它同时是这条谱系里唯一允许倒退的游戏:前五款的进度账本只朝终局方向走,领地只增不减、目标物与覆盖层只减不增、分数只涨不跌。而拼图的一次对换可能在归位一块的同时毁掉两块已就位的。

它的难度因此不来自模式识别,而来自规划与工作记忆。消除的对象是「错位」这个抽象物,而错位会在操作中再生产。

单词游戏:字母概率的定价与变现

消除游戏在棋盘上做文章,文字游戏则把棋盘换成了语言:表面上玩的是词汇量,底层结算的全是同一件事——字母出现的概率。概率在哪里出现,游戏就在哪里收费:拼字游戏把它标成分数,变位词把它压缩成索引签名,填字游戏把它翻译成交叉约束,猜词游戏把它做成一条条反馈信息。站内六个文字小游戏,就是「字母概率 × 位置约束」这条公式的六种变现方式。

动手版本:拼字计分、拼字谜、拼词游戏、填字游戏、猜数字、猜单词。

字母的标价

打开拼字计分的源码,最先看到的是一张二十六行的定价表。

A、E、I、O、U、L、N、S、T、R 这十个字母各值 1 分。D、G 值 2 分。B、C、M、P 值 3 分。F、H、V、W、Y 值 4 分。K 单独一档 5 分。J、X 值 8 分。Q、Z 压轴,各值 10 分。

这张表严格来说是英语字母频率的倒函数。现代英语文本里 E 的出现率约 12%,T 约 9%,而 Q 和 Z 都不足 0.1%。频率差两个数量级,分值正好差十倍。

定价逻辑一句话就能说破:分值补偿的是稀缺性。你几乎随时能凑出一打 E,所以 E 不值钱。一局棋你未必见得摸到一次 Q,所以 Q 必须贵,否则没有局会有人为它设计战术。

定价表的旁边是一张供给表。程序里的字母袋是一串写死的字符串,展开数一数共 98 枚方块。A 有 8 枚,E、I 各 10 枚,O 8 枚,N、T 各 6 枚,R、D、L、S、U 各 4 到 5 枚,其余字母基本都是 2 枚,而 Q、X、Z 各只有 1 枚。

经典 Scrabble 套装是 100 枚(E 有 12 枚、J 和 K 各只有 1 枚),站内这版做了微调。但骨架没动:高价字母的供给严格稀缺。全袋 98 枚里 Q 和 Z 合计贡献 20 分,占整袋分值的比例远高于它们 2% 的数量占比。

稀缺性可以直接换算成概率。从 98 枚里无放回摸 7 枚,至少摸到一枚 Q 的概率是

1−(977)(987)=1−9198=114≈7.1%1 - \frac{\binom{97}{7}}{\binom{98}{7}} = 1 - \frac{91}{98} = \frac{1}{14} \approx 7.1\%

(第二步用了恒等式 (n−1k)/(nk)=(n−k)/n\binom{n-1}{k}/\binom{n}{k} = (n-k)/n。)

平均每十四局才能在开局架上盼来一次 Q,而盼来了还未必用得出去。Q 后面几乎必须跟 U,这条搭配约束本身就是频率表写给玩家的潜规则:贵的东西往往还要搭着别的稀缺品才能流通。

供给的另一面

频率定价里最值得琢磨的是 S。

论出现频率它排在英语前十,定价却只有 1 分,和 E 一个价。

原因是 S 在英语里是复数的万能后缀。棋盘上任何已经成立的名词,接一枚 S 就延长成新词,还能顺带重算一遍加成格。

设计者把它的货币价值压到地板上,让它以「嫁接能力」的形式在别处付费。

整套计分表里,分值补偿稀缺性。不稀缺但好用的资源,价格会被棋盘用别的方式收走。理解了 S,就理解了这张表的全部逻辑。

这张供给表还能算出一个基准量。整袋字母的分值总和是 200 分,摊在 98 枚上平均每枚约 2.04 分,于是 7 枚字母架的期望基础分约 14.3 分。

这个数字是所有走位决策的零点。不加成时出一条词,先拿它对照一下。低于 14 分说明这手在消耗运气,高于 14 分说明词本身跑赢了大盘,剩下的差额才轮到加成格来放大。

概率表给不出来的是方差。7 枚字母的架子天然承受元音辅音比例的波动。摸到全辅音架是每个玩家都经历过的噩梦。

站内实现给玩家和电脑各发一袋独立的 98 枚,两边互不消耗。所以不存在「Q 被对手摸走」的零和博弈,只有「这局我的袋子给不给面子」的独立抽样。

用满 7 枚字母出词有 50 分的 BINGO 奖励。这个数字把「留牌还是出牌」变成了一道期望计算题:现在出小词落袋为安,还是再等一抽凑齐七字母?

棋盘是乘法器

如果说计分表是字母的一级市场,棋盘就是二级市场。

15×15 的格子上散布着 61 个加成格。三倍单词格(TW)8 个,占住四角和每边中点。双倍单词格(DW)17 个,包括中央起点★。三倍字母格(TL)12 个。双倍字母格(DL)24 个。

计分函数的次序写得非常清楚:

词得分 = Σ(每格字母分 × 该格字母倍数) × Π(沿途单词倍数)

字母倍数只作用于自己那格,单词倍数作用于全词,且多个单词倍数之间连乘。一条词同时踩到两个 DW 就是 ×4。

这个「加法在前、乘法在后」的次序决定了全部走位策略。

加法层里,一枚 Q 踩上 TL 从 10 分变 30 分,边际收益 20 分。乘法层里,同样是这枚 Q,若所在词同时吃到 DW,整条词翻倍。

拿 QUIZ 算一笔账。基础分 10+1+1+10=2210+1+1+10=22。Q 踩 TL 得 (10×3)+1+1+10=42(10\times3)+1+1+10=42。这个格子若同时是 DW,得 (30+1+1+10)×2=84(30+1+1+10)\times2=84。

同一枚 Q,位置不同,价格差出四倍。玩家真正在经营的不是字母,而是字母与加成格的相对几何。

出牌有一条硬约束:一次落下的所有字母必须共线(同一行或同一列)。程序会把新字母沿直线向两端延伸、把接上的旧字母一并算入完整词,不共线的落子直接拒绝。

这条规则把自由拼贴变成了「在已有词的延长线上做文章」。每一次得分都在为下一手制造或封堵加成格,棋盘的紧张感全从这里来。

架子经营

供给的方差最终都落在 7 枚字母的架子上,于是架子管理成了计分之外的第二门手艺。

站内实现给这门手艺配了两个不起眼但很说明问题的按钮。

收回:把已落到盘上还没提交的字母一键撤回架子。落错的子不必心疼,试错的成本被降到零。

洗牌:随机打乱架子顺序。后者看似多余,实则是对抗「形状思维定势」的经典手法。同一组字母按一种顺序盯久了,眼里就只剩那几个词首,换一个排列,被位置掩盖的组合会自己浮出来。

程序还允许逐格回收已落的字母(点一下已落的字就回到架上)。「先铺开试试、再整体收回」的探索式摆牌由此成为默认工作流。棋盘从计分场兼做了草稿纸。

这类设计在博弈工具里有一个通用名字,叫降低探索成本。让玩家敢试,试错的产出(对字母组合的直觉)就沉淀得更快。

电脑对手把这套经济学简化到了极致,恰好从反面衬托架子管理的价值。它的词库只有 30 个常用词(CAT、STAR、QUIZ、JAZZ 这个量级),每回合把架子建成多重集、按顺序找第一个「字母够拼」的词出手,一个都凑不出就整架换新。

得分按基础分乘一个只看词长的系数。7 字母及以上 ×3,5 到 6 字母 ×2。再补上七字母的 50 分奖励。

它完全无视加成格。一个只相信「长度即财富」的对手把计分系统的第一层演示得干干净净:没有位置感的分数,天花板一眼望得到。

它的查找逻辑可以当代码课例题:多重集比对、一次遍历、零回溯。词库只有 30 个词时,穷举就是最优解。

变位词

把一个词的字母顺序打乱叫变位词,把乱序还原回去是拼字谜和拼词游戏的共同主题。

这类游戏的数学底座是一个朴素到容易被跳过的观察:互为变位词的词共享同一个排序签名。把字母按字典序排列,listen、silent、enlist、tinsel 全都变成 eilnst。

签名是变位词的完美主键。预处理时给词典每个词条算一次签名,查询时把输入排序后去哈希表里一查,整个「同字母家族」一次取回。

真实的变位词词典和某些拼写检查的候选生成,用的正是这个 O(n log n) 预处理换 O(1) 查询的套路。

家族、生成器与拒绝采样

站内的生成器模式没有建全量词典,而是内置了 15 个手工整理的家族。

listen 一族 4 个成员。stop 一族最兴旺:stop、tops、opts、pots、spot 共 5 个。skate、stake、steak、takes 是一族 4 个。往下还有 earth/heart/hater、angel/angle/glean、night/thing、lemon/melon、dusty/study、care/race/acre、below/bowel/elbow 这些两三人小户。

输入一个词,程序找到它所属的家族,把其余成员列出来。同一袋字母能合法装下多少个词,取决于语言的词库而非字母本身。这是签名粒度最直观的展示。

打乱这个动作本身也有一处工程细节。朴素的随机重排有 1/n!1/n! 的概率把词排回原样。六个字母就是 1/7201/720,玩家一眼识破「乱序」即原词,体验立刻破功。

源码的解法是拒绝采样:排完检查是否等于原词,等于就重来。拼字谜的版本封顶重试 20 次,拼词游戏的版本是不设上限的 while 循环。

反正单次命中率 719/720719/720,期望重试次数约等于 1。一道 if 判不掉的公平性问题,两行拒绝采样修好。这是随机算法里最常见的姿势:抽样分布不对,就重新抽到对为止。

信息也明码标价

拼字谜的计分规则里藏着一个信息经济学的定价。普通答对得 10 分,点了提示再看答案只得 5 分。一条提示恰好值 5 分。

它的限时模式把这套定价推得更进一步。每个词 20 秒倒计时,得分是「10 + 剩余秒数」,答得越快赚得越多,超时颗粒无收地跳下一题(一轮 8 词)。

速度被标价之后,玩家自发地在「多想一秒多赚一分」与「时间烧光一无所有」之间找边际平衡点。

一条提示值多少分,就是它把候选集砍掉了多少。信息量的这个度量,跟猜词游戏里每条反馈的价值是同一回事。下文马上会绕回来。

拼字谜还有两个模式。密码文模式把变位词换成替换密码:随机生成一张字母到字母的映射表,把谚语逐字加密,你靠频率分析和模式匹配逐字母攻破。多人模式则把中文谚语也拉进密码系统,每个不同的汉字随机分配一个 A–Z 代号。

密码文

密码文模式恰好给了这门「字母经济学」一个攻防两用的舞台。拼字游戏里玩家按频率给字母定价,密码破译者按频率给字母验明正身。

替换密码保留明文的全部统计结构。密文里出现次数最多的字母,大概率就是 E 或 T 的化身。三个字母反复出现的小词,八成是 THE。跟在撇号后面的单字母,几乎是 S 或 T。

破读者做的正是计分表设计者的逆运算:把密文频率和英语的先验频率一条条对齐认领。

源码也懂得封堵这条捷径。生成映射表时最多重试 40 次,直到没有任何字母映射到自己。这在数学上叫错排,随机排列恰好错排的概率趋于 1/e≈36.8%1/e \approx 36.8\%,所以平均两三次就能成功。

而错排的收益正是把「A 加密成 A」这类白送的锚点全部封掉,破读者只能老老实实从统计特征入手。

至于多人模式的中文谚语,先验频率表一失效,就得改靠字数、标点和成语的固定搭配来推。同一套「先验 × 证据」的推理,先验换了,后验全变,破读的直觉也得整个换掉。

至于拼词游戏,它是这条线上最轻的一环。20 道六字母题(planet、rocket、castle、dragon 这个难度带),答对一题 10 分,卡住了可以看释义提示,也可以直接跳下一题。计时器是正计时,只记账不惩罚,整体是一张「无压力」的牌桌。

但别小看这道题的搜索空间。六个字母的全排列有 6!=7206! = 720 种。人类当然不是在 720 种里线性扫描,而是在执行一层层剪枝。

Q 后面几乎必然跟 U,这一条就砍掉所有 Q 不在 U 前的排列。元音倾向于间隔出现。常见的辅音串很少出现在词尾。720 被砍到几十,几十再被「这是不是个词」的词典查询砍到一。

玩家在无意识中跑的就是带启发式的深度优先搜索。而这 20 道题恰好全部卡在「人工可解」的规模上。再长两个字母,8!=403208! = 40320 就把裸枚举逼出局,剪枝的价值随长度指数放大。

中文填字

填字游戏的机制核心不是词,是交叉点。

站内这版是中文填字。词库收了一百八十多个词,几乎全是双字词(人生、天气、大学、火山、西瓜……)外加「科学家」「对不起」「没关系」「辛苦了」四个三字词,去重后共 142 个不同的汉字担纲交叉任务。

生成一张盘的流程是这样的。先给词库建倒排索引:每个汉字映射到含它的全部词。给每个词算一个「交叉潜力分」,即它能与多少个其他词共享至少一个字。得分最高的词当种子,横放在 15×15 盘面正中。

之后进入多轮扫描。早期轮次设词长门槛(先放长词),后续轮次逐轮放宽。每轮里循环「收集盘面上所有可交叉的位置 × 按与盘面已有字的重合数排序候选词」,放得进去就放,直到无处可放。

整套生成会尝试多轮(简单 8 次、困难 16 次),每次换种子换随机顺序,最后保留放下词数最多的那张盘。难度在这里就是目标词数:简单 6 个、普通 10 个、困难 16 个。

放置前的合法性检查比想象中苛刻,一共四条。交叉处字符必须一致。除交叉点外,新词不得压到任何已有字母。新词每个格子的垂直方向相邻格必须为空(防止贴出来的字串拼出盘面之外的东西)。词头之前和词尾之后也必须是空格。

四条检查合起来只保证一件事:盘面上每一段连续的字都恰好是一个词库里的词。

英文填字为了盘面密度容忍大量「巧合串」,中文双字词结构太脆。这里选择用「只许在交叉点相触」换语义严格。牺牲密度,换取每一格的含义都干净。

解题端是生成端的镜像。一条提示「人的生命历程」加一个已知的首字,候选立刻从一百八十多个缩到十来个。再来一个交叉字,往往只剩唯一解。

变量是格子,取值是汉字,词是约束。这就是约束满足问题的日常形态。

而且被界面做成了肉眼可见的联动:点一条线索选中整条词,每个格子只收一个汉字,程序在每次输入后立刻核对全部线索,全对即自动判定完成,连提交按钮都不需要。

卡住了可以一键铺开全部答案。有个小细节:再点一次那个按钮并不是收起答案,而是直接换一张新盘。程序对「偷看完的盘面」不做续局处理,要玩就玩张干净的。

生成器与游玩是两个并排的模式标签,共用同一套生成内核。每多填对一个词,其余所有线索都变简单了一点。约束传播这个算法课名词,在这里是「填对一处、邻线候选塌缩」的具体手感。

猜词的剪枝

前面几节都是「词已知、顺序未知」,最后这一类游戏反其道:词本身未知,每次操作都在缩小候选集。

猜单词是其中无序的那种。词库 30 多个常见词(几乎全是六字母),你有 6 次猜错的机会。猜对一个字母,它在词里的所有位置同时亮出。猜错,绞架上多画一笔,画满六笔游戏结束。

字母之间没有大小序,二分用不上,但「按概率提问」的逻辑不变。把 26 个字母按出现频率排序,E、T、A、O、I、N 这一批高频字母是天然探针。猜中时赚到的位置信息最多,猜错时损失的预算换来的排除范围也最大。

源码把这条信息回路做得非常显性。猜过的字母在键盘上留下绿(在词中)红(不在词中)两种标记,配合遮罩里的已知位置(比如 S _ _ _ E R),候选集塌缩的速度远比直觉快。屏幕键盘之外它还监听物理键盘,26 个字母键直接当快捷键用。

6 次错误预算乍看苛刻,其实暗合频率表的分层。前六七个高频字母大概率命中,真正的考验是用位置模式读出剩下的空格。高频字母便宜,所以敢拿来当探针——拼字计分表里 E 只值 1 分,用的也是同一条定价逻辑。

猜数字是有序的那种,也是这类问题的教科书开局。1 到 100 之间想一个数,每次猜测只反馈「大了」或「小了」。

数轴上有单调性,于是有最优解。每次猜当前区间的中点,每次反馈恰好砍掉一半候选,⌈log⁡2100⌉=7\lceil \log_2 100 \rceil = 7 次必然锁定答案。而且这是信息意义上的下界,任何策略都无法稳定做得更好。

二分查找的前提(搜索空间单调)和代价(每次一步之内砍半)在这道小游戏里裸露得清清楚楚。想把这套思路接回正题库,站内的 Hot100 算法笔记里二分查找一节有完整的题单和单调性判据。

站内的实现还配了一本流水账。每次猜测连同结论进入历史记录,最佳纪录存进浏览器本地。玩家能直观看到自己离 7 次的理论下界差多远。

这个页面其实是个训练合集。猜数字只是默认标签,记忆翻牌、注意力计数、心算、反应速度、24 点都挂在同一组标签下。其中 24 点在你放弃时会暴力枚举四个数的所有两两组合与运算顺序来找解。搜索空间有限时,穷举加剪枝永远是最诚实的算法。


感谢您的阅读!如果可以,给俺点些关注吧~

游戏数值设计

周日 8月 16 2026
19691 字 · 68 分钟