附录 C:全书公式、参数与数据集索引

《SLG 游戏产品设计》卷二附录 C:把第 1–15 章的核心公式、config 参数与 out 数据集做成可检索索引——公式名、所在节、形式与整数约束,参数名、所在配置文件、键路径、取值与单位,产物路径、产出脚本与对应节,并附一个直接解析本页、断言表里每条路径都真实存在的 S 层校验脚本。

正文每一节的推导都落在两样东西上:一份 config/ 里的整数参数,和一个 sim/ 脚本产出的 out/ 数据集。本页把第 1–15 章的公式、参数与产物抽成三张可检索的索引,并附一个直接解析本页的校验脚本——它会断言索引里列的每一条路径都真实存在。索引不是装饰:改了配置却忘了同步索引,脚本会当场报 FAIL。

全书口径一致:增长率与系数写整数比(growthNum / growthDen),比率写整数百分比或千分比,除法一律整数地板除,随机用自实现的确定性 PRNG(mulberry32,Python/TS 同种子)。

C.1 公式索引

「所在节」一列指向该公式的推导出处;形式栏给的是与 sim/ / proto/ 代码逐位一致的口径,不是数学近似。

公式所在节形式约束
有效攻击 effAtk1.1effAtk = atk × (100 + atkBuffPct) // 100整数百分比、地板除
战力 power(网格校验)1.2power = (wAtk·atk + wDef·def + wHp·hp) × count // den地板除
乘区真值 truth1.3truth = Π(100 + aᵢ) // 100^(k−1)整数幂、TS 用 BigInt
指数成长 v(L)2.1v(L) = base · growthNum^(L−1) // growthDen^(L−1)整数比、地板除
升级成本 cost(L)2.2cost(L) = base · r^(L−1),r = growthNum/growthDen整数比、地板除
单位伤害 dmg3.1dmg = max(minDamage, (atk·pct − def·df)·(100 + terrainPct)//100 // 100)地板除
存活数 alive3.3alive = (hpPool + hp − 1) // hp上取整
攻击分 attackScore4.2attackScore = atkSum × (100 + atkPct) // 100地板除
秩相关 ρ4.3ρ = (denom − 6Σd²) × 100 // denom,denom = n(n²−1)千分比
软保底 pity(n)5.2pity(n) = min(100, base + (n − softStart + 1)·step)n ≥ hard → 100
几何期望 E[X]5.3E[X] = 1/p,Var = (1 − p)/p²p = basePct/100
单卡概率 perCard6.1perCard = rarityPermille // cardsPerRarity三档和 = 1000‰
礼包性价比6.3性价比‰ = 玉璧等价价值 × 1000 // 售价(折玉璧)千分比
ELO 期望胜率 E_A7.1E_A = 1 / (1 + 10^((R_B − R_A)/400))新分 = R_A + K(S_A − E_A)
段位切分边界7.2idx = (cutPct × n) // 100nearest-rank、地板除
赛季膨胀 p′8.1p′ = p + dailyGain + p × growBp // 10000万分比、地板除
重置折算 kept8.2kept = max(floor, v × keepPct // 100)地板除
每元玉璧等价9.1jadePerYuan = valueJade × 100 // priceCents地板除
达成度 achievement9.3achievement = min(100, cum × 100 // required)上限 100
每元玉璧千分比10.1perYuanMilli = effJade × 100 × 1000 // priceCents千分比
弧弹性 E10.3E = ΔQ × P₀ // (Q₀ × ΔP)千分比、取绝对值
留存递推 R(d)11.1R(d) = R(d−1) × keep // 1000微千分比递推
LTV(N)11.2LTV(N) = Σ 留存率(d) × ARPDAU整数分
逐日存活率 survive(day)12.1survive(day) = min(cap, base + (day − 2)·decay)整数百分比
两比例样本量 n13.2n = (z_{1−α/2} + z_β)² (p₁(1−p₁) + p₂(1−p₂)) / (p₁−p₂)²上取整
相位排期14.1(day − fromDay) % cadenceDays < durationDays整数取模
赛季奖励预算14.2预算 = Σ 面额 × 参与率 × 次数地板除、免费/付费分开
基尼系数 gini15.1gini = 2·cum // (n·total) − (n + 1)/n升序、保留 3 位
有效战力 effA15.2effA = Pa × (100 + bias) // 100地板除

C.2 参数索引

「键路径」是在 config/ JSON 里的取址,点号分层、[i] 取数组下标。取值栏必须与磁盘上的配置逐位一致——下面的校验脚本会逐个回读核对。

参数所在 config键路径取值单位
样板攻击 atkconfig/n01_attrs.jsonsample.values.atk260点
升级成本增长率config/n02_curve.jsoncost.growthNum128整数比
减伤上限config/n03_battle.jsondamage.reductionCapPct70整数百分比
暴击率config/n03_battle.jsonrandom.critChancePct25整数百分比
克制地形加成config/n04_counter.jsonterrainBonusPct15整数百分比
三军齐备攻击加成config/n04_lineup.jsonbonuses[0].atkPct12整数百分比
保底基础概率config/n05_drops.jsonpity.basePct3整数百分比
硬保底抽数config/n05_drops.jsonpity.hardPity60抽
SSR 千分比config/n06_gacha.jsonpools[0].rarityPermille.ssr16千分比
ELO K 因子config/n07_match.jsonelo.kFactor32整数分
膨胀失控阈值config/n08_inflation.jsoncostBands.gapWarnBp50000万分比
资源保留率config/season.jsonreset.resourceKeepPct30整数百分比
付费墙最早日config/n09_protection.jsonprotection.minPaywallDay10天
档位梯度下限config/n10_pricing.jsontierAnchor.stepMinPermille45千分比
折扣需求弹性config/n10_pricing.jsondiscountWindow.liftPermillePerOffPct30千分比/折扣点
D1 留存config/n11_ltv.jsonretention.d1Permille450千分比
每安装成本 CACconfig/n11_ltv.jsonacquisition.cacCentsPerInstall400分
流失定义窗口config/n12_churn.jsonchurnDefinitionDays7天
显著性 αconfig/n13_experiment.jsonexperiment.alphaPermille50千分比
赛季长度config/season.jsonseasonLengthDays60天
奖励预算目标config/n14_event.jsontargetRewardBudgetPct30整数百分比
健康度活跃率阈值config/n15_alliance.jsonhealth.thresholds.activeRatePct60整数百分比

C.3 数据集索引

每个 S 层脚本产出 out/ 下的 JSON + CSV + SVG(B 层只出 JSON)。下表列的是主产物;.csv / .svg 与脚本同名前缀,不再单列。

产物产出脚本所在节
out/n01_attrs.jsonsim/n01_attrs.py1.1
out/n01_ext.jsonsim/n01_ext.py1.3
out/n02_cost.jsonsim/n02_cost_curve.py2.2
out/n02_fit.jsonsim/n02_curve_fit.py2.3
out/n03_damage.jsonsim/n03_damage.py3.1
out/n03_round_cross.jsonsim/n03_round_cross.py3.3
out/n04_counter_matrix.jsonsim/n04_counter_matrix.py4.1
out/n04_strength.jsonsim/n04_strength.py4.3
out/n05_pity.jsonsim/n05_pity.py5.2
out/n05_expect.jsonsim/n05_expect.py5.3
out/n06_cost.jsonsim/n06_expected_cost.py6.2
out/n06_bundle.jsonsim/n06_bundle_value.py6.3
out/n07_elo.jsonsim/n07_elo.py7.1
out/n07_fairness.jsonsim/n07_fairness.py7.3
out/n08_compress.jsonsim/n08_compress.py8.2
out/n08_catchup.jsonsim/n08_catchup.py8.3
out/n10_elasticity.jsonsim/n10_elasticity.py10.3
out/n11_ltv.jsonsim/n11_ltv_payback.py11.2
out/n12_joint.jsonsim/n12_joint.py12.3
out/n13_stats.jsonsim/n13_pitfalls.py13.2
out/n14_settlement.jsonsim/n14_settlement.py14.3
out/n15_gvg.jsonsim/n15_gvg.py15.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 节;上面两个脚本合起来,把「索引里写了什么」与「磁盘上真有什么」绑在了一起。改配置、加脚本、动公式,先跑它们,再动正文。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「slg」更多文章

  1. 12.3 留存与付费的联合分析
  2. 12.2 流失预警与召回
  3. 9.3 付费节奏与体验保护