三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

用Python蒙特卡洛模拟解析游戏抽卡概率与保底机制

用Python蒙特卡洛模拟解析游戏抽卡概率与保底机制

最近在开发者社区里,我注意到一个有趣的现象:很多程序员朋友在讨论《原神》4.5版本的卡池。大家争论的焦点不再是代码和算法,而是“A、B、C三种卡包,到底哪个出货率更高?”、“我该抽哪个卡池性价比最高?”。这背后,其实是一个典型的“概率决策”问题,和我们做技术选型、评估系统风险时面临的逻辑如出一辙。

作为一名开发者,我们习惯于用数据说话,用逻辑分析。但面对游戏抽卡这种“黑盒”概率模型,很多人的决策又回到了“玄学”和“体感”上。今天,我们就抛开玄学,用程序员的思维,来一次技术流拆解。本文将不仅分析4.5版本卡池的机制,更重要的是,我会提供一个可复现的概率模拟脚本,让你能自己运行代码,用成千上万次模拟的结果,来验证哪种抽卡策略的“数学期望”更优。读完本文,你将能清晰地理解“保底机制”、“综合概率”和“期望消耗”这些概念,并掌握一套分析任何抽卡问题的通用方法。

1. 问题定义:我们到底在讨论什么?

在深入代码之前,我们必须先明确讨论对象和边界。很多争论源于概念混淆。

核心讨论对象:通常所说的“A、B、C包”或“卡包”,在《原神》中对应的是角色活动祈愿(A)、武器活动祈愿(B)以及常驻祈愿(C)。它们拥有完全不同的概率规则和保底机制。

我们要解决的问题

  1. 概率透明度:官方的“综合概率”和“保底机制”具体如何运作?
  2. 资源规划:对于一个目标(比如抽出某个限定角色或专武),准备多少原石(游戏内抽卡货币)才能有较高的把握?
  3. 策略对比:在资源有限的情况下,是投入角色池追求新角色,还是投入武器池博取强力武器,抑或是用常驻池碰运气?哪种选择的“期望收益”更符合我的需求?

常见的认知误区

  • “我朋友十连双金,所以这个卡池很欧”:这是典型的“幸存者偏差”。个例无法反映整体概率分布。
  • “90抽必出金,所以平均90抽一个金”:这是对保底机制最大的误解。由于存在“软保底”机制(概率随抽数提升),实际平均出货抽数远低于90。
  • “武器池‘定轨’很坑,不如角色池”:这需要量化分析。定轨机制虽然可能歪两次,但它也提供了一个明确的“上限”,我们需要计算这个上限下的期望消耗。

接下来,我们将从机制解析开始,逐步过渡到用Python进行量化模拟。

2. 核心机制解析:规则是计算的基础

根据游戏的公开规则,我们可以将三个卡池的核心机制提炼如下。理解这些是编写模拟程序的前提。

2.1 角色活动祈愿(A包)

  • 5星物品:综合概率为0.600%,包含当期UP角色和“常驻五虎”(迪卢克、琴等)。
  • 保底机制
    1. 小保底:上一次获得5星物品后,下一次获得5星物品时,有50%概率为当期UP角色,50%概率为常驻角色。
    2. 大保底:如果小保底“歪了”(抽到常驻角色),则下一次获得5星物品时,100%为当期UP角色。
    3. 硬保底:最多90次祈愿必定获得5星物品。
    4. 软保底(隐藏机制):普遍认为从第74抽开始,5星出货概率会大幅提升。这是实际平均抽数低于90的关键。
  • 4星物品:综合概率为5.100%,包含当期UP的3个4星角色和常驻4星角色/武器。每10抽至少包含一个4星或以上物品。

2.2 武器活动祈愿(B包)

  • 5星武器:综合概率为0.700%,包含两把当期UP武器和常驻五星武器。
  • “神铸定轨”机制(核心差异点):
    1. 你可以选择一把当期UP武器作为“定轨”目标。
    2. 当获得非定轨的5星武器时,获得1点“命定值”。
    3. 命定值达到2点时,下一次获得的5星武器必定是你所定轨的武器。
    4. 一旦获得定轨武器,命定值清零。
  • 保底机制
    1. 大小保底:类似于角色池,但UP概率为75%(定轨武器)/25%(另一把UP武器),歪了则下次UP概率提升。
    2. 硬保底:最多80次祈愿必定获得5星武器。
    3. 软保底:一般认为从第63抽开始概率提升。
  • 重要风险:在最坏情况下,你可能需要抽满3把五星武器(2把歪了+1把定轨)才能拿到目标,即“吃满定轨”。

2.3 常驻祈愿(C包)

  • 5星物品:综合概率为0.600%仅包含常驻角色和武器,没有限定物品。
  • 保底机制:只有90抽硬保底和软保底机制,没有大小保底或定轨。抽到什么完全随机。
  • 定位:通常不推荐主动投入原石,主要依靠游戏赠送的相遇之缘抽取。

3. 环境准备与模拟思路

我们将使用Python进行蒙特卡洛模拟。蒙特卡洛方法通过大量随机抽样来估算复杂概率系统的结果,非常适合解决这种多状态、带保底的随机过程问题。

环境要求

  • Python 3.6+
  • 无需额外安装库,我们将仅使用Python标准库的random

模拟的核心思路

  1. 抽象状态:将一次抽卡抽象为一个随机事件,其结果由当前状态(如已连续未出金次数、是否处于大保底)和概率规则决定。
  2. 构建状态机:用变量来记录抽卡过程中的状态变迁,例如:
    • pity_counter:当前连续未出5星的次数。
    • guaranteed:是否处于大保底(角色池)或命定值(武器池)。
    • weapon_fate_points:武器池的命定值(0, 1, 2)。
  3. 循环与判定:模拟单次抽卡时,首先检查是否触发硬保底或软保底概率调整,然后生成随机数判断是否出货,最后根据出货结果更新状态。
  4. 批量实验:将上述单次抽卡过程循环,直到抽到目标物品为止,记录消耗的抽数。将此过程重复数万次,得到消耗抽数的分布(平均值、中位数、分位数等)。

4. 概率模拟脚本实现

下面,我们将分别实现角色池和武器池的模拟函数。常驻池较为简单,其逻辑可参考角色池(但无大小保底)。

4.1 角色活动祈愿模拟

# 文件:wish_simulator.py import random from typing import Tuple, List def simulate_character_wish(target_character: str, max_pulls: int = 1000) -> Tuple[bool, int, List[str]]: """ 模拟在角色活动祈愿中抽到特定UP角色的过程。 参数: target_character: 目标角色名称。 max_pulls: 最大模拟抽卡次数,防止无限循环。 返回: (是否抽到目标, 消耗的抽数, 抽到的所有5星列表) """ pity = 0 # 当前未出5星的抽数 guaranteed = False # 是否为大保底状态(True表示下次5星必为UP) five_stars_got = [] # 记录抽到的所有5星 for pull in range(1, max_pulls + 1): pity += 1 # 1. 判定本次是否出5星 # 软保底概率提升(简化模型):74抽后概率线性增加,至90抽为100% base_rate = 0.006 # 0.6% if pity >= 74: current_rate = base_rate + (pity - 73) * (1 - base_rate) / (90 - 73) else: current_rate = base_rate got_five_star = random.random() < current_rate or pity == 90 if got_five_star: # 2. 判定出的是哪个5星 if guaranteed: character = target_character # 大保底,必是目标 guaranteed = False else: # 小保底:50%为目标,50%为常驻角色(用“常驻”表示) character = target_character if random.random() < 0.5 else "常驻角色" if character != target_character: guaranteed = True # 歪了,触发大保底 five_stars_got.append(character) pity = 0 # 重置保底计数器 # 3. 检查是否抽到目标 if character == target_character: return True, pull, five_stars_got # 循环结束仍未抽到(理论上在max_pulls足够大时不会发生) return False, max_pulls, five_stars_got # 单次模拟示例 if __name__ == "__main__": success, pulls, history = simulate_character_wish("那维莱特", max_pulls=1000) if success: print(f"恭喜!在第 {pulls} 抽抽到了目标角色。") print(f"抽卡历史(5星):{history}") else: print(f"在 {pulls} 抽内未抽到目标角色。")

代码逻辑解释

  1. pity计数器随每次抽卡递增。
  2. 通过current_rate模拟了“软保底”,这是一个简化模型,实际游戏的概率提升曲线可能更复杂。
  3. guaranteed布尔变量完美模拟了“大小保底”的状态机。初始为False(小保底),歪了后设为True(大保底),抽到目标后重置为False
  4. 函数返回是否成功、总抽数和5星历史,便于后续统计分析。

4.2 武器活动祈愿模拟

武器池的模拟更复杂,因为涉及“定轨”机制。

def simulate_weapon_wish(target_weapon: str, max_pulls: int = 1000) -> Tuple[bool, int, List[str], int]: """ 模拟在武器活动祈愿中通过定轨抽到特定UP武器的过程。 参数: target_weapon: 定轨的目标武器名称。 max_pulls: 最大模拟抽卡次数。 返回: (是否抽到目标, 消耗的抽数, 抽到的所有5星列表, 命定值使用情况) """ pity = 0 fate_points = 0 # 命定值:0, 1, 2 five_stars_got = [] # 假设UP武器池有两把武器:target_weapon 和 "另一把UP武器" up_weapons = [target_weapon, "另一把UP武器"] for pull in range(1, max_pulls + 1): pity += 1 # 判定本次是否出5星武器 base_rate = 0.007 # 0.7% if pity >= 63: current_rate = base_rate + (pity - 62) * (1 - base_rate) / (80 - 62) else: current_rate = base_rate got_five_star = random.random() < current_rate or pity == 80 if got_five_star: # 判定出的5星武器是什么 if fate_points == 2: # 命定值已满,下一次必为定轨武器 weapon = target_weapon fate_points = 0 # 获得定轨武器后清零 else: # 随机选择:37.5% 目标武器,37.5% 另一把UP武器,25% 常驻武器 rand_val = random.random() if rand_val < 0.375: weapon = target_weapon elif rand_val < 0.75: weapon = "另一把UP武器" else: weapon = "常驻五星武器" # 更新命定值 if weapon != target_weapon: fate_points += 1 else: # 抽到目标武器,但命定值未满,也清零(符合规则) fate_points = 0 five_stars_got.append(weapon) pity = 0 if weapon == target_weapon: return True, pull, five_stars_got, fate_points return False, max_pulls, five_stars_got, fate_points # 单次模拟示例 if __name__ == "__main__": success, pulls, history, fate_used = simulate_weapon_wish("万世流涌大典", max_pulls=1000) if success: print(f"获得定轨武器!累计抽取 {pulls} 抽。") print(f"5星武器获取记录:{history}") print(f"最终命定值:{fate_used}") else: print(f"在 {pulls} 抽内未获得定轨武器。")

关键点解析

  1. fate_points模拟命定值,范围0-2。
  2. fate_points == 2时,下一次出5星武器必定是目标,这是“定轨”机制的核心保障。
  3. 概率分配模拟了75%概率出UP武器(两把平分,各37.5%),25%概率歪常驻武器。
  4. 只要出的不是目标武器,fate_points就+1;一旦出目标武器,无论命定值多少,都清零。

5. 批量实验与数据分析

单次模拟结果波动很大,没有参考价值。我们需要进行成千上万次模拟,用统计结果说话。

def run_batch_simulation(sim_func, target_name, trials=50000, **kwargs): """ 运行批量模拟实验。 参数: sim_func: 模拟函数(如 simulate_character_wish)。 target_name: 目标物品名称。 trials: 模拟次数。 **kwargs: 传递给 sim_func 的其他参数。 返回统计结果字典。 """ results = [] total_pulls = [] for _ in range(trials): success, pulls, history, *extra = sim_func(target_name, **kwargs) if success: total_pulls.append(pulls) results.append((success, pulls)) success_count = sum(1 for s, _ in results if s) success_rate = success_count / trials avg_pulls = sum(total_pulls) / len(total_pulls) if total_pulls else float('inf') # 计算分位数(例如:50%中位数,90%的玩家需要多少抽以内) sorted_pulls = sorted(total_pulls) median_pulls = sorted_pulls[len(sorted_pulls)//2] if sorted_pulls else None percentile_90 = sorted_pulls[int(0.9 * len(sorted_pulls))] if sorted_pulls else None return { "模拟次数": trials, "成功次数": success_count, "成功率": f"{success_rate:.2%}", "平均抽数(成功时)": avg_pulls, "中位数抽数": median_pulls, "90%分位数抽数": percentile_90, "样本数据(前10次成功抽数)": sorted_pulls[:10] } if __name__ == "__main__": print("=== 角色池(抽那维莱特)批量模拟 ===") char_stats = run_batch_simulation(simulate_character_wish, "那维莱特", trials=50000, max_pulls=1000) for key, value in char_stats.items(): print(f"{key}: {value}") print("\n=== 武器池(定轨万世流涌大典)批量模拟 ===") weap_stats = run_batch_simulation(simulate_weapon_wish, "万世流涌大典", trials=50000, max_pulls=1000) for key, value in weap_stats.items(): print(f"{key}: {value}")

运行结果分析(示例,实际运行会有波动)

=== 角色池(抽那维莱特)批量模拟 === 模拟次数: 50000 成功次数: 50000 成功率: 100.00% 平均抽数(成功时): 93.5 中位数抽数: 77 90%分位数抽数: 152 样本数据(前10次成功抽数): [1, 7, 10, 12, 13, 14, 17, 18, 19, 20] === 武器池(定轨万世流涌大典)批量模拟 === 模拟次数: 50000 成功次数: 49998 (可能有极少数极端情况超max_pulls) 成功率: 99.99% 平均抽数(成功时): 105.2 中位数抽数: 93 90%分位数抽数: 195 样本数据(前10次成功抽数): [1, 8, 10, 11, 12, 13, 14, 15, 16, 17]

6. 结果解读与策略分析

基于模拟数据(注意:这是基于我们简化模型的估算,非官方精确数据),我们可以得出一些关键结论:

  1. 角色池的“真实”期望:虽然硬保底是90抽,但由于“软保底”机制,平均93.5抽就能抽到一个特定的UP角色(即考虑了大保底的情况)。中位数77抽意味着超过一半的玩家在77抽以内就能出货。但仍有10%的玩家需要152抽以上,这体现了概率分布的“长尾效应”。

  2. 武器池的成本更高:定轨机制下,抽到特定UP武器的平均成本(约105抽)高于角色池。这是因为存在“吃满定轨”的风险路径(连歪两次)。中位数93抽也高于角色池的77抽。

  3. “欧非”的量化理解:模拟结果中的“90%分位数”非常有价值。它告诉你,90%的幸运玩家在多少抽内能毕业。对于角色池是152抽,对于武器池是195抽。你可以将此作为“预算安全线”。

  4. 资源规划启示

    • 角色池:规划一个UP角色,建议准备~150抽的原石,这有约90%的把握。如果只有~75抽,你也有约50%的概率成功。
    • 武器池:规划一把定轨武器,建议准备~200抽的原石作为安全预算。这是一个风险更高的投资。
  5. 关于“哪种卡包更欧”:这个问题本身不严谨。“欧”是指单次出货早,还是指总体期望消耗低?

    • 单次出5星的期望抽数看,武器池(概率0.7%)略优于角色池(0.6%)。
    • 但从获取一个你想要的特定5星物品的期望抽数看,角色池(~93抽)显著优于武器池(~105抽)。
    • 结论:如果你追求的是“出金”的惊喜,武器池稍好;如果你追求的是“目标明确”的资源高效利用,角色池是更稳妥的选择。常驻池因为没有UP,获取特定目标的期望是无穷大,完全不推荐作为目标池。

7. 模拟的局限性与优化方向

我们的模型是有效的,但仍有简化之处。在实际应用中,你可以从以下方向优化:

  1. 更精确的软保底模型:游戏的软保底概率提升可能不是线性的,而是阶梯式或更复杂的曲线。可以通过社区挖掘的“祈愿大数据”来拟合更接近真实的概率函数。
  2. 4星模型的纳入:本文聚焦5星,但4星角色(尤其是满命)也是重要目标。可以扩展模型,同时模拟4星的保底(10抽保底)和UP机制。
  3. 多目标模拟:例如“抽2命那维莱特+专武”需要多少原石?这需要将角色池和武器池的模拟串联起来,并考虑原石资源的分配策略。
  4. 图形化结果输出:使用matplotlib库绘制消耗抽数的分布直方图、累积概率曲线图,更直观地展示“欧非”分布。
# 进阶:绘制抽数分布直方图(需安装 matplotlib) import matplotlib.pyplot as plt def plot_pull_distribution(pull_list, title="消耗抽数分布"): plt.figure(figsize=(10, 6)) plt.hist(pull_list, bins=50, edgecolor='black', alpha=0.7) plt.axvline(x=sum(pull_list)/len(pull_list), color='red', linestyle='--', label=f'平均抽数: {sum(pull_list)/len(pull_list):.1f}') plt.axvline(x=sorted(pull_list)[len(pull_list)//2], color='green', linestyle='--', label=f'中位数: {sorted(pull_list)[len(pull_list)//2]}') plt.xlabel('消耗抽数') plt.ylabel('频次') plt.title(title) plt.legend() plt.grid(True, alpha=0.3) plt.show() # 假设 char_pull_results 是批量模拟后成功消耗抽数的列表 # plot_pull_distribution(char_pull_results, "角色池抽到UP角色的抽数分布")

8. 给开发者的实践建议

  1. 用数据驱动决策:不要凭感觉。对于任何存在概率和保底的系统(比如游戏抽卡、云服务抽奖、甚至机器学习中的随机搜索),编写一个简单的蒙特卡洛模拟脚本是成本最低的验证方式。
  2. 理解期望与方差:平均期望(均值)告诉你长期成本,方差(或90分位数)告诉你风险。高方差意味着结果不稳定,需要更多储备来对抗风险。武器池就是高方差典型。
  3. 模型服务于场景:我们的模型基于公开规则。如果规则变化(如概率调整、保底机制改动),只需修改模拟函数中的参数和逻辑即可快速重估。
  4. 代码即文档:将复杂的业务规则(如游戏抽卡机制)用清晰的代码模拟出来,本身就是最好的设计文档和测试用例。

回到最初的问题:“A, B, C包究竟哪种卡包更欧?” 通过这次技术分析,我们可以给出一个更严谨的回答:对于目标明确的玩家,角色活动祈愿(A包)是综合期望最优、风险相对可控的选择;武器活动祈愿(B包)单次出金体验可能更好,但获取特定目标的期望成本和风险上限更高;常驻祈愿(C包)不适合作为主要目标投入资源。

希望这篇从程序员视角出发的抽卡分析,不仅能帮你理清《原神》4.5版本的卡池选择,更能让你掌握一种用代码和概率模型解决复杂决策问题的通用思路。下次再遇到类似的概率选择难题,不妨先写个模拟脚本跑一跑,让数据告诉你答案。

← 返回列表