Python实战——蒙特卡洛模拟分析杀牌游戏技能收益

事情是这样的,我做了一堆三国杀的将牌,但是O神。

先感谢这个造将平台:sgs-shap网页版

(以5血3技能为常规标准来评判)对芙芙的评价非常不一。

这个角色确实参考了蒜香,同时借鉴了芙芙喜欢表演的特性,装备就相当于舞台道具喽。


模型假设

最核心的点在于芙芙开大收益不高,而且开完大还扣体力上限,而且本身几乎是纯爆发将。

那么我们就来1W次蒙特卡洛模拟一遍,芙芙开大过牌的期望值是多少?过装备的期望值是多少?

当然毫无疑问有一个假设前提:牌堆为随机过牌,那么从 160 张牌里抽 10 张,和从 80 张牌里抽 10 张,每一张牌抽到"杀"的概率初始值是基本稳定的。

当然,你的1技能是不正常的摸牌,每次都会把装备抽出来,25/160比例远小于1/3,所以当牌堆被摸掉一些后,开大摸装备的概率实际上会变低。而如果有其他角色能够更改牌堆的,这个概率照样可能变化。

不过不考虑这些了,直接开始写模拟吧。


艾斯比式手写代码教程

蒙特卡洛模拟

1.首先先把牌和装备牌列出来:

python 复制代码
# 1. 定义军争160张牌堆 (简化版分布)
# 键为牌名,值为该牌名在牌堆中的数量
DECK_COMPOSITION = {
    # 基本牌 85 张
    '杀': 30, '雷杀': 9, "火杀": 5, '闪': 24, '桃': 12, '酒': 5,
    # 锦囊牌 50 张
    '过河拆桥': 6, '顺手牵羊': 5, '无中生有': 4, '无懈可击': 7, "借刀杀人": 2, '决斗': 3,
    '南蛮入侵': 3, '万箭齐发': 1, '五谷丰登': 2, '桃园结义': 1, '火攻': 3, '铁索连环': 6,
    '兵粮寸断': 2, '乐不思蜀': 3, '闪电': 2,
    # 装备牌 25 张
    '诸葛连弩': 2, '雌雄双股剑': 1, '寒冰剑': 1, '青釭剑': 1, '古锭刀': 1,
    '青龙偃月刀': 1, '丈八蛇矛': 1, '贯石斧': 1, '方天画戟': 1, '麒麟弓': 1, '朱雀羽扇': 1,
    '八卦阵': 2, '仁王盾': 1, '藤甲': 2, '白银狮子': 1,
    '赤兔': 1, '大宛': 1, '紫骍': 1, '的卢': 1, '绝影': 1, '爪黄飞电': 1, '骅骝': 1
}

# 标记哪些是装备牌,用于统计另一个技能的收益
EQUIPMENT_NAMES = [
    '诸葛连弩', '雌雄双股剑', '寒冰剑', '青釭剑', '古锭刀', '青龙偃月刀',
    '丈八蛇矛', '贯石斧', '方天画戟', '麒麟弓', '朱雀羽扇', '八卦阵',
    '仁王盾', '藤甲', '白银狮子', '赤兔', '大宛', '紫骍', '的卢', '绝影', '爪黄飞电', '骅骝'
]

2.读取字典数据

现在的DECK_COMPOSITION是一个字典,EQUIPMENT_NAMES是一个列表,如果写for i in DECK_COMPOSITION那么 Python 只会默认给你键,不给你值。

而调用.items()就会把字典拆成如[("杀", 30), ("火杀", 9), ......]这样的列表,那么name和count就能分别接住牌名和数据。

所以这里我们写

python 复制代码
for name, count in DECK_COMPOSITION.items():

3.建立牌堆

在Python中,'*'对于列表有着特殊的含义,叫做序列重复。[name]*count这个运算的结果表示count个列表,即[name, name, name],......。

在这里用full_deck[]列表作为牌堆。

那么有的同学可能会用full_deck.append([name]*count),但是,如果你把这些append进去,那么full_deck就变成[[name1, name1, name1,......], [name2,name2,name2,......]],就相当于是二维的了。

正确结果是用 full_deck.append([name]*count),就能把一个序列里的元素拆开,一个一个放进列表里。那么full_deck[]里面就是160个元素(160张牌)了。

python 复制代码
full_deck = []
for name, count in DECK_COMPOSITION.items():
    full_deck.extend([name] * count)

4.选牌

到现在逻辑就变成了160张牌选10张牌。那么思路1:就是打乱牌堆列表,然后截取前10个。思路2:不打乱,随机抽选,这两个思路完全等价。

random.shuffle(full_deck)

random.sample(full_deck, 10)

对于截取列表的操作:top_10 = full_deck[:10];对于随机抽选的,直接top_10 = random.sample(full_deck, 10)。

5.去重过滤

用到set()容器。 in set的复杂度为O(1),in dist的复杂度为O(n)。虽然我们只需要从10个找若干个,但是空间开两个会方便些,一个是集合,判定是否出现过,一个是列表,存储牌值。

python 复制代码
seen = set()
gained = []
for card in top_10:
    if card not in seen:
        gained.append(card)
        seen.add(card)

那么角色这个技能的总过牌就是gained[]里面的所有值,过牌量是len(gained)。

6.找出武器

我们要用gained里面的元素比对装备区的所有牌。

假设一个数据(牌名)为c,那么c in EQUIPMENT_NAMES就意味着python自动检查c是否在该容器中,并返回值布尔值。

用上列表推导式:[c for c in gained if c in EQUIPMENT_NAMES]

意味着:如果符合条件,就把这张牌放进一个临时的新列表里。所以对这个表达式取len()即可。

total_gained.append(len(gained))

equip_gained.append(len([c for c in gained if c in EQUIPMENT_NAMES]))

7.重复1万次

现在就需要2个统计量,一个统计拿牌次数,一个统计含装备次数,采用列表形式。整段函数返回的就是这2个列。

8.函数整体CODE

python 复制代码
def simulate(iterations=10000):
    full_deck = []
    for name, count in DECK_COMPOSITION.items():
        full_deck.extend([name] * count)

    total_gained, equip_gained = [], []

    for _ in range(iterations):
        random.shuffle(full_deck)
        top_10 = full_deck[:10]
        # 等价于top_10 = random.choice(full_deck, 10)

        seen = set()
        gained = []
        for card in top_10:
            if card not in seen:
                gained.append(card)
                seen.add(card)

        total_gained.append(len(gained))
        equip_gained.append(len([c for c in gained if c in EQUIPMENT_NAMES]))

    return total_gained, equip_gained

输出结果

1.分布列的计算

pd.Series()是Pandas最基础的数据结构,可以带上很对数据处理的方法,比如.mean(),.sum()等。

dist = s.value_counts()会扫描整个列表,统计每个数字出现的次数,然后返回键值对。假如是[7,7,6],那么dist={7:2, 6:1}.如果加上normalize=True,那么返回的是比例,dist={7:0.67, 6:0.33}。然后用sort_index能够升序排列。

python 复制代码
def print_distribution(data, title):
    """控制台显示分布列"""
    print(f"\n{'='*10} {title} 分布列 {'='*10}")
    s = pd.Series(data)
    dist = s.value_counts(normalize=True).sort_index()
    for val, prob in dist.items():
        print(f"获得数量 {val:2d} | 概率 {prob:6.2%}")
    print(f"--- 平均收益: {s.mean():.2f} 张 ---")

2.图表的生成

实在是看不懂,用AI解释。

python 复制代码
    # 绘制总牌数 (使用 #CC5289)
    plt.bar(s_total.index - 0.2, s_total.values, width=0.4,
            color='#CC5289', alpha=0.8, edgecolor='black',
            label=f'Total Gained (Avg: {avg_total:.2f})')

    # 绘制装备牌数 (使用 #84B4DA)
    plt.bar(s_equip.index + 0.2, s_equip.values, width=0.4,
            color='#84B4DA', alpha=0.8, edgecolor='black',
            label=f'Equip Gained (Avg: {avg_equip:.2f})')

绘制柱状图的核心命令

位置与数值参数:

s_total.index - 0.2 (x 坐标)------目的是配合下一行代码中的 + 0.2,使得两根柱子能"肩并肩"靠在一起,而不是重叠。

s_total.values (y 坐标)------这代表了柱子的高度。

外观样式参数:

width=0.4------设定柱子的宽度。

color='#CC5289'------设置柱子内部的填充颜色。

alpha=0.8 (透明度)------范围从 0(完全透明)到 1(不透明)。

edgecolor='black' (边框颜色)------给柱子加上一圈黑色的轮廓线。

图例标注参数

label=f'Total Gained (Avg: {avg_total:.2f})'------这是给这组数据起的"名字"。

python 复制代码
    # 标注数值
    for i, v in s_total.items():
        plt.text(i - 0.2, v + 0.005, f'{v:.1%}', ha='center', fontsize=9, color='#883355')
    for i, v in s_equip.items():
        plt.text(i + 0.2, v + 0.005, f'{v:.1%}', ha='center', fontsize=9, color='#446688')
python 复制代码
    # 图表设置
    plt.title("Distribution of Skill 'Mingxing' Benefits (10,000 Sims)", fontsize=15)
    plt.xlabel("Number of Cards", fontsize=12)
    plt.ylabel("Probability (%)", fontsize=12)
    plt.xticks(range(0, 11))
    plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f'{y * 100:.0f}%'))
python 复制代码
    # 图例显示平均值
    plt.legend(fontsize=12, loc='upper right', frameon=True, shadow=True)
    plt.grid(axis='y', linestyle='--', alpha=0.6)
    plt.tight_layout()
    plt.show()

Code

python 复制代码
import random
import pandas as pd
import matplotlib.pyplot as plt

# 1. 定义军争160张牌堆 (简化版分布)
# 键为牌名,值为该牌名在牌堆中的数量
DECK_COMPOSITION = {
    # 基本牌 85 张
    '杀': 30, '雷杀': 9, "火杀": 5, '闪': 24, '桃': 12, '酒': 5,
    # 锦囊牌 50 张
    '过河拆桥': 6, '顺手牵羊': 5, '无中生有': 4, '无懈可击': 7, "借刀杀人": 2, '决斗': 3,
    '南蛮入侵': 3, '万箭齐发': 1, '五谷丰登': 2, '桃园结义': 1, '火攻': 3, '铁索连环': 6,
    '兵粮寸断': 2, '乐不思蜀': 3, '闪电': 2,
    # 装备牌 25 张
    '诸葛连弩': 2, '雌雄双股剑': 1, '寒冰剑': 1, '青釭剑': 1, '古锭刀': 1,
    '青龙偃月刀': 1, '丈八蛇矛': 1, '贯石斧': 1, '方天画戟': 1, '麒麟弓': 1, '朱雀羽扇': 1,
    '八卦阵': 2, '仁王盾': 1, '藤甲': 2, '白银狮子': 1,
    '赤兔': 1, '大宛': 1, '紫骍': 1, '的卢': 1, '绝影': 1, '爪黄飞电': 1, '骅骝': 1
}

# 标记哪些是装备牌,用于统计另一个技能的收益
EQUIPMENT_NAMES = [
    '诸葛连弩', '雌雄双股剑', '寒冰剑', '青釭剑', '古锭刀', '青龙偃月刀',
    '丈八蛇矛', '贯石斧', '方天画戟', '麒麟弓', '朱雀羽扇', '八卦阵',
    '仁王盾', '藤甲', '白银狮子', '赤兔', '大宛', '紫骍', '的卢', '绝影', '爪黄飞电', '骅骝'
]

# ==========================================
# 2. 执行模拟
# ==========================================
def simulate(iterations=10000):
    full_deck = []
    for name, count in DECK_COMPOSITION.items():
        full_deck.extend([name] * count)

    total_gained, equip_gained = [], []

    for _ in range(iterations):
        random.shuffle(full_deck)
        top_10 = full_deck[:10]
        # 等价于top_10 = random.choice(full_deck, 10)

        seen = set()
        gained = []
        for card in top_10:
            if card not in seen:
                gained.append(card)
                seen.add(card)

        total_gained.append(len(gained))
        equip_gained.append(len([c for c in gained if c in EQUIPMENT_NAMES]))

    return total_gained, equip_gained

# ==========================================
# 3. 输出分布列
# ==========================================
def print_distribution(data, title):
    """控制台显示分布列"""
    print(f"\n{'='*10} {title} 分布列 {'='*10}")
    s = pd.Series(data)
    dist = s.value_counts(normalize=True).sort_index()
    for val, prob in dist.items():
        print(f"获得数量 {val:2d} | 概率 {prob:6.2%}")
    print(f"--- 平均收益: {s.mean():.2f} 张 ---")

# ==========================================
# 4. 绘图分析
# ==========================================
def plot_combined_distribution(total_data, equip_data):
    # 计算频率
    s_total = pd.Series(total_data).value_counts(normalize=True).sort_index()
    s_equip = pd.Series(equip_data).value_counts(normalize=True).sort_index()

    # 计算平均值
    avg_total = sum(total_data) / len(total_data)
    avg_equip = sum(equip_data) / len(equip_data)

    plt.figure(figsize=(12, 7))
    plt.style.use('seaborn-v0_8-whitegrid')

    # 绘制总牌数 (使用 #CC5289)
    plt.bar(s_total.index - 0.2, s_total.values, width=0.4,
            color='#CC5289', alpha=0.8, edgecolor='black',
            label=f'Total Gained (Avg: {avg_total:.2f})')

    # 绘制装备牌数 (使用 #84B4DA)
    plt.bar(s_equip.index + 0.2, s_equip.values, width=0.4,
            color='#84B4DA', alpha=0.8, edgecolor='black',
            label=f'Equip Gained (Avg: {avg_equip:.2f})')

    # 标注数值
    for i, v in s_total.items():
        plt.text(i - 0.2, v + 0.005, f'{v:.1%}', ha='center', fontsize=9, color='#883355')
    for i, v in s_equip.items():
        plt.text(i + 0.2, v + 0.005, f'{v:.1%}', ha='center', fontsize=9, color='#446688')

    # 图表设置
    plt.title("Distribution of Skill 'Mingxing' Benefits (10,000 Sims)", fontsize=15)
    plt.xlabel("Number of Cards", fontsize=12)
    plt.ylabel("Probability (%)", fontsize=12)
    plt.xticks(range(0, 11))
    plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f'{y * 100:.0f}%'))

    # 图例显示平均值
    plt.legend(fontsize=12, loc='upper right', frameon=True, shadow=True)

    plt.grid(axis='y', linestyle='--', alpha=0.6)
    plt.tight_layout()
    plt.show()

if __name__ == "__main__":
    t_res, e_res = simulate(10000)
    print_distribution(t_res, "技能【明星】获得总牌数")
    print_distribution(e_res, "其中获得的装备牌数")
    plot_combined_distribution(t_res, e_res)

结果:


至于扩展结论以及在游戏中的实操,咱就不提了,那就是一千个人一千个看法喽。

相关推荐
老绿光2 小时前
Python 字典完全指南:从入门到实战
linux·服务器·python
是小蟹呀^2 小时前
【总结】LangChain中如何维持记忆
python·langchain·memory
蓝色的杯子2 小时前
OpenClaw一文详细了解-手搓OpenClaw-4 Tool Runtime
人工智能·python
克里普crirp2 小时前
电离层TEC地图中添加晨昏线/昼夜转换线
python
Dxy12393102162 小时前
Python使用PyEnchant详解:打造高效拼写检查工具
开发语言·python
架构师老Y3 小时前
011、消息队列应用:RabbitMQ、Kafka与Celery
python·架构·kafka·rabbitmq·ruby
枫叶林FYL3 小时前
【Python高级工程与架构实战】项目四:生产级LLM Agent框架:基于PydanticAI的类型安全企业级实现
人工智能·python·自然语言处理
龙腾AI白云3 小时前
多模大模型应用实战:智能问答系统开发
python·机器学习·数据分析·django·tornado
Hommy883 小时前
【开源剪映小助手】配置与部署
python·开源·aigc·剪映小助手