和平精英抽扭蛋概率计算(附Kotlin测试代码)

2,502 阅读10分钟

最近入坑了和平精英,算是我开始玩的第一款腾讯系的手游。众所周知手游是氪金大户,而对于和平精英来说,氪金主要也就是为了抽各种皮肤了。什么人物的衣服,背包,头盔,枪械的外观,甚至是车的品牌,都是可以通过氪金来获得的。作为刚入坑的小白,我在打开活动页面之后,直接被各种复杂的规则吓傻了:什么普通追加,保护追加,保底奖励,升降级概率,各种概念层出不穷,琳琅满目。本着“一切到最后都是数学问题”的原则,我决定写个模拟抽扭蛋测试的代码,来看看到底平均需要花多少钱,才能抽到自己想要的皮肤。

规则说明

首先稍微介绍一下规则,这里以最近出的“激战未来”系列为例:

  1. 花费6幸运币启动扭蛋机,随机获得一个1-3星的扭蛋,不同的扭蛋类型与星级排列组合,对应不同的奖励;
  2. 普通追加:免费,20%概率升星成功(因为有五个扭蛋类型,和当前类型一致才能升星),随机提高1-3星;80%升星失败,随机降低1-2星,并直接领取奖励;
  3. 保护追加:需要花费幸运币,20%概率升星成功,失败不掉星,3次保底必升星成功,随机提高1-3星。但当前扭蛋星级越高,保护追加一次的幸运币消耗越高;
  4. 升降星概率:82%升1星、17%升2星和1%升3星;75%掉1星,25%掉2星;
  5. 领取奖励:根据当前的扭蛋等级与星级,可以选择直接领取对应物品或者换算成碎片(一种可以兑换其他物品的货币),但要注意的是:如果对应的物品你现在没有的话,换算碎片会是物品价格的一半(后面会详细说明)。

然后我们来看一下不同星级对应的碎片价值:

星级碎片价值
02
112
236
3108
4320
5960
62880
78640

要说明一下的是:这里的碎片价值是指每一星级对应物品在兑换商店的价格,比如说你想要五级的“绚金战神”的皮肤,那么你需要在商店里用960个碎片去兑换,或者你需要抽到“绚金战神”的扭蛋并追加到五星。但同星级的不同扭蛋之间是不可以等价兑换的,除非你已经拥有了那个星级的对应物品,举个例子:假如我现在什么物品都没有,而我想要“绚金战神”的五星皮肤,那么当我抽到“苍蓝魅影”的扭蛋并追加到五星,我只有两个选择:1.兑换“苍蓝魅影”的五星皮肤,放弃“绚金战神”;2.折算成扭蛋碎片960/2 = 480个,然后继续获得碎片直到获得960个碎片,去兑换商店去进行兑换。但如果我已经有了“苍蓝魅影”的五星皮肤,而此时我又抽到“苍蓝魅影”的扭蛋并追加到五星,那么我就可以直接折算960个碎片并去兑换其他同价格的五星皮肤,此时相当于不同扭蛋类型之间进行了等价兑换。

这其实是一个重要的规则,因为它让你在“退而求其次”和“从一而终”之间做出一个取舍,大部分情况下我们都会选择“退而求其次”,也就是兑换了价值更高但并不是最喜欢的那款,从而导致之后还是想要一开始看上的那款,然后重新花钱去抽。所以这里做出的选择,一定要在一开始就好好考虑清楚。

策略分析

于是这里就可以引出我们平常一般使用的几种策略:傻瓜式平A,普通追加,保护追加。我们来依次解释一下策略内容,然后用代码进行模拟试抽,通过计算10000次试抽的平均收益,来评估一下不同的策略适合什么样的氪金目的。

先来创建几个需要用到的辅助类:

/**
 * 单次抽扭蛋的结果类
 */
data class DrawEggResult(
    val egg: Egg,   // 扭蛋类型
    val level: Int  // 星级
)

/**
 * 不同策略下抽扭蛋的最终结果类
 */
data class DrawingResult(
    val spending: Int,      // 幸运币总花销
    val points: Int,        // 获得碎片总数

    // 已兑换物品记录
    val eggItems: Map<Egg, MutableList<Boolean>> = mutableMapOf<Egg, MutableList<Boolean>>().apply {
        Egg.values().forEach { egg ->
            this[egg] = mutableListOf(true, true, true, false, false, false, false, false)
        }
    }
)

/**
 * 定义扭蛋的枚举类
 */
enum class Egg(val description: String) {
    EGG_1("苍蓝魅影"),
    EGG_2("翡翠骑士"),
    EGG_3("绚金战神"),
    EGG_4("炫彩锋芒"),
    EGG_5("幻彩光梭")
}

傻瓜式平A

这个策略简单来说就是:只抽一轮,决不追加,先兑换三级物品,再兑换碎片。因为追加其实存在很大的风险:80%的可能性扭蛋类型不一致,那么就会降1-2星,所以我们只抽一轮,不给它降星的机会,如果是三星,立刻兑换对应物品,其他时候则直接兑换碎片。通过碎片的快速积累来直接兑换我们最终想要的物品。话不多说,上代码:

private val pointsList = listOf(2, 12, 36, 108, 320, 960, 2880, 8640)       // 碎片价值表

/**
 * 傻瓜式平A,直接兑换三级物品,然后兑换碎片
 * @param expectedPoints        目标碎片数
 * @param spendingBudget        幸运币预算
 */
fun dummyDrawing(expectedPoints: Int, spendingBudget: Int): DrawingResult {
    var spending = 0
    var points = 0

    // 记录已兑换物品
    val eggItemRetrieved = mutableMapOf<Egg, MutableList<Boolean>>().apply {
        Egg.values().forEach { egg ->
            this[egg] = mutableListOf(true, true, true, false, false, false, false, false)
        }
    }

    // 当碎片数达到目标或者幸运币超出预算时停止
    while (points < expectedPoints && spending < spendingBudget) {
        spending += 6
        val result = getNextDrawResult()
        if (eggItemRetrieved[result.egg]?.get(result.level) == true) {
            points += pointsList[result.level]   // 已有兑换物品,获得全额碎片返还
        } else {
            eggItemRetrieved[result.egg]?.set(result.level, true)   // 没有当前物品,兑换该物品
        }
    }
    return DrawingResult(spending, points, eggItemRetrieved)
}

/**
 * 抽一次扭蛋并返回结果
 */
internal fun getNextDrawResult(): DrawEggResult {
    // 扭蛋类型
    val resultEgg = Egg.values()[randomPick(Egg.values().size)]
    // 扭蛋级别
    return when (randomPick(100)) {
        0 -> DrawEggResult(resultEgg, 3)
        in 1 until 18 -> DrawEggResult(resultEgg, 2)
        else -> DrawEggResult(resultEgg, 1)
    }
}

private fun randomPick(total: Int): Int {
    return Random.nextInt(total)
}

然后在单元测试中,设置幸运币预算为648,相当于充一个648,打印10000次测试的结果

@Test
fun testAverageDummyDrawing() {
    var totalSpending = 0       // 幸运币总花费
    var totalGetPoints = 0      // 净获得的碎片数量
    var totalEqualPoints = 0    // 物品折算的总碎片数量
    val drawingTimes = 10000
    for (i in 0 until drawingTimes) {
        val drawingResult = drawing.dummyDrawing(10000, 648)    // 满足最高幸运币预算的条件即可
        print("第 $i 次傻瓜式平A,花费幸运币:${drawingResult.spending},获得碎片:${drawingResult.points} ")
        for (egg in Egg.values()) {
            if (drawingResult.hasItemRetrieved(3, egg)) {
                print("3星物品:${egg.description} ")
            }
        }
        println("折算碎片: ${drawingResult.points + drawingResult.retrievedItemToPoints()} ")
        totalSpending += drawingResult.spending
        totalGetPoints += drawingResult.points
        totalEqualPoints += drawingResult.points + drawingResult.retrievedItemToPoints()
    }
    val averageSpending = totalSpending.toDouble() / drawingTimes
    val averageGetPoints = totalGetPoints.toDouble() / drawingTimes
    val averageEqualPoints = totalEqualPoints.toDouble() / drawingTimes
    println("平均花费幸运币:${averageSpending},平均获得碎片:${averageGetPoints},平均折算碎片:${averageEqualPoints},获得性价比:${averageGetPoints / averageSpending},折算性价比:${averageEqualPoints / averageSpending}")
}

最终结果: 平均花费幸运币:648.0,平均获得碎片:1734.8796,平均折算碎片:1840.0176,获得性价比:2.68,折算性价比:2.84

其实如果我们用上面的价值表格来计算性价比期望:

E = (P1V1 + P2V2 + P3V3) / 6 = 0.822 + 0.176 + 0.0118 = 2.84

可以看出这个值就是”折算性价比“,而”获得性价比“将近0.2的降幅主要是因为我们需要先兑换三级物品,才能拿到满额的108碎片返还。

优点

  • 策略简单有效,最终比例稳定
  • 同时有一定概率获得三级物品
  • 适合有着明确的目标(只想要一套五级皮肤,或者只想要一辆六星车皮等情况)的玩家选择

缺点

  • 花费时间(平A需要不停地抽&兑换,比较慢)
  • 只能获得三星物品,高于三星的物品都需要用碎片兑换
  • 对于想追高7星奖励的玩家来说,可能效率比较低
  • 对于想体验风险,获得高回报的玩家来说,收获太低

普通追加

这个策略的大致思路就是:见好就收,只使用普通追加,一旦抽并追加到五星及以上,则先兑换物品,再兑换碎片。因为普通追加是免费的,所以我们抽一次的成本并没有增加,而与此同时我们有更大的概率直接获得5,6,7星的物品。跟上面的傻瓜式平A一样,我们需要先兑换物品,在兑换碎片,这样可以使我们再次到达同样的星级时获得100%的碎片返还。代码如下:

/**
 * 普通追加,一旦抽并追加到五星及以上,则先兑换物品,再兑换碎片
 * @param expectedPoints        目标碎片数
 * @param spendingBudget        幸运币预算
 */
fun normalAddingDrawing(expectedPoints: Int, spendingBudget: Int): DrawingResult {
    var spending = 0
    var points = 0

    // 记录已兑换物品
    val eggItemRetrieved = mutableMapOf<Egg, MutableList<Boolean>>().apply {
        Egg.values().forEach { egg ->
            this[egg] = mutableListOf(true, true, true, false, false, false, false, false)
        }
    }

    // 当碎片数达到目标或者幸运币超出预算时停止
    while (points < expectedPoints && spending < spendingBudget) {
        spending += 6

        var currentLevel = 0
        var currentEgg: Egg? = null
        while (true) {
            val result = getNextDrawResult()
            if (currentEgg == null) {
                // 第一次抽取结束,准备继续追加
                currentEgg = result.egg
                currentLevel = result.level
            } else {
                if (currentEgg == result.egg) {
                    // 追加成功
                    currentLevel = min(currentLevel + result.level, 7)
                    if (currentLevel > 4) {
                        if (eggItemRetrieved[result.egg]?.get(currentLevel) == true) {
                            points += pointsList[currentLevel]   // 积分100%返还
                        } else {
                            eggItemRetrieved[result.egg]?.set(currentLevel, true)   // 兑换对应物品
                        }
                        // 本轮结束,直接退出
                        break
                    }
                } else {
                    // 追加失败,本轮结束,结算碎片并退出
                    val nextLevel = max(currentLevel - getDecreasingLevel(), 0)
                    points += pointsList[nextLevel] / (if (eggItemRetrieved[result.egg]?.get(nextLevel) == true) 1 else 2)
                    break
                }
            }
        }
    }
    return DrawingResult(spending, points, eggItemRetrieved)
}

/**
 * 返回随机降星的星级
 */
private fun getDecreasingLevel(): Int {
    return when (randomPick(4)) {
        0 -> 2
        else -> 1
    }
}

同样通过单元测试,设置幸运币预算为648,相当于充一个648,打印10000次测试的结果

@Test
fun testAverageNormalAddingDrawing() {
    var totalSpending = 0       // 幸运币总花费
    var totalGetPoints = 0      // 净获得的碎片数量
    var totalEqualPoints = 0    // 物品折算的总碎片数量
    val drawingTimes = 10000
    for (i in 0 until drawingTimes) {
        val drawingResult = drawing.normalAddingDrawing(10000, 648)    // 满足最高幸运币预算的条件即可
        print("第 $i 次普通追加,花费幸运币:${drawingResult.spending},获得碎片:${drawingResult.points} ")
        for (egg in Egg.values()) {
            for (level in 5..7) {
                if (drawingResult.hasItemRetrieved(level, egg)) {
                    print("${level}星物品:${egg.description} ")
                }
            }
        }
        println("折算碎片: ${drawingResult.points + drawingResult.retrievedItemToPoints()} ")
        totalSpending += drawingResult.spending
        totalGetPoints += drawingResult.points
        totalEqualPoints += drawingResult.points + drawingResult.retrievedItemToPoints()
    }
    val averageSpending = totalSpending.toDouble() / drawingTimes
    val averageGetPoints = totalGetPoints.toDouble() / drawingTimes
    val averageEqualPoints = totalEqualPoints.toDouble() / drawingTimes
    println("平均花费幸运币:${averageSpending},平均获得碎片:${averageGetPoints},平均折算碎片:${averageEqualPoints},获得性价比:${averageGetPoints / averageSpending},折算性价比:${averageEqualPoints / averageSpending}")
}

最终结果为:平均花费幸运币:648.0,平均获得碎片:797.8586,平均折算碎片:1934.5946,获得性价比:1.23,折算性价比:2.99

可以看出,如果只算净获得的碎片,那么性价比相对较低,但如果把兑换的物品按碎片价格折算一下,那么可以看到,折算后的性价比和傻瓜式平A策略相比提高了0.15。

优点

  • 折算后性价比更高,总收益更大
  • 对于欧皇,更容易一发入魂,获得相当大的收益
  • 适合喜欢承受一定风险,想获得更高回报的玩家选择

缺点

  • 兑换的物品不一定是你最想要的,对于已经有确定目标的玩家来说,容易抽到自己不想要的皮肤或其他物品
  • 普通追加上下限更高,更看运气,一旦接近了下限,很有可能获得性价比接近1,甚至不到1的惨痛结果
  • 对于想稳定只一套物品就够的玩家,风险太大,不一定能承受

保护追加

对于保护追加,可以参考这篇文章对于它的分析,基本上可以说肯定是亏的,毕竟是拿钱来补花费的时间嘛。所以这里就不再用代码来模拟了,性价比肯定是没有前两种策略高的。

总结 & 说明

对于只想花一小部分钱(1000以下)获得1-2个特定皮肤的小伙伴,直接选择傻瓜式平A

对于想搏一搏,单车变摩托,或者收集控,想攒齐皮肤获的小伙伴,可以选择普通追加

对于重度氪金玩家,不差钱,差时间的小伙伴,直接保护追加,短平快

最后说明一下,这不是软文,腾讯也没有给我任何好处。毕竟“买的不如卖的精”,这些概率早就是算好的了,作为玩家再怎么模拟也占不到任何便宜,这里只是想让没有抽扭蛋经验的小伙伴有一个简单的了解和期望。最后一句:游戏有风险,氪金需谨慎,希望大家玩得开心就好。

参考链接:

和平精英扭蛋机超详细全攻略