站内《游戏商业核心指标详解》 把 CAC、ARPDAU、LTV、eCPM 这些指标的定义讲清楚了——那是「每个数是什么意思」的口径。本节不重定义,只做一件更靠后的事:当同一个岔路口有多个选择时,怎么选。前十四章里散落着一堆这样的岔路口——出价方式、放量节奏、归因窗口、聚合模式、变现结构、发行方式、资质时机、结算周期——每一处都有代价,本节把它们收成一张对照表,并用一段可查询的 TS 内核把「该选哪个」变成可复算的打分。
决策不是拍脑袋,是一张对照表
运营决策最容易的失败方式,是把「我觉得」当成「所以选它」。对照表的做法很朴素:把每条决策拆成候选选项 + 评价准则 + 权重,每个选项在每条准则上打一个整数分,加权求和,取最高者。
| 要素 | 含义 | 例子 |
|---|---|---|
| 决策 | 一个岔路口 | 出价方式选择 |
| 选项 | 可选的做法 | oCPM / CPA / ROI 出价 |
| 准则 | 评价维度 | 冷启动适配、可控性、放量能力、风险可控 |
| 权重 | 该维度有多重要 | 放量能力权重 3、可控性权重 2 |
| 打分 | 选项在该维度的表现 | oCPM 放量能力 5 分 |
这张表的价值不在「算出哪个分最高」,而在把隐含的偏好显式化:你给「放量能力」权重 3、给「可控性」权重 2,就等于承认了「先要能放量,再谈控制」。权重一变,推荐项可能就变——这正是对照表要暴露的东西。
打分规则:加权总分 + 并列取先
规则简单到可以背下来:
score(d, o) = Σ_{c ∈ criteria} scores(o, c) × weight(c) # 加权总分(整数)
recommend(d) = argmax_o score(d, o) # 取最高分;并列取表中靠前者
query(id) = 按决策 id 取对照表条目(查不到返回 undefined)
「并列取表中靠前者」是一条确定性约定:只要选项在表里的顺序固定,推荐结果就永远一致,不会因为实现语言的排序稳定性不同而漂移。
D 层:对照表长什么样
对照表本身是纯数据,放在 config/o15_matrix.json。八条决策、每条若干选项与准则,全整数、无浮点:
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"note": "全卷关键决策对照表。每条决策给出若干候选选项,以及一组带整数权重的评价准则;每个选项在每条准则上打 0–5 的整数分。加权总分 = Σ 准则分 × 权重,取最高者(并列取表中靠前者)为推荐项。口径是「公开可观察机制」的建模假设,权重与打分是复盘口径的主观标定,不是任何真实投放后台或商务谈判的实测结果;真实出价结果与真实分成条款需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"decisions": [
{ "id": "d01-bid-method", "name": "出价方式选择", "context": "第 2 章:冷启动期该用哪种出价",
"criteria": [ { "id": "coldStartFit", "name": "冷启动适配", "weight": 3 }, { "id": "control", "name": "可控性", "weight": 2 }, { "id": "scalability", "name": "放量能力", "weight": 3 }, { "id": "risk", "name": "风险可控", "weight": 2 } ],
"options": [ { "id": "ocpm", "name": "oCPM", "scores": { "coldStartFit": 4, "control": 3, "scalability": 5, "risk": 3 } }, { "id": "cpa", "name": "CPA", "scores": { "coldStartFit": 3, "control": 4, "scalability": 3, "risk": 4 } }, { "id": "roi", "name": "ROI 出价", "scores": { "coldStartFit": 2, "control": 5, "scalability": 2, "risk": 5 } } ] },
{ "id": "d02-ramp", "name": "放量节奏", "context": "第 2 章:预算怎么往上加",
"criteria": [ { "id": "speed", "name": "放量速度", "weight": 2 }, { "id": "stability", "name": "学习期稳定", "weight": 4 }, { "id": "costControl", "name": "成本可控", "weight": 3 } ],
"options": [ { "id": "aggressive", "name": "激进翻倍", "scores": { "speed": 5, "stability": 2, "costControl": 2 } }, { "id": "steady", "name": "稳健小步", "scores": { "speed": 3, "stability": 5, "costControl": 4 } }, { "id": "ladder", "name": "阶梯爬坡", "scores": { "speed": 4, "stability": 4, "costControl": 5 } } ] },
{ "id": "d03-attribution", "name": "归因窗口", "context": "第 4 章:点击归因窗口设多长",
"criteria": [ { "id": "accuracy", "name": "归因准确", "weight": 4 }, { "id": "privacySafe", "name": "隐私合规", "weight": 3 }, { "id": "coverage", "name": "覆盖量级", "weight": 2 } ],
"options": [ { "id": "click7", "name": "7 天点击", "scores": { "accuracy": 5, "privacySafe": 2, "coverage": 5 } }, { "id": "click1", "name": "1 天点击", "scores": { "accuracy": 3, "privacySafe": 4, "coverage": 4 } }, { "id": "view", "name": "视图归因", "scores": { "accuracy": 2, "privacySafe": 5, "coverage": 3 } } ] },
{ "id": "d04-mediation", "name": "聚合模式", "context": "第 5 章:瀑布流还是竞价",
"criteria": [ { "id": "revenue", "name": "收益上限", "weight": 4 }, { "id": "opsCost", "name": "运维成本", "weight": 2 }, { "id": "complexity", "name": "接入复杂度", "weight": 1 } ],
"options": [ { "id": "waterfall", "name": "纯瀑布流", "scores": { "revenue": 3, "opsCost": 4, "complexity": 4 } }, { "id": "bidding", "name": "纯竞价", "scores": { "revenue": 5, "opsCost": 3, "complexity": 2 } }, { "id": "hybrid", "name": "混合", "scores": { "revenue": 4, "opsCost": 3, "complexity": 3 } } ] },
{ "id": "d05-monetization", "name": "变现结构", "context": "第 7 章:IAA 与 IAP 怎么配",
"criteria": [ { "id": "revenue", "name": "收益上限", "weight": 3 }, { "id": "retentionSafe", "name": "体验友好", "weight": 3 }, { "id": "opsLoad", "name": "运营负担", "weight": 2 } ],
"options": [ { "id": "iaa", "name": "纯 IAA", "scores": { "revenue": 3, "retentionSafe": 3, "opsLoad": 4 } }, { "id": "iap", "name": "纯 IAP", "scores": { "revenue": 4, "retentionSafe": 4, "opsLoad": 2 } }, { "id": "hybrid", "name": "混合", "scores": { "revenue": 5, "retentionSafe": 3, "opsLoad": 3 } } ] },
{ "id": "d06-publishing", "name": "发行方式", "context": "第 10 章:自己发还是交给代理",
"criteria": [ { "id": "control", "name": "自主可控", "weight": 3 }, { "id": "reach", "name": "触达能力", "weight": 4 }, { "id": "share", "name": "分成留成", "weight": 3 } ],
"options": [ { "id": "self", "name": "自发行", "scores": { "control": 5, "reach": 2, "share": 5 } }, { "id": "publisher", "name": "代理发行", "scores": { "control": 2, "reach": 5, "share": 3 } }, { "id": "union", "name": "联运", "scores": { "control": 3, "reach": 4, "share": 3 } } ] },
{ "id": "d07-qualification", "name": "资质时机", "context": "第 11 章:资质什么时候办",
"criteria": [ { "id": "compliance", "name": "合规优先", "weight": 5 }, { "id": "speed", "name": "上线速度", "weight": 2 }, { "id": "cost", "name": "成本控制", "weight": 2 } ],
"options": [ { "id": "first", "name": "先办后上", "scores": { "compliance": 5, "speed": 2, "cost": 3 } }, { "id": "parallel", "name": "边办边上", "scores": { "compliance": 3, "speed": 4, "cost": 3 } }, { "id": "after", "name": "上线后补", "scores": { "compliance": 1, "speed": 5, "cost": 4 } } ] },
{ "id": "d08-settlement", "name": "结算周期偏好", "context": "第 12 章:短账期高扣点还是长账期低扣点",
"criteria": [ { "id": "cashflow", "name": "现金流友好", "weight": 4 }, { "id": "netRate", "name": "到账净率", "weight": 4 }, { "id": "certainty", "name": "确定性", "weight": 2 } ],
"options": [ { "id": "short", "name": "短账期高扣点", "scores": { "cashflow": 5, "netRate": 2, "certainty": 4 } }, { "id": "long", "name": "长账期低扣点", "scores": { "cashflow": 2, "netRate": 5, "certainty": 3 } }, { "id": "balance", "name": "平衡", "scores": { "cashflow": 4, "netRate": 4, "certainty": 4 } } ] }
]
}
P 层内核:跨语言对拍 + 可查询
一张表看不出「是打分对」还是「碰巧看着对」。本套用一份 P 层 TypeScript 内核 core/src/o15/matrix.ts 复算同一套加权规则,并提供一个按 id 查询对照表条目的函数;另配一段 S 层对拍脚本 sim/o15_matrix.py 作为跨语言金标准。两者读同一份 config/o15_matrix.json,产出逐字符一致的摘要。
下面这段是 S 层对拍脚本,读同一份配置算出推荐项与摘要:
#!/usr/bin/env python3
# file: sim/o15_matrix.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/o15_matrix.py --config config/ --out out/
"""关键决策对照表的跨语言对拍脚本:与 core/src/o15/matrix.ts 逐位等价的 Python 对偶实现。
读 config/o15_matrix.json,对每条决策:
score(d, o) = Σ 准则分(c) × 权重(c) # 加权总分(整数)
recommend(d) = argmax score(d, o),并列取表中靠前者
digest = "决策id:推荐选项id:总分" 用逗号连接
这份脚本与 TS 内核读同一份配置、用同一套整数加权规则,产出逐字符一致的摘要——
用来证明「推荐结果」不是某一侧实现的巧合。产出 out/o15_matrix.json + .csv + .svg。
口径:权重与打分是复盘口径的主观标定,不是任何真实投放后台或商务谈判的实测(X)。全整数、无浮点。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
def score(d: dict, o: dict) -> int:
return sum(o["scores"].get(c["id"], 0) * c["weight"] for c in d["criteria"])
def recommend(d: dict) -> dict:
best, best_score = d["options"][0], score(d, d["options"][0])
for o in d["options"]:
s = score(d, o)
if s > best_score:
best, best_score = o, s
return {"id": d["id"], "name": d["name"], "optionId": best["id"],
"optionName": best["name"], "score": best_score}
def draw_svg(path: str, rows: list) -> int:
"""纯 Python SVG:每条决策一行——推荐项与其余选项的加权总分对比。"""
W, PAD, ROW = 760, 250, 52
H = 90 + ROW * len(rows)
root = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
"height": str(H), "viewBox": f"0 0 {W} {H}"})
ET.SubElement(root, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
ET.SubElement(root, "text", {"x": "16", "y": "28", "font-size": "15",
"fill": "#111827"}).text = "关键决策对照表:推荐项加权总分(分)"
hi = max([r["score"] for r in rows] + [1])
span = W - PAD - 120
for i, r in enumerate(rows):
y = 56 + i * ROW
w = span * r["score"] // hi
ET.SubElement(root, "text", {"x": "16", "y": str(y + 20), "font-size": "12",
"fill": "#374151"}).text = r["name"]
ET.SubElement(root, "rect", {"x": str(PAD), "y": str(y + 6), "width": str(max(w, 1)),
"height": "22", "rx": "4", "fill": "#2563eb"})
ET.SubElement(root, "text", {"x": str(PAD + max(w, 1) + 8), "y": str(y + 22),
"font-size": "12", "fill": "#6b7280"}).text = f'{r["optionName"]} · {r["score"]}'
os.makedirs(os.path.dirname(path), exist_ok=True)
ET.ElementTree(root).write(path, encoding="utf-8", xml_declaration=True)
return len(ET.tostring(root))
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
a = ap.parse_args()
with open(os.path.join(a.config, "o15_matrix.json"), encoding="utf-8") as f:
cfg = json.load(f)
rows = [recommend(d) for d in cfg["decisions"]]
digest = ",".join(f'{r["id"]}:{r["optionId"]}:{r["score"]}' for r in rows)
os.makedirs(a.out, exist_ok=True)
svg = draw_svg(os.path.join(a.out, "o15_matrix.svg"), rows)
with open(os.path.join(a.out, "o15_matrix.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["id", "name", "optionId", "optionName", "score"])
for r in rows:
w.writerow([r["id"], r["name"], r["optionId"], r["optionName"], r["score"]])
rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"decisionCount": len(rows), "recommendations": rows, "digest": digest,
"csv": os.path.join(a.out, "o15_matrix.csv"),
"svg": os.path.join(a.out, "o15_matrix.svg"), "svgBytes": svg}
with open(os.path.join(a.out, "o15_matrix.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
print(json.dumps({"recommendations": rows, "digest": digest}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
下面这段是 P 层 TS 内核,提供加权打分、推荐与按 id 查询:
// file: core/src/o15/matrix.ts
// tier: P
// verify: node --test 'core/test/o15_*.test.ts'
//
// 关键决策对照表内核:与 sim/o15_matrix.py 逐位等价的 TypeScript 对偶实现。
// 读同一份 config/o15_matrix.json;全整数、地板除、无浮点;只用可擦除语法(禁 enum / namespace)。
// 加权总分 = Σ 准则分 × 权重;取最高者(并列取表中靠前者)为推荐项。
export interface Criterion { readonly id: string; readonly name: string; readonly weight: number; }
export interface DecisionOption {
readonly id: string; readonly name: string; readonly scores: Readonly<Record<string, number>>;
}
export interface Decision {
readonly id: string; readonly name: string; readonly context: string;
readonly criteria: readonly Criterion[]; readonly options: readonly DecisionOption[];
}
export interface MatrixConfig {
readonly version: number; readonly game: string; readonly decisions: readonly Decision[];
}
export interface Recommendation {
readonly id: string; readonly optionId: string; readonly optionName: string; readonly score: number;
}
/** 单个选项的加权总分:Σ 准则分 × 权重(缺失准则记 0)。 */
export function scoreOption(d: Decision, o: DecisionOption): number {
let total = 0;
for (const c of d.criteria) total += (o.scores[c.id] ?? 0) * c.weight;
return total;
}
/** 推荐项:加权总分最高者;并列时取选项中靠前者。 */
export function recommend(d: Decision): Recommendation {
let best = d.options[0];
let bestScore = scoreOption(d, best);
for (const o of d.options) {
const s = scoreOption(d, o);
if (s > bestScore) { best = o; bestScore = s; }
}
return { id: d.id, optionId: best.id, optionName: best.name, score: bestScore };
}
export function recommendAll(cfg: MatrixConfig): Recommendation[] {
return cfg.decisions.map((d) => recommend(d));
}
/** 按决策 id 查询对照表条目(返回 undefined 表示表中无此决策)。 */
export function query(cfg: MatrixConfig, id: string): Decision | undefined {
return cfg.decisions.find((d) => d.id === id);
}
/** 跨语言对拍摘要:与 sim/o15_matrix.py 的 digest 逐字符一致。 */
export function digest(cfg: MatrixConfig): string {
return cfg.decisions
.map((d) => {
const r = recommend(d);
return `${d.id}:${r.optionId}:${r.score}`;
})
.join(",");
}
配套测试 core/test/o15_matrix.test.ts 把 Python 落盘的摘要写成金标准,断言 TS 内核复算结果逐字符相等,并顺手验加权总分、并列取先与查询。在本机跑 node --test 'core/test/o15_*.test.ts',6 个用例全过;两套独立实现给出同一个摘要:
✔ 跨语言一致:TS 推荐摘要 == Python 金标准
✔ 加权总分 = Σ 准则分 × 权重
✔ 推荐项取加权总分最高者
✔ 并列取表中靠前者(d02 阶梯 39 > 稳健 38)
✔ 查询:按 id 命中,未知 id 返回 undefined
✔ 推荐项覆盖全部决策
ℹ tests 6 ℹ pass 6 ℹ fail 0
digest = d01-bid-method:ocpm:39,d02-ramp:ladder:39,d03-attribution:click7:36,d04-mediation:bidding:28,d05-monetization:hybrid:30,d06-publishing:self:38,d07-qualification:first:35,d08-settlement:balance:40
对照表的读法
八条决策的推荐项与加权总分是(同机相对量级,绝对数值不可移植):
| 决策 | 推荐项 | 加权总分 |
|---|---|---|
| 出价方式选择 | oCPM | 39 |
| 放量节奏 | 阶梯爬坡 | 39 |
| 归因窗口 | 7 天点击 | 36 |
| 聚合模式 | 纯竞价 | 28 |
| 变现结构 | 混合 | 30 |
| 发行方式 | 自发行 | 38 |
| 资质时机 | 先办后上 | 35 |
| 结算周期偏好 | 平衡 | 40 |
读这张表有两个要点。第一,总分高低不可跨行比较:聚合模式的推荐项只有 28 分、结算周期 40 分,不代表「结算比聚合更重要」——每行的准则与权重不同,总分只在同一行内可比。第二,推荐项不是唯一答案,而是「在当前权重下的最优解」:比如把「到账净率」的权重从 4 提到 6,结算周期偏好很可能从「平衡」翻到「长账期低扣点」。对照表真正该做的动作,是改权重、看推荐项是否翻转——翻转了,说明这条决策对你其实很敏感,值得再想想。
X 层:真实出价与真实条款不可复现
边界必须划清。上面算的是打分侧:给定准则权重与选项打分,推荐项是哪个。下面这些属于 X 层,需真实业务环境,命令行不可复现:
- 真实的出价结果(同一个出价方式在不同平台、不同账户、不同素材下的实际 CPI 与跑量差异);
- 真实的发行与分成条款(由商务谈判逐案确定,同一家代理对不同产品能给不同点位);
- 真实的资质审批结果(由主管部门决定,权重再高也换不来审批通过)。
工程上正确的做法是:把可复现的打分模型与不可复现的真实结果分开——打分模型回答「在权重与打分成立时该选哪个」,真实结果拿到后反标定打分(把实际表现填回 config 的 scores,推荐项立刻反映现实)。
自检表
| 检查项 | 合格标准 |
|---|---|
| 选项齐全 | 每条决策至少两个候选,不预设结论 |
| 权重显式 | 每条准则的权重写死在 config,不藏在口头 |
| 并列确定 | 并列取表中靠前者,结果可复现 |
| 可查询 | 提供按 id 查询,能直接取某条决策 |
| 跨语言一致 | Python 与 TS 两套实现的摘要逐字符相等 |
| 边界清晰 | 真实出价与条款标 X,不写成「实测」 |
到这一节,卷四「钱怎么花、怎么收、怎么算」这条线已经走完。下一节进入收官:把这一卷散落的清单、模型与决策收成一份可执行的操盘手册。
阅读导航:上一节:15.2 投放、结算与资质的典型失败 · 下一节:16.1 装配成一份操盘手册 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。