放置游戏数值模拟实战:用 Python 跑蒙特卡洛,比拍脑袋靠谱 10 倍
本文是「放置游戏策划实战」系列第 2 篇。上一篇讲了进阶路线总纲和"数值飞轮"的概念框架,这篇直接上手:用 Python 写一个可运行的数值模拟器,跑掉率、卡点、留存、敏感性四个实验,每个都有真实输出和可视化。
所有代码可以直接复制运行,参数换成你自己的项目数值就能立刻出结果。别再拍脑袋定数值了——跑一次实验,比你猜一百次都准。
目录
- 一、为什么独立开发者更需要数值模拟
- 二、模型假设与参数定义
- 三、实验一:掉率蒙特卡洛——脸黑玩家的体验下限
- 四、实验二:推图卡点分布——多数玩家卡在哪
- 五、实验三:留存曲线——推关节奏决定留存
- 六、实验四:数值敏感性——成长速度调一点会怎样
- 七、如何把这套方法接进你的项目
- 八、完整可运行脚本(一键复刻全部实验)
- 九、总结与下期预告
阅读本文需要:Python 基础(变量、函数、循环),了解
numpy基本概念。不需要数学背景——每个公式我会解释设计意图。运行环境:Python 3.8+,安装
numpy和matplotlib(pip install numpy matplotlib)。完整脚本在第八节,可直接复制运行。
一、为什么独立开发者更需要数值模拟
大厂有专职数值策划 + 数据分析师团队,可以拉几百万条真实埋点做 A/B 测试。独立开发者没有这些资源,但你有一样东西大厂没有的:当天改、当天发的灵活性。
数值模拟就是你的"穷人版 A/B 测试":
| 大厂做法 | 独立开发者替代方案 |
|---|---|
| 拉百万级埋点分析卡点 | 用 Python 模拟 3000 名虚拟玩家 |
| A/B 测试不同数值 | 跑蒙特卡洛对比多组参数 |
| 数据团队写 SQL 看 LTV | 自己写脚本看留存/卡点分布 |
核心逻辑就一句话:在发版本之前,先用代码算一遍"如果数值是这样,玩家大概会怎样"。
下面四个实验,覆盖了放置类游戏最常崩的四个环节。
二、模型假设与参数定义
先统一一套"极简放置游戏"模型。这套参数参考了《剑与远征》(AFK Arena)的公开数值分析——关卡公比、Boss 倍率、日成长率都贴近行业真实水平。你只需替换成自己的数值即可。
import numpy as np
rng = np.random.default_rng(20240711) # 固定种子,结果可复现
# ── 核心参数(参考剑与远征 / AFK Arena 数值体系)──
P0 = 1000 # 初始战力(AFK Arena 新号约 1000 战力)
LEVEL1 = 1000 # 第 1 关需求(新号起步能直接打)
R = 1.06 # 关卡公比(每关 +6%,每 10 关跨度 ≈1.79x)
BOSS_MULT = 1.40 # Boss 墙倍率(AFK Arena 约 1.3~1.5x)
M_BASE = 1.15 # 日乘区基准(每天战力成长 +15%)
M_SIGMA = 0.05 # 玩家间成长差异(不同在线时长 / 玩法风格)
DAYS = 7 # 模拟时长(天)
N_PLAYERS = 3000 # 模拟玩家数
两个关键函数:
def level_req(n):
"""第 n 关的战力需求(n 从 1 开始)
Boss 墙采用"台阶式"设计:
- 每 10 关一道 Boss,Boss 关 = 上一关 × BOSS_MULT
- 过了 Boss 后,后续关卡从 Boss 关需求继续等比增长,不会回跌
- 实现方式:基础等比 × BOSS_MULT^(已过Boss数)
"""
boss_count = n // 10 # 已经过的 Boss 数
base = LEVEL1 * (R ** (n - 1)) # 纯等比基础
return base * (BOSS_MULT ** boss_count) # 台阶式 Boss 加成
def player_power(m, day):
"""玩家在第 day 天的战力(指数成长 = 独立乘区乘法的结果)"""
return P0 * (m ** day) # 初始 × 日乘区^天数
参数来源说明:
- 关卡公比 R=1.06:AFK Arena 每章(约 5 关)战力需求增长约 20%,换算每关约 4%。我们取 6%(略高),让 7 天内有明显的卡点分化效果,方便观察。
- Boss 倍率 1.40:AFK Arena 的 Boss 关约为同章普通关的 1.3~1.5 倍。1.4 倍的 Boss 墙意味着玩家需要额外积累约 0.5~1 天的挂机收益才能突破——这恰好是 AFK Arena 设计的"12~24 小时等待窗口"。
- 日乘区 M=1.15:AFK Arena 活跃 F2P 玩家日均战力增长约 5~8%(纯挂机 3~5% + 主动玩法 2~3%)。我们取 15%,叠加了新手期福利(首抽、升级赠送等),前 7 天成长速度比长期更快。
- 台阶式 Boss:保证关卡需求单调递增,不会出现"过了 Boss 下一关反而更简单"的不自然曲线。
数据参考:Alexandre Macmillan, “Idle mechanics and monetizing progression in AFK Arena”; Reddit r/afkarena 社区数据汇总。
三、实验一:掉率蒙特卡洛——脸黑玩家的体验下限
问题
你的某个稀有装备掉落率设成了 1%。直觉上觉得"100 次差不多能掉一次"。但脸黑玩家的实际体验是什么?
方法
蒙特卡洛模拟:对每个玩家反复"掷骰子"直到第一次成功,记录需要多少次。这在概率论中叫几何分布(Geometric Distribution)——每次独立试验成功概率为 rate,记录"第几次才第一次成功"。
rates = [0.01, 0.05, 0.10] # 分别测试 1%、5%、10% 三种掉率
trials = 5000 # 每种掉率模拟 5000 名玩家
for rate in rates:
# 几何分布:直接生成"首次成功所需次数",一行替代手动循环
attempts = rng.geometric(p=rate, size=trials)
print(f"掉率 {rate*100:.0f}%:")
print(f" 平均 {attempts.mean():.1f} 次")
print(f" 50% 的玩家 ≤ {int(np.percentile(attempts, 50))} 次")
print(f" 10% 的脸黑玩家 ≥ {int(np.percentile(attempts, 90))} 次")
结果
| 掉率 | 平均所需次数 | 50% 玩家 ≤ | 10% 脸黑玩家 ≥ |
|---|---|---|---|
| 1% | ~100 次 | 69 次 | 225 次 |
| 5% | ~20 次 | 14 次 | 45 次 |
| 10% | ~10 次 | 7 次 | 23 次 |

关键发现
- 1% 掉率的陷阱:平均 100 次听起来还行,但 10% 的玩家要刷 225 次以上才第一次掉。如果每次打怪 30 秒,225 次就是 112 分钟纯战斗——这对休闲玩家来说已经接近弃游阈值了。
- 5% 是个分水岭:10% 脸黑玩家从 225 次降到 45 次,体验断崖式改善。
- 10% 以上基本安全:最黑的 10% 也只需 23 次,大多数人在 7 次以内就能拿到。
实操建议
如果你的稀有掉率 ≤ 3%,一定要加保底机制(如"累计 80 次未掉必出"),否则 10% 的玩家会因为"脸太黑"而流失——他们不是不玩你的游戏,是被随机性劝退了。
四、实验二:推图卡点分布——多数玩家卡在哪
问题
你的游戏上线 7 天了,TapTap 后台显示 DAU 在缓慢下降。你想知道:大多数玩家到底卡在哪一关?是不是某堵 Boss 墙把人拦住了?
方法
模拟 3000 名玩家 7 天的成长轨迹,每人每天根据自身战力计算"最高到达关卡"。
def sim_players(m_base=M_BASE, n=N_PLAYERS, days=DAYS, seed=20240711):
rng = np.random.default_rng(seed)
ms = np.clip(rng.normal(m_base, M_SIGMA, size=n), 1.03, 1.30)
max_level = 80
reqs = np.array([level_req(i) for i in range(1, max_level + 1)])
reached = np.zeros((n, days + 1), dtype=int)
for d in range(days + 1):
power = player_power(ms, d) # 当天战力
reached[:, d] = np.searchsorted(reqs, power,
side="right") # 战力够到第几关
return reached, ms
reached, ms = sim_players()
final = reached[:, DAYS] # 第 7 天各人的最高关
结果

| 统计指标 | 数值 |
|---|---|
| 中位数 | 第 12 关 |
| 卡在第 10 关(第一道 Boss 墙)前的比例 | 35.3% |
| 卡在第 20 关(第二道 Boss 墙)前的比例 | 99.7% |
| P90 到达关卡 | 第 18 关 |
| 最高到达 | 第 20 关 |
图表解读
这张直方图揭示了三个关键信号:
- 第 9~10 关出现第一个明显卡点——第一道 Boss 墙(
BOSS_MULT=1.40)拦住了 35% 的玩家。在真实游戏中,Boss 墙的"12~24 小时等待窗口"是刻意设计的心流断点——让玩家产生"差一点就过了"的动机,从而第二天再来。 - 第 11~12 关有小高峰——突破 Boss 后的释放感。过了第 10 关的玩家在 11~14 关快速推进,说明 Boss 后的难度梯度是合理的(没有回跌)。
- 第 20 关是 7 天内的极限——几乎所有玩家都停在了第二道 Boss 墙前。这符合真实节奏:AFK Arena 的 F2P 玩家前 7 天大约推到第 5~7 章(25~35 关),而我们的模型每 10 关对应约 2 章,12 关 ≈ Ch.2~3,考虑到模型更简洁所以偏保守。
实操建议
35% 的玩家卡在第一道 Boss 前 = 正常现象,但如果超过 50% 就需要调整。真实放置游戏会通过以下手段缓解:
- 策略深度:AFK Arena 允许通过阵营加成(+20%)和属性克制(+30%+)弥补战力差距,让"不是只能靠数值碾压"
- 过渡奖励:卡住期间仍然有挂机收益、日常任务等正向反馈
- 关卡回顾:回去刷已通关关卡获取资源,不会"无事可做"
- 飞升/转生:长期来看,引入重置机制让数值永远有成长空间
五、实验三:留存曲线——推关节奏决定留存
问题
玩家每天上线,如果连续几天都没推过新关,他还会继续玩吗?推关节奏如何影响留存? 这里我们模拟 30 天的留存曲线,并引入"自然流失"层——即使推了新关,也有部分玩家因为"下载了没养成习惯"而离开。这更贴近真实放置游戏的留存特征。
方法
基于上面的玩家模拟,给每位玩家加一个分层流失概率模型:
alive = np.ones(n, dtype=bool) # 是否仍在玩
no_adv_streak = np.zeros(n) # 连续未推关天数
retained = []
for d in range(1, 30 + 1): # 从第 1 天到第 30 天
advanced = reached[:, d] > reached[:, d - 1] # 当天是否推了新关
no_adv_streak = np.where(advanced, 0, no_adv_streak + 1)
# 分层流失模型:
# Day 1: 首日 35% 自然流失(下载了但没玩下去)
# Day 2-3: 过渡期,流失率逐渐降低
# Day 4+: 稳定期
# 推了新关 → 低流失;连续卡住 → 流失概率逐日上升
if d == 1:
base_churn, adv_churn = 0.35, 0.08
elif d == 2:
base_churn, adv_churn = 0.12, 0.03
elif d <= 7:
base_churn, adv_churn = 0.08, 0.02
else:
base_churn, adv_churn = 0.05, 0.01
churn_p = np.where(advanced, adv_churn,
np.minimum(base_churn + 0.025 * no_adv_streak, 0.85))
alive = alive & (rng.random(n) > churn_p)
retained.append(alive.mean())
结果

| 时间节点 | 模拟留存率(首开口径) | 行业基准(安装口径) |
|---|---|---|
| 第 1 天 | 91.6% | 35~40% |
| 第 7 天 | 70.0% | 15~20% |
| 第 14 天 | 55.8% | 10~15% |
| 第 30 天 | 33.5% | 5~10% |
行业基准来源:GameAnalytics 2024 Mobile Benchmarks, Maf.ad 2024 Retention Report
口径说明:模拟留存从"首次进入游戏"算起,行业基准从"安装"算起。安装→首开的转化率约 40~45%,所以模拟 D1 的 91.6% ≈ 安装口径 91.6%×42% ≈ 38.5%,正好在行业基准范围内。
关键发现
- 我们的模型留存偏高——这是因为模拟中假设所有玩家都是"成功下载并进入游戏"的,没有计入"下载后未打开"的漏斗损耗。真实行业中 D1 留存 35~40% 是从"安装"算起,而我们是从"首次游戏"算起。如果扣除安装→首开的约 55% 损耗,我们的 D1 ≈ 91.6% × 42% ≈ 38.5%,正好在行业基准范围内。
- D7 ≈ 70%(从首开算)或 ≈29%(从安装算)——接近行业中位数偏上。说明当前的推关节奏在 7 天内是健康的。
- D30 ≈ 33.5%(从首开算)或 ≈14%(从安装算)——略高于行业均值,说明模型中卡点带来的长期流失还不够严重。在真实游戏中,30 天后的卡点往往是多因素叠加的。
实操建议
留存分析的核心不是看绝对数字,而是看"留存断崖出现在哪里"。
- 如果你的 D1→D7 断崖特别大(比如 D1=40% 但 D7 只有 5%),说明新手引导或前 3 天的体验有严重问题
- 如果 D7→D30 的斜率很陡,说明中期卡点或内容消耗速度不对
- 放置类游戏的正确留存曲线应该是"前 3 天快速下降,之后趋平"——因为 Boss 墙的等待机制创造了每日登录习惯
六、实验四:数值敏感性——成长速度调一点会怎样
问题
你觉得当前成长速度可能偏慢,想把日乘区 M 从 1.15 提高到 1.18 或 1.20。但具体提高多少才能把卡点率降到可接受范围?
方法
系统性地改变 M_BASE,观察两个指标的变化:中位关卡数(玩家实际推到了哪)和 卡在第 20 关前的比例(第二道 Boss 墙的拦截面)。
m_list = [1.08, 1.10, 1.12, 1.14, 1.15, 1.16, 1.18, 1.20]
stuck_rate = []
for mb in m_list:
reached, _ = sim_players(m_base=mb, n=1500)
stuck_rate.append((reached[:, DAYS] < 20).mean() * 100)
结果

| 战力日乘区 M | 日增长率 | 7 天中位关卡 | 卡在第 20 关前 | 评估 |
|---|---|---|---|---|
| 1.08 | +8% | 第 9 关 | 100.0% | 几乎全员卡死 |
| 1.10 | +10% | 第 9 关 | 99.9% | 仍然严重 |
| 1.12 | +12% | 第 9 关 | 99.9% | 偏高 |
| 1.14 | +14% | 第 10 关 | 99.8% | 勉强 |
| 1.15 | +15% | 第 11 关 | 99.7% | 当前值 |
| 1.16 | +16% | 第 12 关 | 99.3% | 略有改善 |
| 1.18 | +18% | 第 14 关 | 98.7% | 明显改善 |
| 1.20 | +20% | 第 17 关 | 96.8% | 推荐 |
关键发现
- 中位关卡是更好的观察指标——从第 9 关到第 17 关(M 从 1.08 到 1.20),差距明显且直观。"卡在第20关前"这个指标在所有参数下都接近 100%,区分度不够,是因为 7 天内几乎不可能推到第二道 Boss 墙——这符合真实放置游戏的节奏设计。
- 关键观察不是"卡不卡",而是**“卡多久”**:M 从 1.15 提到 1.20,中位关卡从 11 涨到 17,意味着玩家平均多推 6 关——这在前期体感差异很大。
- 曲线在中低段(1.08~1.14)非常平坦,说明低于日增长 15% 时,调参几乎没有效果。真正的拐点在 M ≥ 1.16 之后。
实操建议
放置游戏的敏感性分析和线性游戏不同——不是追求"不卡关",而是控制"卡关时长"。
- 建议目标:玩家在每道 Boss 墙前平均等待 0.5~1.5 天(即 M 需要让玩家在 Boss 等待期间仍然有成长感)
- 不要盲目调高 M——M 过高会导致玩家"一睁眼就过了 Boss",反而失去每日登录动机
- 更好的策略是调 Boss 倍率而非 M:把
BOSS_MULT从 1.40 降到 1.25~1.30,卡点率会显著下降且不影响整体成长节奏
七、如何把这套方法接进你的项目
上面四个实验用的都是"通用参数",真正有价值的是把它接进你自己的项目:
Step 1:参数本地化
把你游戏的实际数值填进去:
# 示例:替换成你项目的真实数据
P0 = 你的初始战力
LEVEL1 = 第 1 关实际需求
R = 你实际的关卡公比(通常 0.03~0.08 之间)
BOSS_MULT = 你的 Boss 倍率(通常 1.2~1.5 之间)
M_BASE = 根据你后台数据估算的日均成长(通常 1.05~1.20)
M_SIGMA = 根据玩家在线时长分布估算的差异(通常 0.02~0.08)
Step 2:接入 TapTap 埋点验证
模拟只能预测趋势,真实数据才是金标准:
import pandas as pd
import numpy as np
# 伪代码:用 TapTap 后台导出的 CSV 做对比
real_data = pd.read_csv("taptap_level_distribution.csv")
simulated_final = final # 来自实验二
# 对比模拟 vs 真实的关卡分布
print("模拟中位数:", np.median(simulated_final))
print("真实中位数:", real_data["max_level"].median())
如果模拟分布和真实分布形状一致但位置偏移,说明模型结构是对的,只需微调参数。如果形状都不像,说明你的飞轮模型本身有问题,需要回到 P1 重审循环结构。
Step 3:版本迭代闭环
定数值假设 → 跑模拟 → 发版本 → 拉 TapTap 埋点 → 对比 → 修正参数 → 下一轮
这就是上一篇说的"每一个版本都是一次数值实验"。半年下来,你的数值敏感度会比任何培训班出来的都强。
八、完整可运行脚本(一键复刻全部实验)
把以下代码保存为 idle_sim.py,直接 python idle_sim.py 即可生成全部四张图:
# -*- coding: utf-8 -*-
"""放置游戏数值模拟器 —— 一键跑通 4 个实验
参数参考剑与远征(AFK Arena)公开数值分析
"""
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib import font_manager as fm
import os, platform
# ── 字体配置(自动适配系统)──
_sys = platform.system()
if _sys == "Darwin":
FONT = "/System/Library/Fonts/STHeiti Medium.ttc"
elif _sys == "Windows":
FONT = "C:/Windows/Fonts/msyh.ttc"
else:
FONT = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
fm.fontManager.addfont(FONT)
plt.rcParams["font.family"] = fm.FontProperties(fname=FONT).get_name()
plt.rcParams["axes.unicode_minus"] = False
OUT = "./output"
os.makedirs(OUT, exist_ok=True)
SEED = 20240711
# ═══════════════════════ 参数区(改成你的数值)═══════════════════════
# 参考剑与远征(AFK Arena)数值体系
P0 = 1000 # 初始战力
LEVEL1 = 1000 # 第 1 关需求
R = 1.06 # 关卡公比(每关 +6%)
BOSS_MULT = 1.40 # Boss 墙倍率(台阶式,单调递增)
M_BASE = 1.15 # 日乘区(每天 +15%)
M_SIGMA = 0.05 # 玩家间差异
DAYS = 7 # 模拟天数
N_PLAYERS = 3000 # 模拟人数
# ══════════════════════════════════════════════════════════════════════
def level_req(n):
"""台阶式 Boss:基础等比 × BOSS_MULT^(已过Boss数),单调递增"""
boss_count = n // 10
base = LEVEL1 * (R ** (n - 1))
return base * (BOSS_MULT ** boss_count)
def player_power(m, day):
return P0 * (m ** day)
def sim_players(m_base=M_BASE, n=N_PLAYERS, days=DAYS, seed=SEED):
rng = np.random.default_rng(seed)
ms = np.clip(rng.normal(m_base, M_SIGMA, size=n), 1.03, 1.30)
reqs = np.array([level_req(i) for i in range(1, 81)])
reached = np.zeros((n, days + 1), dtype=int)
for d in range(days + 1):
reached[:, d] = np.searchsorted(reqs, player_power(ms, d),
side="right")
return reached, ms
# ── 实验 1:掉率蒙特卡洛 ──
def exp_drop():
rng = np.random.default_rng(SEED)
rates = [0.01, 0.05, 0.10]
fig, ax = plt.subplots(figsize=(8, 4.2), dpi=110)
for rate, c in zip(rates, ["#DC2626", "#F59E0B", "#16A34A"]):
trials = 5000
attempts = rng.geometric(p=rate, size=trials)
ax.hist(attempts, bins=60, density=True, alpha=0.55, color=c,
label=f"掉率{rate*100:.0f}%(期望{1/rate:.0f}次)",
range=(0, 160))
ax.axvline(1/rate, color=c, ls="--", lw=1.8)
ax.set_xlabel("首次掉落所需次数"); ax.set_ylabel("玩家占比")
ax.set_title("掉率蒙特卡洛:首次掉落次数的分布", fontsize=13, weight="bold")
ax.legend(fontsize=9); fig.tight_layout()
fig.savefig(f"{OUT}/01_掉率.png", dpi=110); plt.close(fig)
# ── 实验 2:推图卡点 ──
def exp_progression():
reached, _ = sim_players()
final = reached[:, DAYS]
fig, ax = plt.subplots(figsize=(8, 4.2), dpi=110)
bins = np.arange(0, int(final.max()) + 2) - 0.5
ax.hist(final, bins=bins, color="#4F46E5", alpha=0.85, edgecolor="white")
med = int(np.median(final))
ax.axvline(med, color="#F59E0B", ls="--", lw=2,
label=f"中位数 第{med}关")
for b in [10, 20, 30]:
if b <= final.max():
ax.axvline(b, color="#DC2626", ls=":", lw=1.3, alpha=0.6,
label=f"Boss 第{b}关" if b == 10 else "")
ax.set_xlabel("7天内到达的最高关卡"); ax.set_ylabel("玩家数")
ax.set_title("推图进度分布:多数玩家卡在哪?", fontsize=13, weight="bold")
ax.legend(fontsize=9); fig.tight_layout()
fig.savefig(f"{OUT}/02_卡点.png", dpi=110); plt.close(fig)
# ── 实验 3:留存曲线 ──
def exp_retention():
rng = np.random.default_rng(SEED)
reached, ms = sim_players(days=30, seed=SEED)
n = reached.shape[0]; D = 30
alive = np.ones(n, dtype=bool)
streak = np.zeros(n); ret = []
for d in range(1, D + 1):
adv = reached[:, d] > reached[:, d - 1]
streak = np.where(adv, 0, streak + 1)
# 分层流失:首日高流失,后续稳定
if d == 1:
base_churn, adv_churn = 0.35, 0.08
elif d == 2:
base_churn, adv_churn = 0.12, 0.03
elif d <= 7:
base_churn, adv_churn = 0.08, 0.02
else:
base_churn, adv_churn = 0.05, 0.01
cp = np.where(adv, adv_churn,
np.minimum(base_churn + 0.025 * streak, 0.85))
alive &= rng.random(n) > cp; ret.append(alive.mean())
fig, ax = plt.subplots(figsize=(8, 4.2), dpi=110)
ax.plot(range(1, D + 1), np.array(ret) * 100, color="#06B6D4",
lw=2.5, marker='o', ms=3)
# 行业基准线
for day_idx, benchmark, label in [(0, 37, "D1 基准 37%"),
(6, 17.5, "D7 基准 17.5%"),
(29, 7.5, "D30 基准 7.5%")]:
ax.axhline(benchmark, color="#DC2626", ls=":", lw=1, alpha=0.5)
ax.text(D + 0.5, benchmark, label, fontsize=7, color="#DC2626",
va="center")
ax.set_xlabel("天数"); ax.set_ylabel("留存率(%)")
ax.set_title("模拟留存曲线:推关节奏决定留存(含行业基准)",
fontsize=13, weight="bold")
ax.grid(alpha=.3); fig.tight_layout()
fig.savefig(f"{OUT}/03_留存.png", dpi=110); plt.close(fig)
# ── 实验 4:敏感性 ──
def exp_sensitivity():
ml = [1.08, 1.10, 1.12, 1.14, 1.15, 1.16, 1.18, 1.20]
sr_20, sr_med = [], []
for mb in ml:
r, _ = sim_players(m_base=mb, n=1500, days=7)
sr_20.append((r[:, 7] < 20).mean() * 100)
sr_med.append(int(np.median(r[:, 7])))
fig, ax = plt.subplots(figsize=(8, 4.2), dpi=110)
ax.plot(ml, sr_20, color="#4F46E5", lw=2.5, marker='s', ms=6,
label="卡<第20关比例")
ax2 = ax.twinx()
ax2.plot(ml, sr_med, color="#16A34A", lw=2.5, marker='o', ms=6,
label="中位关卡数")
ax.axvline(1.15, color="#F59E0B", ls="--", lw=1.5, alpha=0.7)
ax.set_xlabel("战力日乘区 M")
ax.set_ylabel("7天卡在第20关前的比例(%)", color="#4F46E5")
ax2.set_ylabel("7天中位关卡数", color="#16A34A")
ax.set_title("数值敏感性:成长速度如何影响推图进度",
fontsize=13, weight="bold")
ax.legend(loc="upper left", fontsize=9)
ax2.legend(loc="upper right", fontsize=9)
ax.grid(alpha=.3); fig.tight_layout()
fig.savefig(f"{OUT}/04_敏感性.png", dpi=110); plt.close(fig)
if __name__ == "__main__":
exp_drop(); exp_progression(); exp_retention(); exp_sensitivity()
print("✅ 全部图表已保存至", OUT)
print(os.listdir(OUT))
九、总结与下期预告
今天我们用 Python 跑了四个实验,覆盖放置类游戏最容易崩的四个环节:
| 实验 | 解决的核心问题 | 一句话结论 |
|---|---|---|
| 掉率蒙特卡洛 | 掉率设多少合适 | 1% 掉率必须加保底,否则 10% 脸黑玩家刷 200+ 次才出 |
| 推图卡点分布 | 多数玩家卡在哪 | 35% 卡在第一道 Boss 前 = 正常;超过 50% 就需要调整 |
| 留存曲线 | 留存是否健康 | 留存分析的核心不是绝对数字,而是"断崖出现在哪里" |
| 敏感性分析 | 成长速度调多少 | 放置游戏不是追求不卡关,而是控制"卡关时长" |
核心方法论:别拍脑袋,跑模拟。3000 行虚拟玩家 + 4 张图 = 一个版本的数值体检报告。
下一篇 我会深拆「独立乘区」——为什么战力公式必须是乘法而不是加法,以及如何在 Excel 里搭一张投放回收表来控制膨胀。如果你有自己的项目数值想代入跑一下,欢迎在评论区贴出来,我帮你看看哪里该调。
更多推荐


所有评论(0)