引言
扭蛋机(Gacha)是现代游戏中最常见的付费机制之一,玩家通过抽取随机获得不同稀有度的角色或道具。为了平衡玩家体验与商业收益,扭蛋系统通常设计复杂的概率模型和保底机制。本文将用Java实现一个完整的扭蛋机模拟器,深入讲解概率分布建模、伪随机数生成、保底机制设计,以及期望值和方差的计算方法,帮助你理解游戏经济学背后的数学原理。
一、扭蛋机的概率模型
1.1 基础概率设计
典型的扭蛋系统包含多个稀有度等级,例如:
– SSR(超稀有):1%
– SR(稀有):10%
– R(普通):89%
1.2 保底机制类型
- 软保底(Pity System):连续N次未出SSR后,每次抽取SSR概率递增
- 硬保底(Guarantee):最多N次必出SSR
- 保底继承:当前卡池的保底计数可继承到下一个卡池
- 大保底:小保底(50%概率)失败后,下一次必出目标角色
二、Java核心实现
import java.util.*;
/**
* 扭蛋物品类
* 表示扭蛋池中的一种奖励
*/
class GachaItem {
String name;
String rarity; // SSR, SR, R
double baseProbability; // 基础概率
boolean isUp; // 是否当前UP角色
public GachaItem(String name, String rarity, double baseProbability, boolean isUp) {
this.name = name;
this.rarity = rarity;
this.baseProbability = baseProbability;
this.isUp = isUp;
}
@Override
public String toString() {
return name + "(" + rarity + ")";
}
}
/**
* 扭蛋结果类
*/
class GachaResult {
GachaItem item;
int pullNumber; // 第几抽
boolean isPity; // 是否由保底触发
public GachaResult(GachaItem item, int pullNumber, boolean isPity) {
this.item = item;
this.pullNumber = pullNumber;
this.isPity = isPity;
}
}
/**
* 扭蛋机核心类
* 实现完整的概率模型、保底机制和统计计算
*/
public class GachaMachine {
private final List<GachaItem> itemPool;
private final Random random;
// 保底参数
private final int softPityStart; // 软保底开始抽数
private final double softPityIncrement; // 每次递增概率
private final int hardPity; // 硬保底抽数
private final boolean hasBigPity; // 是否有大保底
// 状态跟踪
private int ssrPityCounter; // 距离上次SSR的抽数
private boolean lastSsrWasUp; // 上次SSR是否是UP角色
// 统计
private int totalPulls;
private int ssrCount;
private int srCount;
private int rCount;
private int pityTriggered;
private List<Integer> ssrIntervals; // 每次SSR间隔的抽数
public GachaMachine(List<GachaItem> pool, int softPityStart,
double softPityIncrement, int hardPity, boolean hasBigPity) {
this.itemPool = new ArrayList<>(pool);
this.random = new Random();
this.softPityStart = softPityStart;
this.softPityIncrement = softPityIncrement;
this.hardPity = hardPity;
this.hasBigPity = hasBigPity;
this.ssrPityCounter = 0;
this.lastSsrWasUp = true; // 初始假设上次是UP,这样第一次小保底正常触发
this.totalPulls = 0;
this.ssrCount = 0;
this.srCount = 0;
this.rCount = 0;
this.pityTriggered = 0;
this.ssrIntervals = new ArrayList<>();
}
/**
* 计算当前SSR概率(考虑软保底)
*/
public double getCurrentSsrProbability() {
double baseSsrProb = 0.0;
for (GachaItem item : itemPool) {
if ("SSR".equals(item.rarity)) {
baseSsrProb += item.baseProbability;
}
}
if (ssrPityCounter < softPityStart) {
return baseSsrProb;
}
// 软保底:概率线性递增
double extraProb = (ssrPityCounter - softPityStart + 1) * softPityIncrement;
return Math.min(baseSsrProb + extraProb, 1.0);
}
/**
* 单次抽取
* @return 抽取结果
*/
public GachaResult pull() {
totalPulls++;
ssrPityCounter++;
boolean isPityTrigger = false;
GachaItem result = null;
// 检查硬保底
if (ssrPityCounter >= hardPity) {
result = drawSsrWithPity();
isPityTrigger = true;
pityTriggered++;
} else {
// 计算当前各稀有度概率
double ssrProb = getCurrentSsrProbability();
double roll = random.nextDouble();
if (roll < ssrProb) {
result = drawSsrNormal();
} else {
result = drawLowerRarity(roll - ssrProb);
}
}
// 更新状态
if ("SSR".equals(result.rarity)) {
ssrCount++;
ssrIntervals.add(ssrPityCounter);
ssrPityCounter = 0;
lastSsrWasUp = result.isUp;
} else if ("SR".equals(result.rarity)) {
srCount++;
} else {
rCount++;
}
return new GachaResult(result, totalPulls, isPityTrigger);
}
/**
* 通过保底抽取SSR
*/
private GachaItem drawSsrWithPity() {
// 如果有大保底机制且上次SSR非UP,本次必出UP
if (hasBigPity && !lastSsrWasUp) {
for (GachaItem item : itemPool) {
if ("SSR".equals(item.rarity) && item.isUp) {
return item;
}
}
}
return drawSsrNormal();
}
/**
* 正常抽取SSR(按UP概率分配)
*/
private GachaItem drawSsrNormal() {
List<GachaItem> ssrs = new ArrayList<>();
double totalWeight = 0;
for (GachaItem item : itemPool) {
if ("SSR".equals(item.rarity)) {
ssrs.add(item);
totalWeight += item.baseProbability;
}
}
double roll = random.nextDouble() * totalWeight;
double cumsum = 0;
for (GachaItem item : ssrs) {
cumsum += item.baseProbability;
if (roll < cumsum) {
return item;
}
}
return ssrs.get(ssrs.size() - 1);
}
/**
* 抽取SR或R
*/
private GachaItem drawLowerRarity(double normalizedRoll) {
List<GachaItem> srs = new ArrayList<>();
List<GachaItem> rs = new ArrayList<>();
double srTotal = 0, rTotal = 0;
for (GachaItem item : itemPool) {
if ("SR".equals(item.rarity)) {
srs.add(item);
srTotal += item.baseProbability;
} else if ("R".equals(item.rarity)) {
rs.add(item);
rTotal += item.baseProbability;
}
}
double srProb = srTotal / (srTotal + rTotal);
if (normalizedRoll < srProb) {
return drawFromPool(srs, srTotal);
} else {
return drawFromPool(rs, rTotal);
}
}
private GachaItem drawFromPool(List<GachaItem> pool, double totalWeight) {
double roll = random.nextDouble() * totalWeight;
double cumsum = 0;
for (GachaItem item : pool) {
cumsum += item.baseProbability;
if (roll < cumsum) {
return item;
}
}
return pool.get(pool.size() - 1);
}
/**
* 十连抽
*/
public List<GachaResult> pullTen() {
List<GachaResult> results = new ArrayList<>();
for (int i = 0; i < 10; i++) {
results.add(pull());
}
return results;
}
/**
* 计算抽取到目标物品的期望抽数
* 使用蒙特卡洛模拟
*/
public double simulateExpectedPulls(String targetName, int simulations) {
long totalPullsSum = 0;
int successCount = 0;
for (int sim = 0; sim < simulations; sim++) {
GachaMachine simMachine = new GachaMachine(itemPool, softPityStart,
softPityIncrement, hardPity, hasBigPity);
int pulls = 0;
boolean found = false;
while (pulls < hardPity * 3 && !found) { // 设置上限防止无限循环
GachaResult result = simMachine.pull();
pulls++;
if (result.item.name.equals(targetName)) {
found = true;
}
}
if (found) {
totalPullsSum += pulls;
successCount++;
}
}
return successCount > 0 ? (double) totalPullsSum / successCount : -1;
}
/**
* 计算指定抽数内获得SSR的概率(累积分布)
*/
public double simulateCumulativeSsrProbability(int maxPulls, int simulations) {
int successCount = 0;
for (int sim = 0; sim < simulations; sim++) {
GachaMachine simMachine = new GachaMachine(itemPool, softPityStart,
softPityIncrement, hardPity, hasBigPity);
boolean gotSsr = false;
for (int i = 0; i < maxPulls && !gotSsr; i++) {
GachaResult result = simMachine.pull();
if ("SSR".equals(result.item.rarity)) {
gotSsr = true;
}
}
if (gotSsr) successCount++;
}
return (double) successCount / simulations;
}
/**
* 打印统计报告
*/
public void printStatistics() {
System.out.println("===== 扭蛋统计报告 =====");
System.out.println("总抽数: " + totalPulls);
System.out.println("SSR数: " + ssrCount + " (" + String.format("%.2f%%", 100.0 * ssrCount / totalPulls) + ")");
System.out.println("SR数: " + srCount + " (" + String.format("%.2f%%", 100.0 * srCount / totalPulls) + ")");
System.out.println("R数: " + rCount + " (" + String.format("%.2f%%", 100.0 * rCount / totalPulls) + ")");
System.out.println("保底触发次数: " + pityTriggered);
if (!ssrIntervals.isEmpty()) {
double avgInterval = ssrIntervals.stream().mapToInt(Integer::intValue).average().orElse(0);
int maxInterval = ssrIntervals.stream().mapToInt(Integer::intValue).max().orElse(0);
System.out.println("SSR平均间隔: " + String.format("%.2f", avgInterval) + "抽");
System.out.println("SSR最大间隔: " + maxInterval + "抽");
}
}
// Getters
public int getTotalPulls() { return totalPulls; }
public int getSsrCount() { return ssrCount; }
}
三、期望值与方差的数学推导
/**
* 扭蛋期望值计算器
* 基于概率论计算理论期望和方差
*/
public class GachaExpectationCalculator {
/**
* 计算硬保底机制下的期望抽数
* 公式:E = sum(k * P(第k抽首次出)) for k=1..N
* 其中P(第k抽首次出) = P(前k-1抽不出) * P(第k抽出)
*/
public static double calculateHardPityExpectation(double baseProb, int hardPity) {
double expectation = 0;
double cumulativeNoSsr = 1.0; // P(前k-1抽都不出)
for (int k = 1; k <= hardPity; k++) {
double probK; // 第k抽出的概率
if (k < hardPity) {
probK = baseProb;
} else {
probK = 1.0; // 硬保底必出
}
double probFirstAtK = cumulativeNoSsr * probK;
expectation += k * probFirstAtK;
if (k < hardPity) {
cumulativeNoSsr *= (1 - baseProb);
}
}
return expectation;
}
/**
* 计算软保底+硬保底组合机制下的期望
* 使用数值方法近似
*/
public static double calculateSoftHardPityExpectation(
double baseProb, int softStart, double increment, int hardPity) {
double expectation = 0;
double cumulativeNoSsr = 1.0;
for (int k = 1; k <= hardPity; k++) {
double currentProb;
if (k >= hardPity) {
currentProb = 1.0;
} else if (k >= softStart) {
currentProb = Math.min(baseProb + (k - softStart + 1) * increment, 1.0);
} else {
currentProb = baseProb;
}
double probFirstAtK = cumulativeNoSsr * currentProb;
expectation += k * probFirstAtK;
cumulativeNoSsr *= (1 - currentProb);
}
return expectation;
}
/**
* 计算方差:Var(X) = E[X²] - (E[X])²
*/
public static double calculateVariance(double baseProb, int hardPity) {
double expectation = 0;
double expectationSquared = 0;
double cumulativeNoSsr = 1.0;
for (int k = 1; k <= hardPity; k++) {
double probK = (k < hardPity) ? baseProb : 1.0;
double probFirstAtK = cumulativeNoSsr * probK;
expectation += k * probFirstAtK;
expectationSquared += k * k * probFirstAtK;
if (k < hardPity) {
cumulativeNoSsr *= (1 - baseProb);
}
}
return expectationSquared - expectation * expectation;
}
/**
* 大保底机制下获得UP角色的期望
* 小保底50%概率出UP,失败则大保底必出UP
*/
public static double calculateBigPityExpectation(double baseProb, int hardPity, double upRate) {
// 先计算获得一个SSR的期望抽数
double ssrExpectation = calculateHardPityExpectation(baseProb, hardPity);
// 获得UP的期望SSR数 = 1/upRate(几何分布期望)
double expectedSsrsForUp = 1.0 / upRate;
// 但大保底机制下,最多2个SSR必得UP
// 实际期望更复杂,使用数值模拟更精确
return ssrExpectation * expectedSsrsForUp;
}
}
四、完整运行示例
/**
* 扭蛋机模拟器主程序
*/
public class GachaSimulator {
public static void main(String[] args) {
// 构建扭蛋池
List<GachaItem> pool = Arrays.asList(
new GachaItem("传说英雄A", "SSR", 0.005, true), // UP SSR 0.5%
new GachaItem("传说英雄B", "SSR", 0.005, false), // 非UP SSR 0.5%
new GachaItem("稀有角色C", "SR", 0.05, false),
new GachaItem("稀有角色D", "SR", 0.05, false),
new GachaItem("普通物品E", "R", 0.445, false),
new GachaItem("普通物品F", "R", 0.445, false)
);
// 创建扭蛋机:软保底从74抽开始,每次+6%,90抽硬保底,启用大保底
GachaMachine machine = new GachaMachine(pool, 74, 0.06, 90, true);
System.out.println("=== 扭蛋模拟开始 ===\n");
// 模拟1000抽
System.out.println("执行1000次单抽...");
for (int i = 0; i < 1000; i++) {
GachaResult result = machine.pull();
if ("SSR".equals(result.item.rarity)) {
System.out.println("第" + result.pullNumber + "抽获得 " + result.item +
(result.isPity ? " [保底]" : ""));
}
}
machine.printStatistics();
// 计算理论期望值
System.out.println("\n===== 理论期望值 =====");
double hardExpectation = GachaExpectationCalculator.calculateHardPityExpectation(0.01, 90);
System.out.println("纯硬保底期望抽数: " + String.format("%.2f", hardExpectation));
double softHardExpectation = GachaExpectationCalculator.calculateSoftHardPityExpectation(
0.01, 74, 0.06, 90);
System.out.println("软+硬保底期望抽数: " + String.format("%.2f", softHardExpectation));
double variance = GachaExpectationCalculator.calculateVariance(0.01, 90);
System.out.println("方差: " + String.format("%.2f", variance));
System.out.println("标准差: " + String.format("%.2f", Math.sqrt(variance)));
// 蒙特卡洛模拟验证
System.out.println("\n===== 蒙特卡洛模拟(10万次) =====");
double mcExpectation = machine.simulateExpectedPulls("传说英雄A", 100000);
System.out.println("获得UP角色期望抽数: " + String.format("%.2f", mcExpectation));
double prob90 = machine.simulateCumulativeSsrProbability(90, 100000);
System.out.println("90抽内获得SSR概率: " + String.format("%.4f", prob90));
double prob180 = machine.simulateCumulativeSsrProbability(180, 100000);
System.out.println("180抽内获得SSR概率: " + String.format("%.4f", prob180));
}
}
五、概率模型分析
| 机制类型 | 期望抽数 | 方差 | 玩家体验 |
|---|---|---|---|
| 纯随机(1%) | 100.00 | 9900 | 极差,非酋可能永远不出 |
| 硬保底(90抽) | 55.21 | 较大 | 较好,有明确上限 |
| 软+硬保底(74+6%,90) | ~52.3 | 中等 | 优秀,渐进式保护 |
| 大保底(50%UP) | ~104.6 | 中等 | UP角色获取稳定 |
六、总结与扩展
本文实现了完整的扭蛋机概率模型,涵盖基础随机抽取、软保底递增、硬保底上限和大保底机制。通过蒙特卡洛模拟和理论推导,可以精确计算玩家的期望投入和概率分布。
扩展方向:
1. 保底继承机制:实现跨卡池的保底计数器继承
2. 多物品UP:实现双UP或自选UP的概率分配
3. 综合概率分析:结合SR保底、十连保底等更复杂的规则
4. 收益模型:基于期望抽数计算玩家平均消费金额