4.1 定价锚点与档位设计

定价不是拍脑袋定几个数字,而是一组相互参照的锚点:谁引流、谁走量、谁拉高感知。本节把档位的价值密度、支配关系与选择份额写成可复算的整数模型,用本机实跑的数字说明「诱饵档」如何把份额推向主力档,以及为什么最贵的档位往往是价格锚点而不是走量款。

本节目标:把「档位怎么定、谁当锚点」从拍脑袋变成一组可复算的整数——价值密度、支配关系与选择份额。

站内有一篇 广告变现设计清单式短文 ,讲的是广告位怎么摆、变现点怎么列——那是一份可以直接照抄的清单。本章换一个方向:不列清单,而是搭一套可跑的价格弹性与折扣模型——档位之间怎么相互参照、折扣该打多深、价格弹性多大、副作用会吃掉多少收入,全部用本机实跑的确定数字说话。本节先解决第一件事:定价的锚点与档位。

第 2 章讲了内购点的结构(一次买什么、分成几档),第 3 章讲了 LTV 与回本。本节不重复这两块,只回答一个更前置的问题:同一批内购内容,价格标签怎么排——哪一档负责引流、哪一档负责走量、哪一档负责把整组价格的感知拉高。

4.1 定价锚点与档位设计

4.1.1 价格是相对的,不是绝对的

没有人能凭空判断「3000 分(30 元)买 100 颗钻石」是贵还是便宜。人判断价格靠的是参照:旁边那一档多少钱、能买多少东西。定价设计里最反直觉的一条经验就来自这里——一个档位的价值,往往由它旁边那个档位决定。

三个角色因此分工不同:

  • 引流档:价格最低,门槛低到几乎不用想。它的任务不是赚钱,是让用户完成「第一次付费」这个动作。
  • 走量档:整组里性价比最高的那一档,是你希望大多数人选的那一档,也是真正的收入主力。
  • 锚点档:价格最高、单位价值未必最高。它存在的意义是把整组价格的感知拉高——当 6800 分那一档摆在那里,3000 分那一档看起来就没那么贵了。

再加一个诱饵档(decoy),就是经典的「诱饵效应」:它的内容比走量档差、价格却差不多,几乎没人会选它,但它的存在会让走量档显得划算。本节要做的,就是把「显得划算」这件事量化。

4.1.2 D 层:档位与选择模型配置

下面这份是 D 层配置:四个档位(引流 / 走量 / 诱饵 / 锚点)的价格与内容价值单位,加上一个离散选择模型的参数。金额一律以「分」记账,价值单位用「钻石」这样的等价物表示——它把不同档位的「内容多少」压到一个可比较的刻度上。

{
  "version": 1,
  "game": "merge-island",
  "note": "定价档位设计口径(D 层)。档位价格、内容价值单位与选择模型参数均为示例假设;真实平台的档位分布与转化需真实业务环境,命令行不可复现(X)。金额一律以「分」记账,比例一律整数百分比 / 千分比,无浮点。",
  "currency": "fen",
  "valueUnit": "diamond",
  "scenarios": {
    "base": ["entry", "main", "top"],
    "decoy": ["entry", "main", "decoy", "top"]
  },
  "tiers": [
    {"id": "entry", "label": "体验包", "priceFen": 600, "valueUnits": 15, "role": "entry"},
    {"id": "main", "label": "主力包", "priceFen": 3000, "valueUnits": 100, "role": "main"},
    {"id": "decoy", "label": "诱饵包", "priceFen": 3000, "valueUnits": 55, "role": "decoy"},
    {"id": "top", "label": "豪华包", "priceFen": 6800, "valueUnits": 180, "role": "top"}
  ],
  "choice": {
    "priceSensitivity": 3,
    "dominateBonusPct": 25
  }
}

两个情景共用同一批档位:base 只有引流 / 走量 / 锚点三档,decoy 多塞了一个诱饵档。priceSensitivity 是选择模型的价格敏感度指数,dominateBonusPct 是「支配加成」——被严格支配的诱饵档,会把份额推给支配它的那一档。

4.1.3 S 层:价值密度、支配与份额

下面这段是 S 层仿真脚本。它先算每个档位的价值密度(每元能买到的价值单位),再统计每个档位严格支配了几个档位(价值不低于、价格不高于,且至少一项严格),把支配关系折成效用加成,最后用「效用^k 归一化」做离散选择,输出两情景下的份额千分比。全程整数,幂运算用 Python 原生大整数。

#!/usr/bin/env python3
# file: sim/l04_tiers.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/l04_tiers.py --config config/ --out out/
"""定价档位设计:价值密度、锚点位置与诱饵效应(整数、可复算)。

读 config/l04_tiers.json:档位清单 + 选择模型参数 + 两个情景(基础 / 加入诱饵)。
1) 每档价值密度 density = valueUnits * 1000000 // priceFen(每元价值,越大越划算);
2) 严格支配计数:价值不低于、价格不高于,且至少一项严格的档位数;
3) 效用 utility = density * (100 + dominateBonusPct * dominates) // 100;
4) 离散选择 share_i = utility_i^k / Σ utility_j^k(整数幂,千分比,补偿到 1000);
5) 对比基础集与加入诱饵档后主力档份额的变化——诱饵效应。
全程整数。产出 out/l04_tiers.json + out/l04_tiers.csv。
"""

from __future__ import annotations

import argparse
import csv
import json
import os
import sys


def load(cfg_dir, name):
    with open(os.path.join(cfg_dir, f"{name}.json"), encoding="utf-8") as f:
        return json.load(f)


def density(t):
    return t["valueUnits"] * 1000000 // t["priceFen"]


def dominates(a, b):
    """a 严格支配 b:价值不低于、价格不高于,且至少一项严格。"""
    return (a["valueUnits"] >= b["valueUnits"] and a["priceFen"] <= b["priceFen"]
            and (a["valueUnits"] > b["valueUnits"] or a["priceFen"] < b["priceFen"]))


def dominate_count(t, tiers):
    return sum(1 for o in tiers if o["id"] != t["id"] and dominates(t, o))


def utility(t, tiers, choice):
    """效用 = 价值密度 ×(1 + 支配加成)。支配加成把被支配诱饵的份额拉向支配者。"""
    return density(t) * (100 + choice["dominateBonusPct"] * dominate_count(t, tiers)) // 100


def shares_permille(tiers, choice):
    """share_i ∝ utility_i^k。整数幂 + 地板除,最大项补偿使总和 = 1000。"""
    utils = [utility(t, tiers, choice) for t in tiers]
    powers = [u ** choice["priceSensitivity"] for u in utils]
    total = sum(powers)
    raw = [p * 1000 // total for p in powers]
    raw[raw.index(max(raw))] += 1000 - sum(raw)
    return utils, raw


def anchor_permille(t, tiers):
    """锚点比:该档价格相对最贵档(价格锚点)的千分比。"""
    return t["priceFen"] * 1000 // max(x["priceFen"] for x in tiers)


def pack(tiers, utils, shares):
    return [{"id": t["id"], "label": t["label"], "role": t["role"], "priceFen": t["priceFen"],
             "valueUnits": t["valueUnits"], "density": density(t),
             "dominates": dominate_count(t, tiers), "anchorPermille": anchor_permille(t, tiers),
             "utility": u, "sharePermille": s} for t, u, s in zip(tiers, utils, shares)]


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", default="config/")
    ap.add_argument("--out", default="out/")
    args = ap.parse_args()

    cfg = load(args.config, "l04_tiers")
    by_id = {t["id"]: t for t in cfg["tiers"]}
    choice = cfg["choice"]
    base = [by_id[i] for i in cfg["scenarios"]["base"]]
    decoy = [by_id[i] for i in cfg["scenarios"]["decoy"]]
    os.makedirs(args.out, exist_ok=True)

    base_rows = pack(base, *shares_permille(base, choice))
    decoy_rows = pack(decoy, *shares_permille(decoy, choice))
    # 纯价值密度对照(关掉支配加成):证明诱饵效应来自支配加成,而非「多了一个低分档」
    plain = {"priceSensitivity": choice["priceSensitivity"], "dominateBonusPct": 0}
    plain_rows = pack(decoy, *shares_permille(decoy, plain))

    share_of = lambda rows, tid: next(r["sharePermille"] for r in rows if r["id"] == tid)
    main_base, main_decoy, main_plain = (share_of(base_rows, "main"), share_of(decoy_rows, "main"),
                                         share_of(plain_rows, "main"))
    best_value = max(base, key=density)["id"]

    with open(os.path.join(args.out, "l04_tiers.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["scenario", "id", "role", "priceFen", "valueUnits", "density",
                    "dominates", "utility", "sharePermille"])
        for label, rows in (("base", base_rows), ("decoy", decoy_rows)):
            for r in rows:
                w.writerow([label, r["id"], r["role"], r["priceFen"], r["valueUnits"],
                            r["density"], r["dominates"], r["utility"], r["sharePermille"]])

    report = {"source": "sim/l04_tiers.py", "tier": "S", "game": cfg["game"], "note": cfg["note"],
              "choice": choice, "anchorPriceFen": max(t["priceFen"] for t in cfg["tiers"]),
              "bestValueTier": best_value, "baseRows": base_rows, "decoyRows": decoy_rows,
              "mainSharePermille": {"base": main_base, "decoy": main_decoy,
                                    "decoyPlainDensity": main_plain},
              "decoyEffectPermille": main_decoy - main_base,
              "csv": os.path.join(args.out, "l04_tiers.csv")}
    with open(os.path.join(args.out, "l04_tiers.json"), "w", encoding="utf-8") as f:
        json.dump(report, f, ensure_ascii=False, indent=2)

    for label, rows in (("base", base_rows), ("decoy", decoy_rows)):
        for r in rows:
            print(f"{label:>5} {r['id']:>6} {r['priceFen']:>5}分/{r['valueUnits']:>3}u "
                  f"density={r['density']:>6} dom={r['dominates']} share={r['sharePermille']}permille")
    print(f"bestValue={best_value} anchorPrice={report['anchorPriceFen']}fen")
    print(f"main share: base={main_base}permille decoy={main_decoy}permille "
          f"(plainDensity={main_plain}) effect={main_decoy - main_base}permille")
    return 0


if __name__ == "__main__":
    sys.exit(main())

本机实跑输出:

 base  entry   600分/ 15u density= 25000 dom=0 share=219permille
 base   main  3000分/100u density= 33333 dom=0 share=521permille
 base    top  6800分/180u density= 26470 dom=0 share=260permille
decoy  entry   600分/ 15u density= 25000 dom=0 share=138permille
decoy   main  3000分/100u density= 33333 dom=1 share=644permille
decoy  decoy  3000分/ 55u density= 18333 dom=0 share=54permille
decoy    top  6800分/180u density= 26470 dom=0 share=164permille
bestValue=main anchorPrice=6800fen
main share: base=521permille decoy=644permille (plainDensity=481) effect=123permille

4.1.4 逐条读输出

  • 价值密度最高的是主力包(33333),不是最贵的豪华包(26470)。每元买到的钻石,3000 分那一档最多。这符合设计意图:主力包就是「走量档」,性价比要最好。豪华包密度更低,它不靠性价比取胜,靠的是价格锚点作用——把整组价格的感知拉高。
  • 基础三档的份额是 219 / 521 / 260(体验 / 主力 / 豪华)。主力包已经拿到一半以上份额,但体验档还占了两成——这是「门槛低」的自然结果。
  • 加入诱饵档后,主力包份额从 521‰ 升到 644‰(effect=123permille)。诱饵包自己只拿到 54‰,几乎没人选,但它把份额从体验档(219→138)和豪华包(260→164)那里抽了出来,几乎全部转给了主力包。这就是诱饵效应。
  • plainDensity=481 是关键对照。这一列是把「支配加成」关掉(dominateBonusPct=0)后,诱饵情景下主力包的份额——只有 481‰,比不加诱饵的 521‰ 还低。也就是说,如果只看价值密度,多塞一个低分档只会稀释主力份额;诱饵效应完全来自「支配关系」这一项。

要强调:这些份额是同机相对量级,绝对数值不可移植——换一组档位、换一个敏感度指数,份额都会变。可移植的是这套流程:档位与模型参数是输入,份额是输出,改一个参数就能立刻复算。

4.1.5 P 层:同一模型的 TS 内核

同一套模型在 core/ 里有一份 TypeScript 内核,供后续的网页原型与宿主复用。它的关键是幂运算用 BigInt——33333^3 已经超过 2^53,用 Number 会丢精度;除法一律整数地板除,末位补偿使份额总和恰好 1000。

// file: core/src/l04/tiers.ts
// tier: P
// verify: node --test 'core/test/l04_*.test.ts'
// 《合成小岛》定价档位内核:价值密度 + 支配计数 + 离散选择份额。
// 全整数;幂运算用 BigInt(density^3 会超 2^53);禁 enum / namespace / Math.random。

export interface Tier {
  readonly id: string;
  readonly label: string;
  readonly priceFen: number;
  readonly valueUnits: number;
  readonly role: "entry" | "main" | "decoy" | "top";
}
export interface ChoiceModel {
  readonly priceSensitivity: number;
  readonly dominateBonusPct: number;
}
export interface TierScore {
  readonly id: string;
  readonly density: number;
  readonly dominates: number;
  readonly utility: number;
  readonly sharePermille: number;
}

/** 价值密度:每元买到的价值单位 ×1000000(整数)。越大越划算。 */
export function valueDensity(valueUnits: number, priceFen: number): number {
  return Math.floor((valueUnits * 1000000) / priceFen);
}

/** a 是否严格支配 b:价值不低于、价格不高于,且至少一项严格。 */
export function dominates(a: Tier, b: Tier): boolean {
  const geValue = a.valueUnits >= b.valueUnits;
  const lePrice = a.priceFen <= b.priceFen;
  const strict = a.valueUnits > b.valueUnits || a.priceFen < b.priceFen;
  return geValue && lePrice && strict;
}

/** 该档严格支配的档位数(不含自身)。 */
export function dominateCount(tier: Tier, tiers: Tier[]): number {
  let n = 0;
  for (const other of tiers) if (other.id !== tier.id && dominates(tier, other)) n += 1;
  return n;
}

/** 效用 = 价值密度 ×(1 + 支配加成)。支配加成把「被支配的诱饵」的份额拉向支配者。 */
export function utility(tier: Tier, tiers: Tier[], choice: ChoiceModel): number {
  const dom = dominateCount(tier, tiers);
  return Math.floor((valueDensity(tier.valueUnits, tier.priceFen) *
    (100 + choice.dominateBonusPct * dom)) / 100);
}

/** 离散选择:share_i ∝ utility_i^k。整数幂用 BigInt,末位补偿使总和 = 1000。 */
export function sharesPermille(tiers: Tier[], choice: ChoiceModel): TierScore[] {
  const utils = tiers.map((t) => utility(t, tiers, choice));
  const k = BigInt(choice.priceSensitivity);
  const powers = utils.map((u) => BigInt(u) ** k);
  const total = powers.reduce((a, b) => a + b, 0n);
  const raw = powers.map((p) => Number((p * 1000n) / total));
  let hi = 0;
  for (let i = 1; i < raw.length; i += 1) if (raw[i] > raw[hi]) hi = i;
  raw[hi] += 1000 - raw.reduce((a, b) => a + b, 0);
  return tiers.map((t, i) => ({
    id: t.id,
    density: valueDensity(t.valueUnits, t.priceFen),
    dominates: dominateCount(t, tiers),
    utility: utils[i],
    sharePermille: raw[i],
  }));
}

/** 价值密度最高的档位 id(并列时取价格更低者)。 */
export function bestValueTier(tiers: Tier[]): string {
  let best = tiers[0];
  for (const t of tiers) {
    const d = valueDensity(t.valueUnits, t.priceFen);
    const bd = valueDensity(best.valueUnits, best.priceFen);
    if (d > bd || (d === bd && t.priceFen < best.priceFen)) best = t;
  }
  return best.id;
}

配套单测 core/test/l04_tiers.test.ts 用 node --test 'core/test/l04_*.test.ts' 跑,逐条断言「密度整数、支配关系单向、份额总和 = 1000、诱饵使主力份额上升且不靠降低密度、敏感度越高越集中」。P 层的输出与 S 层逐位一致(主力份额 521‰ / 644‰ / 481‰ 三个数字两边相同),这正是「一份配置、跨语言一致」的验收口径。

4.1.6 三条可复用判断

  1. 先定走量档,再定锚点。先想清楚「你希望大多数人选哪一档」,把它的价值密度做成整组最高;最贵那一档负责拉高感知,密度可以低一点。
  2. 诱饵靠的是支配,不是低价。诱饵档必须被走量档严格支配(同价更低价值,或同价值更高价格)才有效;一个单纯「更便宜的档」只会稀释份额。
  3. 把模型参数写进配置。priceSensitivity 与 dominateBonusPct 是假设,不是真理——它们是输入,改了就能立刻复算份额,别把它们硬编码进代码。

小结

  • 价格是相对的:一个档位的价值由它旁边的档位决定;引流 / 走量 / 锚点 / 诱饵四类角色分工不同。
  • D 层配置把档位价格、价值单位与选择模型写成唯一真源;S 层算价值密度、支配关系与份额。
  • 本机实跑:主力包价值密度最高(33333);加入诱饵档后主力份额从 521‰ 升到 644‰,而关掉支配加成反而降到 481‰——诱饵效应来自支配关系。
  • P 层 TS 内核与 S 层逐位一致,幂运算用 BigInt;份额是同机相对量级,绝对数值不可移植。

档位和锚点定下来,下一个问题是卖的时候打不打折:折扣该打多深、限时和稀缺怎么叠加。这是下一节的事。

阅读导航:上一节:目录 · 下一节:折扣、限时与稀缺 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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