烟花大师的核心循环是「组装烟花 → 参加比赛 → 拿名气和资金」。让这套循环转起来的关键是评分公式:它决定了玩家每一次组装决策会得到什么反馈。这篇笔记记录评分公式从拍脑袋到定稿的过程。
为什么不用阈值判定
最早的版本是阈值式的:高度超过 80 得满分,否则线性衰减。玩起来感觉「要么满分要么垃圾」,中间态毫无区分度,玩家凑出一个达标配方后就不再尝试新组合。
阈值的问题在于它把连续的属性空间切成了阶梯。烟花的高度、规模、颜色、持续时间、特效是五个连续维度,玩家真正有意思的是「再改一点点会不会更好」的探索感,阶梯式评分直接掐死了这个动机。
连续值匹配的思路
定稿方案是连续值匹配:每个事件对五个维度各有一个偏好值,玩家烟花属性与偏好的拟合度决定得分。偏离越小分越高,偏离方向不重要——超配和欠配同样扣分。
核心计算大概长这样:
def dimension_score(actual: float, preferred: float, tolerance: float) -> float:
"""单维度得分:偏离越少分越高,超出容差为 0。"""
deviation = abs(actual - preferred)
if deviation >= tolerance:
return 0.0
return 1.0 - (deviation / tolerance) ** 2
用平方而不是线性,是为了让「差一点」和「差很多」的手感差异更明显:接近偏好时曲线平缓(鼓励微调),远离偏好时快速衰减。
复杂度奖励与重复惩罚
光有拟合度会导致最优解永远是「单发贴脸配方」,表演毫无观赏性。所以总分里加了两项:
- 复杂度奖励:节目单里烟花弹数量、组件种类越多,加成越高,但有上限;
- 重复惩罚:同一配方重复出现会递减得分,逼玩家为一场表演设计多个不同烟花。
调参时踩过的坑
最大的坑是容差(tolerance)的取值:定得太宽,玩家随便装都能拿高分,赛事没有挑战性;定得太窄,预算限制的赛事根本完不成。最后的做法是每个职业等级一套容差基准——学徒宽、大师窄,让评分难度跟着玩家成长曲线走。
另一个教训是评分结果必须可解释。得分界面会逐维度显示拟合度条,玩家能看到「颜色差了一点」,而不是一个莫名其妙的总分——否则公式调得再好,玩家也学不会怎么改进。
下一篇打算写 Canvas 粒子表演的渲染管线,以及二次爆炸(多层壳)是怎么在粒子上实现的。