笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • 游戏业务实现(6 年)

    • 游戏业务实现
    • 多模板游戏活动框架
    • 赛事玩法(Bracket / 瑞士轮 / BO 系列)
    • 业务幂等性设计
    • 跨渠道限购与游戏侧分布式事务落地
    • Redis 房间推荐列表
    • 排行榜 / 榜单
    • 抽卡 / 掉落与保底
    • 邮件 / 发奖补偿
    • 匹配 / 段位(MMR)
    • 游戏反作弊
    • 游戏经济与成长
    • 游戏与互联网后台的本质差异
    • 业务代理 · 支付 · 商城

抽卡 / 掉落与保底

带权抽样 + 保底(pity) + 伪随机分布(PRD)——让概率既符合公示、又不让玩家被"真随机"的连续不出逼疯。

一句话结论

保底与 PRD 在综合出货率仍等于公示值的前提下重排方差、封顶最坏体验,随机源只在服务端。

场景问题

抽卡是商业化核心,也是最容易出资损、合规、口碑三重事故的地方:

  • 概率要准且可公示:法规要求概率公示,实际掉率必须和公示一致,差一点点就是虚假宣传。
  • 真随机反人性:独立同分布的"真随机"下,5% 的东西连抽 80 次不出是完全正常的数学结果——但玩家会认定你"暗改概率",直接炸。
  • 保底要能兜底:90 抽必出金是承诺,计数器的累加、清零、跨版本迁移任何一步错都是大事故。
  • 多池叠加:常驻池 + UP 池 + 新手池 + 软保底(概率递增) + 硬保底,规则叠起来极易算错。
  • 发货幂等:抽到的道具要原子发到背包,抽卡扣费和发货不能一半成功一半失败,重试不能重复发。
  • 防刷:客户端算概率 = 送人头,必须服务端定夺,且防并发多抽。

核心矛盾:数学上的"公平随机"和玩家心理上的"感觉公平"是两回事。抽卡系统的精髓就是用保底和 PRD 去弥合这条鸿沟,同时守住概率合规和资损底线。

实现方案

一:带权抽样(别名法 / 前缀和)

给每个奖励一个权重,按权重抽。两种主流实现:

// 前缀和 + 二分:建表 O(n),单次抽样 O(log n),适合权重常变
func (t *PrefixTable) Draw(r uint64) int {
    x := r % t.total
    // 在前缀和数组里二分找第一个 > x 的位置
    return sort.Search(len(t.prefix), func(i int) bool { return t.prefix[i] > x })
}
  • 别名法(Alias Method):预处理 O(n),单次抽样 O(1),适合权重固定、抽样海量的池。
  • 前缀和 + 二分:单次 O(log n),建表简单、权重变动时重建便宜,工程上最常用。

随机源必须服务端且不可预测

抽样用的随机数必须由服务端生成(crypto/rand 或经过充分播种的 PRNG),绝不能让客户端传随机种子或让客户端自己算——否则玩家能预测/构造出货序列。掉率配置也只在服务端,客户端只做表现。

二:保底(pity)计数器

保底是"距离上次出金的抽数"计数器,通常存玩家档:

type GachaState struct {
    PityCounter  int  // 距上次出 SSR 的抽数
    IsUpGuaranteed bool // 大保底:上次歪了,这次必 UP
}

func (g *GachaState) Draw(cfg *PoolCfg, rng *rand.Rand) *Item {
    g.PityCounter++
    hit := false
    switch {
    case g.PityCounter >= cfg.HardPity:      // 硬保底:90 抽必出
        hit = true
    case g.PityCounter > cfg.SoftPityStart:  // 软保底:75 抽后概率递增
        hit = rng.Float64() < softProb(cfg, g.PityCounter)
    default:
        hit = rng.Float64() < cfg.BaseProb   // 基础概率 0.6%
    }
    if !hit { return g.drawNonSSR(cfg, rng) }

    g.PityCounter = 0                        // 出金清零
    if g.IsUpGuaranteed || rng.Float64() < cfg.UpRate {
        g.IsUpGuaranteed = false             // 抽到 UP,大保底重置
        return cfg.UpItem
    }
    g.IsUpGuaranteed = true                  // 歪了,下次大保底
    return g.drawStdSSR(cfg, rng)
}
  • 硬保底:PityCounter >= 90 直接必出,兜底承诺。
  • 软保底:75 抽后概率线性/阶梯递增,让"综合出货率"贴近公示的 ~1.6%(基础 0.6% 靠软保底拉高)。
  • 大保底:出金但歪了(非 UP),标记 IsUpGuaranteed,下次出金必 UP。

保底计数器是玩家资产,比金币还敏感

PityCounter 丢一次 = 玩家白抽几十次的愤怒。它必须和抽卡结果在同一事务里持久化:不能出现"发了道具但计数器没清零"(玩家投诉重复保底)或"计数器加了但抽卡回滚"(白扣抽数)。跨版本/跨池迁移计数器时要写明规则并公示。

打个比方:抽卡系统像扭蛋机 + 存钱罐的组合——扭蛋机每次投币随机吐一颗(基础概率),旁边的存钱罐默默记账"你已经攒了几次没出好货";一旦攒到承诺次数就强制吐一颗保底大奖(硬保底),中间还有一段"越接近门槛、吐好货概率越高"的软保底斜坡。出了 SSR 存钱罐清零,重新开始攒。类比失效边界:现实扭蛋是独立同分布——今天转空 20 次不改变明天的概率;游戏抽卡为了体验大量引入相关性(软保底递增、大保底转 UP、连败补偿),本质是"用可控的正相关抵消真随机的长尾"。玩家如果只盯"公示的基础概率 0.6%"去估摸期望,会严重低估综合出货率(含保底后其实接近 1.6%);反过来也别以为"没保底也能靠脸"——纯独立随机下连黑 80 次是完全合规的数学结果,只是没人玩得下去。

三:伪随机分布(PRD) —— 另一种"不那么随机"

除了保底,还有一种让概率"感觉更均匀"的经典做法(源自 DOTA/War3 暴击设计):每次不中,下次概率线性递增 C;一旦中了,概率重置为 C。

第 n 次未中后的实际概率 P(n) = C * n
调参使"期望出货间隔"= 公示概率对应的间隔
效果:几乎不会出现"连续很多次不出"或"连续爆出"的极端串

PRD 适合暴击、小额掉落这类"要感觉稳定"的场景;保底 pity 适合大额抽卡这类"承诺上限"的场景。两者可叠加。

四:抽卡事务(扣费 + 发货 + 计数器)

扣费、发货、计数器更新必须要么全成功要么全回滚,并挂幂等键(十连一个 draw_id)防止客户端重试导致重复发。货币扣减走统一的原子兑换接口(参考 业务代理 · 支付 · 商城 里的 Deposit::ExchangeProps:消费放 del_prop_list、奖励放 add_prop_list,一次原子完成)。

为什么这么做

为什么要保底/PRD,不用纯随机?
纯随机(独立同分布)下的"连续不出"在数学上完全正常,但玩家不接受概率论——他们只体验到"我氪了 500 块一个都没出"。保底把最坏体验封了顶(90 抽必出),PRD 把方差压小(几乎不会连续爆或连续空)。这不是改概率骗人,而是在综合出货率仍等于公示值的前提下,重新分配概率的方差,让体验可控。

为什么随机和掉率必须在服务端?
只要客户端能参与随机数生成或知道掉率表,玩家就能预测甚至构造出货,商业化直接崩盘。服务端定夺是反作弊的底线,客户端只负责抽卡动画等表现层。

为什么保底计数器要和结果同事务?
保底是玩家用真金白银积累的"资产"。计数器和抽卡结果不同步,就会出现白扣或重复保底,两者都是直接的资损/投诉。原子性是它作为"资产"的必然要求。

为什么别的选择不行

方案为什么不行
纯真随机、无保底长尾玩家连续不出 → 认定暗改概率 → 口碑崩 + 投诉 + 可能被指虚假宣传
客户端算概率/传随机种子玩家可预测或构造出货,商业化归零
保底计数器异步/延迟落库崩溃丢计数器 = 玩家白抽,或重复保底,直接资损投诉
扣费与发货分两个事务扣了没发(资损玩家)或发了没扣(资损公司),且重试重复发
软保底不校准综合概率公示 1.6% 实际算出来 3%(多送)或 0.8%(虚假宣传),两头都是事故
实际掉率与公示不符合规红线,可能面临下架/罚款

概率公示合规

公示的必须是玩家实际能体验到的综合概率(含保底、软保底后的等效出货率),不是那个 0.6% 的基础概率。上线前用蒙特卡洛模拟跑几百万次抽卡,验证"模拟出货率 ≈ 公示值",并把模拟脚本和结论留档备查。

沉淀结论

  • 带权抽样:别名法 O(1) 抽样(权重固定)或前缀和二分 O(log n)(权重常变,工程首选)。
  • 保底 pity:硬保底封顶承诺、软保底校准综合概率、大保底管 UP;计数器是玩家资产,必须与抽卡结果同事务持久化。
  • PRD:另一种降方差手段,适合暴击/小额掉落这类"要感觉稳定"的场景,可与保底叠加。
  • 随机源与掉率表只在服务端,客户端只做表现——反作弊底线。
  • 抽卡事务:扣费 + 发货 + 计数器更新原子完成,挂幂等键防重试重复发。
  • 概率合规:公示综合出货率(含保底),上线前蒙特卡洛验证并留档。

与其他专题的关系

  • 发货与扣费的原子性、防重试重复发 → 业务幂等性设计、业务代理 · 支付 · 商城。
  • 抽样底层的均匀随机与无放回抽样思想,可对照 game-infra 蓄水池抽样。

记忆口诀

  • 带权抽样:别名法 O(1)固定权重 / 前缀和二分 O(log n)常变 / 随机源只在服务端
  • 保底三兄弟:硬保底封顶 / 软保底校准综合率 / 大保底管 UP
  • 计数器:玩家资产 / 同事务持久化 / 丢一次=白抽几十次
  • 合规:公示综合出货率 / 蒙特卡洛验证 / 留档备查

内容来源

综合整理自卡池 / 掉落 / 商业化抽奖系统的实现经验;PRD 伪随机分布借鉴 Warcraft III / DOTA 暴击设计;发货幂等与原子兑换呼应本域 业务幂等性设计 与 业务代理 · 支付 · 商城。

自测:合上资料能说清楚吗?

  1. 数学上"公平的真随机"已经保证综合出货率等于公示值,为什么还要加保底和 PRD?
参考答案

真随机下连续不出在数学上正常,但玩家不接受概率论,只体验到"氪了几百块一个没出"。保底封顶最坏体验、PRD压小方差,在综合出货率仍等于公示值的前提下重新分配方差,让体验可控——不是改概率骗人。

  1. 硬保底、软保底、大保底三者分别解决什么问题?
参考答案

硬保底:90 抽必出,封顶兜底承诺。软保底:75 抽后概率递增,把综合出货率从基础 0.6% 拉到公示 ~1.6%。大保底:出金但歪了(非 UP)时标记,下次出金必 UP。

  1. 带权抽样的"别名法"和"前缀和+二分"如何取舍?
参考答案

别名法:预处理 O(n)、单次抽样 O(1),适合权重固定、抽样海量。前缀和+二分:单次 O(log n),建表简单、权重变动时重建便宜,工程最常用。权重常变选前缀和,权重固定且量大选别名法。

  1. 为什么保底计数器必须和抽卡结果在同一事务里持久化?
参考答案

计数器是玩家用真金白银积累的资产。不同步会出现"发了道具没清零"(重复保底投诉)或"计数器加了但抽卡回滚"(白扣抽数),两者都是直接资损/投诉。原子性是它作为资产的必然要求。

  1. 一次十连抽卡的服务端事务要保证哪些点?
参考答案

扣费+发货+计数器更新要么全成功要么全回滚;挂幂等键(十连一个 draw_id)防客户端重试重复发;随机源与掉率表只在服务端;货币扣减走统一原子兑换接口(消费+奖励一次原子完成)。

最近更新: 2026/9/10 11:38
Prev
排行榜 / 榜单
Next
邮件 / 发奖补偿