15.3 关键决策对照表

全卷关键决策收成一张对照表:每条决策给出候选选项与带整数权重的评价准则,用加权总分选出推荐项;配可跑的 Python 对拍脚本与可查询的 TypeScript 内核,把出价、放量、归因、聚合、变现、发行、资质、结算八条决策的「该选哪个」变成可复算、可查询的打分。

站内《游戏商业核心指标详解》 把 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

对照表的读法

八条决策的推荐项与加权总分是(同机相对量级,绝对数值不可移植):

决策推荐项加权总分
出价方式选择oCPM39
放量节奏阶梯爬坡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 装配成一份操盘手册 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

  1. 附录 F:全书公式、参数与数据集索引
  2. 9.3 同意管理与数据出境
  3. 9.2 COPPA 与儿童向游戏的限制