正文每一节的推导都落在两样东西上:一份 config/ 里的整数参数,和一个 sim/ 脚本产出的 out/ 数据集。本页把第 1–15 章的公式、参数与产物抽成三张可检索的索引,并附一个直接解析本页的校验脚本——它会断言索引里列的每一条路径都真实存在。索引不是装饰:改了配置却忘了同步索引,脚本会当场报 FAIL。
全书口径一致:增长率与系数写整数比(growthNum / growthDen),比率写整数百分比或千分比,除法一律整数地板除,随机用自实现的确定性 PRNG(mulberry32,Python/TS 同种子)。
C.1 公式索引
「所在节」一列指向该公式的推导出处;形式栏给的是与 sim/ / proto/ 代码逐位一致的口径,不是数学近似。
| 公式 | 所在节 | 形式 | 约束 |
|---|---|---|---|
| 有效攻击 effAtk | 1.1 | effAtk = atk × (100 + atkBuffPct) // 100 | 整数百分比、地板除 |
| 战力 power(网格校验) | 1.2 | power = (wAtk·atk + wDef·def + wHp·hp) × count // den | 地板除 |
| 乘区真值 truth | 1.3 | truth = Π(100 + aᵢ) // 100^(k−1) | 整数幂、TS 用 BigInt |
| 指数成长 v(L) | 2.1 | v(L) = base · growthNum^(L−1) // growthDen^(L−1) | 整数比、地板除 |
| 升级成本 cost(L) | 2.2 | cost(L) = base · r^(L−1),r = growthNum/growthDen | 整数比、地板除 |
| 单位伤害 dmg | 3.1 | dmg = max(minDamage, (atk·pct − def·df)·(100 + terrainPct)//100 // 100) | 地板除 |
| 存活数 alive | 3.3 | alive = (hpPool + hp − 1) // hp | 上取整 |
| 攻击分 attackScore | 4.2 | attackScore = atkSum × (100 + atkPct) // 100 | 地板除 |
| 秩相关 ρ | 4.3 | ρ = (denom − 6Σd²) × 100 // denom,denom = n(n²−1) | 千分比 |
| 软保底 pity(n) | 5.2 | pity(n) = min(100, base + (n − softStart + 1)·step) | n ≥ hard → 100 |
| 几何期望 E[X] | 5.3 | E[X] = 1/p,Var = (1 − p)/p² | p = basePct/100 |
| 单卡概率 perCard | 6.1 | perCard = rarityPermille // cardsPerRarity | 三档和 = 1000‰ |
| 礼包性价比 | 6.3 | 性价比‰ = 玉璧等价价值 × 1000 // 售价(折玉璧) | 千分比 |
| ELO 期望胜率 E_A | 7.1 | E_A = 1 / (1 + 10^((R_B − R_A)/400)) | 新分 = R_A + K(S_A − E_A) |
| 段位切分边界 | 7.2 | idx = (cutPct × n) // 100 | nearest-rank、地板除 |
| 赛季膨胀 p′ | 8.1 | p′ = p + dailyGain + p × growBp // 10000 | 万分比、地板除 |
| 重置折算 kept | 8.2 | kept = max(floor, v × keepPct // 100) | 地板除 |
| 每元玉璧等价 | 9.1 | jadePerYuan = valueJade × 100 // priceCents | 地板除 |
| 达成度 achievement | 9.3 | achievement = min(100, cum × 100 // required) | 上限 100 |
| 每元玉璧千分比 | 10.1 | perYuanMilli = effJade × 100 × 1000 // priceCents | 千分比 |
| 弧弹性 E | 10.3 | E = ΔQ × P₀ // (Q₀ × ΔP) | 千分比、取绝对值 |
| 留存递推 R(d) | 11.1 | R(d) = R(d−1) × keep // 1000 | 微千分比递推 |
| LTV(N) | 11.2 | LTV(N) = Σ 留存率(d) × ARPDAU | 整数分 |
| 逐日存活率 survive(day) | 12.1 | survive(day) = min(cap, base + (day − 2)·decay) | 整数百分比 |
| 两比例样本量 n | 13.2 | n = (z_{1−α/2} + z_β)² (p₁(1−p₁) + p₂(1−p₂)) / (p₁−p₂)² | 上取整 |
| 相位排期 | 14.1 | (day − fromDay) % cadenceDays < durationDays | 整数取模 |
| 赛季奖励预算 | 14.2 | 预算 = Σ 面额 × 参与率 × 次数 | 地板除、免费/付费分开 |
| 基尼系数 gini | 15.1 | gini = 2·cum // (n·total) − (n + 1)/n | 升序、保留 3 位 |
| 有效战力 effA | 15.2 | effA = Pa × (100 + bias) // 100 | 地板除 |
C.2 参数索引
「键路径」是在 config/ JSON 里的取址,点号分层、[i] 取数组下标。取值栏必须与磁盘上的配置逐位一致——下面的校验脚本会逐个回读核对。
| 参数 | 所在 config | 键路径 | 取值 | 单位 |
|---|---|---|---|---|
| 样板攻击 atk | config/n01_attrs.json | sample.values.atk | 260 | 点 |
| 升级成本增长率 | config/n02_curve.json | cost.growthNum | 128 | 整数比 |
| 减伤上限 | config/n03_battle.json | damage.reductionCapPct | 70 | 整数百分比 |
| 暴击率 | config/n03_battle.json | random.critChancePct | 25 | 整数百分比 |
| 克制地形加成 | config/n04_counter.json | terrainBonusPct | 15 | 整数百分比 |
| 三军齐备攻击加成 | config/n04_lineup.json | bonuses[0].atkPct | 12 | 整数百分比 |
| 保底基础概率 | config/n05_drops.json | pity.basePct | 3 | 整数百分比 |
| 硬保底抽数 | config/n05_drops.json | pity.hardPity | 60 | 抽 |
| SSR 千分比 | config/n06_gacha.json | pools[0].rarityPermille.ssr | 16 | 千分比 |
| ELO K 因子 | config/n07_match.json | elo.kFactor | 32 | 整数分 |
| 膨胀失控阈值 | config/n08_inflation.json | costBands.gapWarnBp | 50000 | 万分比 |
| 资源保留率 | config/season.json | reset.resourceKeepPct | 30 | 整数百分比 |
| 付费墙最早日 | config/n09_protection.json | protection.minPaywallDay | 10 | 天 |
| 档位梯度下限 | config/n10_pricing.json | tierAnchor.stepMinPermille | 45 | 千分比 |
| 折扣需求弹性 | config/n10_pricing.json | discountWindow.liftPermillePerOffPct | 30 | 千分比/折扣点 |
| D1 留存 | config/n11_ltv.json | retention.d1Permille | 450 | 千分比 |
| 每安装成本 CAC | config/n11_ltv.json | acquisition.cacCentsPerInstall | 400 | 分 |
| 流失定义窗口 | config/n12_churn.json | churnDefinitionDays | 7 | 天 |
| 显著性 α | config/n13_experiment.json | experiment.alphaPermille | 50 | 千分比 |
| 赛季长度 | config/season.json | seasonLengthDays | 60 | 天 |
| 奖励预算目标 | config/n14_event.json | targetRewardBudgetPct | 30 | 整数百分比 |
| 健康度活跃率阈值 | config/n15_alliance.json | health.thresholds.activeRatePct | 60 | 整数百分比 |
C.3 数据集索引
每个 S 层脚本产出 out/ 下的 JSON + CSV + SVG(B 层只出 JSON)。下表列的是主产物;.csv / .svg 与脚本同名前缀,不再单列。
| 产物 | 产出脚本 | 所在节 |
|---|---|---|
out/n01_attrs.json | sim/n01_attrs.py | 1.1 |
out/n01_ext.json | sim/n01_ext.py | 1.3 |
out/n02_cost.json | sim/n02_cost_curve.py | 2.2 |
out/n02_fit.json | sim/n02_curve_fit.py | 2.3 |
out/n03_damage.json | sim/n03_damage.py | 3.1 |
out/n03_round_cross.json | sim/n03_round_cross.py | 3.3 |
out/n04_counter_matrix.json | sim/n04_counter_matrix.py | 4.1 |
out/n04_strength.json | sim/n04_strength.py | 4.3 |
out/n05_pity.json | sim/n05_pity.py | 5.2 |
out/n05_expect.json | sim/n05_expect.py | 5.3 |
out/n06_cost.json | sim/n06_expected_cost.py | 6.2 |
out/n06_bundle.json | sim/n06_bundle_value.py | 6.3 |
out/n07_elo.json | sim/n07_elo.py | 7.1 |
out/n07_fairness.json | sim/n07_fairness.py | 7.3 |
out/n08_compress.json | sim/n08_compress.py | 8.2 |
out/n08_catchup.json | sim/n08_catchup.py | 8.3 |
out/n10_elasticity.json | sim/n10_elasticity.py | 10.3 |
out/n11_ltv.json | sim/n11_ltv_payback.py | 11.2 |
out/n12_joint.json | sim/n12_joint.py | 12.3 |
out/n13_stats.json | sim/n13_pitfalls.py | 13.2 |
out/n14_settlement.json | sim/n14_settlement.py | 14.3 |
out/n15_gvg.json | sim/n15_gvg.py | 15.2 |
C.4 索引一致性校验(S 层)
下面这段是 S 层脚本:它读本页自己的 Markdown,按小节切出三张表,逐行断言——公式表的 slug 有同名 .md、参数表的键路径与取值和 JSON 一致、数据集表的产物与脚本都在磁盘上。任何一处对不上就 FAIL 并返回非零退出码。
# file: sim/nC_index_check.py
# tier: S
# verify: python3 sim/nC_index_check.py --config config/ --out out/ --doc "$HOME/projects/birdor/plumephp/content/posts/books/数值平衡与商业化运营/slg-num-appendix-c.md"
"""附录 C 索引一致性校验:解析附录三张索引表,断言每条路径真实存在。
公式表 -> 节 slug 有同名 .md;参数表 -> config 键路径与取值逐位一致;
数据集表 -> out/ 产物与产出脚本都在磁盘上。零依赖,只读。"""
from __future__ import annotations
import argparse, json, os, re
RELREF = re.compile(r'relref "([^"]+)"')
def tick(c): return c.strip().strip("`").strip()
def rows(text, heading):
m = re.search(r"^## " + re.escape(heading) + r".*?$", text, re.M)
rest = text[m.end():]
nxt = re.search(r"^## ", rest, re.M)
rest = rest[:nxt.start()] if nxt else rest
data = [[x.strip() for x in ln.strip("|").split("|")]
for ln in rest.splitlines() if ln.strip().startswith("|")]
return [r for r in data if not set(r[0]) <= set("-: ")][1:]
def walk(doc, path):
cur = doc
for part in path.split("."):
m = re.fullmatch(r"(\w+)(?:\[(\d+)\])?", part)
cur = cur[m.group(1)]
if m.group(2) is not None: cur = cur[int(m.group(2))]
return cur
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
ap.add_argument("--doc", required=True)
a = ap.parse_args()
doc = os.path.expanduser(os.path.expandvars(a.doc))
content = os.path.dirname(doc)
text = open(doc, encoding="utf-8").read()
fail = []
f = rows(text, "C.1 公式索引")
for r in f:
slug = RELREF.search(r[1]).group(1)
if not os.path.exists(os.path.join(content, slug + ".md")):
fail.append("公式索引节缺失:" + slug)
p = rows(text, "C.2 参数索引")
for r in p:
cfg, key, want = tick(r[1]), tick(r[2]), int(tick(r[3]))
try:
fp = os.path.join(a.config, os.path.basename(cfg))
got = int(walk(json.load(open(fp, encoding="utf-8")), key))
except Exception as exc:
fail.append(f"参数索引失效:{cfg}:{key} ({exc})"); continue
if got != want:
fail.append(f"参数取值不符:{cfg}:{key} 期望 {want} 实为 {got}")
d = rows(text, "C.3 数据集索引")
for r in d:
prod, script = tick(r[0]), tick(r[1])
if not os.path.exists(os.path.join(a.out, os.path.basename(prod))):
fail.append("数据集缺失:" + prod)
if not os.path.exists(os.path.join("sim", os.path.basename(script))):
fail.append("产出脚本缺失:" + script)
print(f"C.1 公式索引:{len(f)} 行,节 slug 全部命中 .md")
print(f"C.2 参数索引:{len(p)} 行,config 键路径与取值全部一致")
print(f"C.3 数据集索引:{len(d)} 行,out/ 产物与产出脚本全部存在")
total = len(f) + len(p) + len(d)
for m in fail: print("FAIL " + m)
if fail:
print(f"FAIL 共 {len(fail)} 处不一致(索引 {total} 行)"); return 1
print(f"OK 三张索引表共 {total} 行,全部与磁盘一致"); return 0
if __name__ == "__main__":
raise SystemExit(main())
本机实跑输出(工作区 $HOME/.slgbook/bastion):
C.1 公式索引:29 行,节 slug 全部命中 .md
C.2 参数索引:22 行,config 键路径与取值全部一致
C.3 数据集索引:22 行,out/ 产物与产出脚本全部存在
OK 三张索引表共 73 行,全部与磁盘一致
C.5 公式基准复算(B 层)
S 层校验的是路径存在,不校验公式本身。下面这段 B 层脚本从 config/ 复算几个代表性公式,并与附录印出的数值对照:属性派生链、升级成本末级、软保底、每元玉璧千分比、第 7 天达成度。它是软告警——偏差只打印,退出码恒 0。
# file: sim/nC_formula_check.py
# tier: B
# verify: python3 sim/nC_formula_check.py --config config/ --out out/
"""附录 C 公式基准:从 config/ 与 out/ 复算代表性公式,与附录印出的数值对照。
B 层软告警——偏差只打印,退出码恒 0。整数地板除,与正文口径一致。"""
from __future__ import annotations
import argparse, json
def load(p): return json.load(open(p, encoding="utf-8"))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
a = ap.parse_args()
warn = []
def chk(tag, got, want):
if got != want: warn.append(f"WARN {tag} 复算 {got} != 附录 {want}")
v = load(f"{a.config}/n01_attrs.json")["sample"]["values"]
ea = v["atk"] * (100 + v["techAtkPct"] + v["heroAtkPct"]) // 100
ed = v["def"] * (100 + v["techDefPct"] + v["heroDefPct"]) // 100
mt = min(ed * 50 // 100, 75)
ehp = v["hp"] * 100 // (100 - mt)
dps = ea * v["speed"] // 100
power = ea * 3 + ed * 2 + ehp + dps * 5
for n, g, w in [("effAtk", ea, 338), ("effDef", ed, 180), ("mitigationPct", mt, 75),
("ehp", ehp, 1440), ("dps", dps, 47), ("power", power, 3049)]:
chk(f"1.1 {n}", g, w)
print(f"1.1 effAtk={ea} effDef={ed} mitigationPct={mt} ehp={ehp} dps={dps} power={power}")
c = load(f"{a.config}/n02_curve.json")["cost"]
n = c["levels"] - 1
cost30 = c["base"] * c["growthNum"] ** n // c["growthDen"] ** n
prod = load(f"{a.out}/n02_cost.json")["report"]["costLast"]
chk("2.2 末级成本", prod, cost30)
print(f"2.2 cost(30) 复算 = {cost30},out/n02_cost.json costLast = {prod}")
p = load(f"{a.config}/n05_drops.json")["pity"]
pity = 100 if 45 >= p["hardPity"] else min(
100, p["basePct"] + (45 - p["softPityStart"] + 1) * p["softPityStepPct"])
chk("5.2 pity(45)", pity, 39)
print(f"5.2 pity(45) = {pity}%")
t1 = load(f"{a.config}/pricing.json")["tiers"][0]
milli = t1["jade"] * (100 + t1["bonusPct"]) // 100 * 100 * 1000 // t1["priceCents"]
chk("10.1 t1 每元玉璧千分比", milli, 10000)
print(f"10.1 t1 每元玉璧千分比 = {milli}")
pr = load(f"{a.config}/n09_protection.json")
cum = pr["players"]["free"]["dailyResource"] * 7
req = next(m["requiredResource"] for m in pr["contentMilestones"] if m["day"] == 7)
ach = min(100, cum * 100 // req)
chk("9.3 第 7 天达成度", ach, 93)
print(f"9.3 免费玩家第 7 天达成度 = {ach}%")
print("7.1 同分 ELO 期望胜率 = 500‰")
for w in warn: print(w)
print("OK 公式基准复算完成,0 处软告警" if not warn
else f"WARN 公式基准复算完成,{len(warn)} 处偏差(软告警,不硬失败)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
本机实跑输出:
1.1 effAtk=338 effDef=180 mitigationPct=75 ehp=1440 dps=47 power=3049
2.2 cost(30) 复算 = 154266,out/n02_cost.json costLast = 154266
5.2 pity(45) = 39%
10.1 t1 每元玉璧千分比 = 10000
9.3 免费玩家第 7 天达成度 = 93%
7.1 同分 ELO 期望胜率 = 500‰
OK 公式基准复算完成,0 处软告警
三张索引表覆盖第 1–15 章共 45 节;上面两个脚本合起来,把「索引里写了什么」与「磁盘上真有什么」绑在了一起。改配置、加脚本、动公式,先跑它们,再动正文。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。