# 保底不是把機率調高：用狀態機算抽卡保底與裝備強化

> 保底和強化退階讓每次抽樣都依賴歷史，要用狀態機和吸收型 Markov chain 才算得準。三套平均成本相近的規則，最倒楣的 1% 玩家差了三倍以上；再談計數器重置、跨池繼承與重送。

原文：https://sheng.page/posts/pity-systems-as-state-machines/ · 發布：2026-10-11 · 作者：Sheng

加了保底，平均抽數可能沒變多少，等待最久的玩家卻會差很多。固定 1.6%、硬保底 100 抽、軟保底逐抽加機率，三套規則平均都要 62 到 63 抽，最倒楣的 1% 玩家卻分別要 286、100、92 抽。機率一旦跟「已經失敗幾次」有關，就得把歷史放進狀態裡，才能算出等待次數的分布。

在〈[掉落率 1%，為什麼挖一百次還是沒有](/posts/drop-rates-and-the-geometric-distribution/)〉裡，挖了 200 次之後，下一次機率還是 1%，因為幾何分布沒有記憶性。保底刻意改掉這項條件，讓系統記住先前抽了多少次。

## 把「失敗幾次」當成狀態

把第 n 抽命中的機率寫成 `rate(n)`，n 表示距離上次命中已經抽了幾次。保底規則的狀態只有這一個整數，不需要模擬，直接往下推每一抽「到這裡還沒中」的機率就能算：

```text
alive(1) = 1
P(第 n 抽第一次中) = alive(n) · rate(n)
alive(n+1) = alive(n) · (1 - rate(n))
```

`rate(n)` 是常數時，這就是上一篇的幾何分布；到某一抽變成 1，就是硬保底；從某一抽開始遞增，就是一般說的軟保底。有了每一抽的機率，平均、中位數、P95 都能直接算出來。

## 《原神》的公告與社群模型

《原神》的祈願說明公告五星角色基礎機率 0.600%、綜合機率（含保底）1.600%，最多 90 抽內必定獲得。軟保底不在官方說明裡，社群維護的 Genshin Impact Wiki 寫明「soft pity」一詞是非官方的，存在與起點都是玩家實測得出，角色池大約從第 74 抽開始。社群常用的模型是從第 74 抽起每抽加 6%，這裡拿它來驗算：如果模型合理，算出來的綜合機率應該接近官方公告的 1.6%。

```python
import sys


def first_hit_dist(rate_at):
    """rate_at(n) = chance that pull n hits, given pulls 1..n-1 all missed.
    Returns P(first hit happens on pull n) for n = 1..N (state = misses so far)."""
    dist, alive, n = [], 1.0, 1
    while alive > 1e-12:
        r = min(1.0, rate_at(n))
        dist.append(alive * r)
        alive *= 1 - r
        n += 1
    return dist


def summary(dist):
    mean = sum((i + 1) * p for i, p in enumerate(dist))
    cdf, qs, out = 0.0, [0.5, 0.9, 0.95, 0.99], {}
    for i, p in enumerate(dist):
        cdf += p
        while qs and cdf >= qs[0] - 1e-12:
            out[qs.pop(0)] = i + 1
    return mean, out, len(dist)


print("== 1. a public example: 0.6% base, guaranteed by pull 90 ==")
print("   soft pity (+6% per pull from pull 74) is a community estimate, not official")
base = 0.006
hard_only = first_hit_dist(lambda n: 1.0 if n >= 90 else base)
soft = first_hit_dist(lambda n: 1.0 if n >= 90 else base + max(0, n - 73) * 0.06)
for label, d in (("hard pity only", hard_only), ("soft + hard pity", soft)):
    mean, q, worst = summary(d)
    print(f"{label:17} mean {mean:5.2f} pulls -> consolidated rate {1/mean*100:.3f}%"
          f"  median {q[0.5]}  P90 {q[0.9]}  P99 {q[0.99]}  worst {worst}")
```

```text
== 1. a public example: 0.6% base, guaranteed by pull 90 ==
   soft pity (+6% per pull from pull 74) is a community estimate, not official
hard pity only    mean 69.70 pulls -> consolidated rate 1.435%  median 90  P90 90  P99 90  worst 90
soft + hard pity  mean 62.30 pulls -> consolidated rate 1.605%  median 76  P90 80  P99 83  worst 90
```

只有 0.6% 加上第 90 抽硬保底，綜合機率是 1.435%，跟公告的 1.6% 對不上；加上社群估計的軟保底，算出 1.605%，才與公告數字相容。演算法本身沒有公開，模型對不對還是需驗證。

「只有硬保底」那一列的中位數是 90，因為在 0.6% 的機率下，89 抽都沒中的機率約 58.5%，超過一半的玩家得靠保底取得，保底也就成了主要的取得方式。0.6% 的基礎機率只讓約四成的人提早拿到。

## 平均相近，長尾差很多

回到太空採礦遊戲，替稀有鑽頭設計三套虛構規則，沿用剛才的算法比較。這些範例在 Python 3.13.16 上跑過：

```python
print("\n== 2. three made-up rules for one rare drill head ==")
rules = {
    "flat 1.6%, no pity": (lambda n: 0.016, False),
    "1.0%, hard pity at 100": (lambda n: 1.0 if n >= 100 else 0.010, True),
    "0.8%, +5%/pull from pull 82": (lambda n: 0.008 + max(0, n - 81) * 0.05, True),
}
for name, (f, capped) in rules.items():
    mean, q, worst = summary(first_hit_dist(f))
    worst_s = str(worst) if capped else "none"
    print(f"{name:30} mean {mean:5.1f}  median {q[0.5]:3d}  P95 {q[0.95]:3d}"
          f"  P99 {q[0.99]:3d}  worst {worst_s}")
```

```text
== 2. three made-up rules for one rare drill head ==
flat 1.6%, no pity             mean  62.5  median  43  P95 186  P99 286  worst none
1.0%, hard pity at 100         mean  63.4  median  69  P95 100  P99 100  worst 100
0.8%, +5%/pull from pull 82    mean  62.5  median  82  P95  90  P99  92  worst 101
```

平均成本差不到一抽，分位數卻差很多：

- 固定 1.6% 的中位數最低，一半的人 43 抽內就拿到，但 P99 要 286 抽，規則裡也沒有上限。
- 硬保底 100 抽的中位數 69，P95 和 P99 都是 100，最差的情況直接寫在規則裡。
- 軟保底的中位數最高，約一半的人在前 81 抽就拿到，其餘的人集中在 82 到 92 抽之間，P95 和 P99 只差兩抽，長尾最短。

如果公告只寫「綜合機率約 1.6%」，三套規則都說得通，玩家卻看不出長尾的差別，最好把保底規則也一併寫明。

## 強化退階：吸收型 Markov chain

裝備強化失敗時，可能留在原級，也可能退一級，狀態就比保底複雜。假設從 +0 強化到 +10，每一級的成功率依序是 95%、90%、85% 一路降到 15%；+5 以上失敗會掉一級。這時候狀態是「現在幾級」，+10 是吸收態（到了就不會離開），其他是暫態。

吸收型 Markov chain 有現成的公式：把暫態之間的轉移寫成矩陣 Q，基本矩陣 `N = (I - Q)^-1`，從各狀態出發到被吸收的期望步數是 `t = N·1`。實作上不必真的求反矩陣，解 `(I - Q)·t = 1` 這個線性方程組就好：

```python
def solve(a, b):
    """Gaussian elimination, small dense systems only."""
    n = len(a)
    m = [row[:] + [b[i]] for i, row in enumerate(a)]
    for c in range(n):
        piv = max(range(c, n), key=lambda r: abs(m[r][c]))
        m[c], m[piv] = m[piv], m[c]
        for r in range(n):
            if r != c and m[r][c]:
                f = m[r][c] / m[c][c]
                m[r] = [x - f * y for x, y in zip(m[r], m[c])]
    return [m[i][n] / m[i][i] for i in range(n)]


print("\n== 3. upgrade +0 to +10 as an absorbing Markov chain ==")
SUCCESS = [0.95, 0.9, 0.85, 0.75, 0.65, 0.55, 0.45, 0.35, 0.25, 0.15]  # from level i to i+1


def expected_tries(drop_from, protect=False):
    """Fail at level >= drop_from loses one level unless protected.
    Solve E[i] = 1 + s*E[i+1] + (1-s)*E[fail_to], with E[10] = 0, i.e. t = (I - Q)^-1 1."""
    n = len(SUCCESS)
    a = [[0.0] * n for _ in range(n)]
    for i, s in enumerate(SUCCESS):
        a[i][i] += 1.0
        if i + 1 < n:
            a[i][i + 1] -= s
        fail_to = i - 1 if (i >= drop_from and not protect) else i
        a[i][fail_to] -= 1 - s
    return solve(a, [1.0] * n)


for label, kw in (("never drops", dict(drop_from=99)),
                  ("drops one level from +5", dict(drop_from=5)),
                  ("drops from +5, protected", dict(drop_from=5, protect=True))):
    e = expected_tries(**kw)
    print(f"{label:25} expected tries +0->+10: {e[0]:7.1f}   +7->+10: {e[7]:6.1f}")
scrolls = sum((1 - s) / s for s in SUCCESS[5:])
print(f"protection scrolls used on average: {scrolls:.1f}")

import random
rng = random.Random(10)
def one_player(drop_from=5):
    lvl = tries = 0
    while lvl < 10:
        tries += 1
        if rng.random() < SUCCESS[lvl]:
            lvl += 1
        elif lvl >= drop_from:
            lvl -= 1
    return tries
runs = sorted(one_player() for _ in range(20_000))
print(f"20,000 players, drops from +5: mean {sum(runs)/len(runs):.1f}  median {runs[10_000]}"
      f"  P95 {runs[19_000]}  P99 {runs[19_800]}  worst {runs[-1]}")
print(f"\nPython {sys.version.split()[0]}")
```

```text
== 3. upgrade +0 to +10 as an absorbing Markov chain ==
never drops               expected tries +0->+10:    23.8   +7->+10:   13.5
drops one level from +5   expected tries +0->+10:   341.9   +7->+10:  326.7
drops from +5, protected  expected tries +0->+10:    23.8   +7->+10:   13.5
protection scrolls used on average: 12.6
20,000 players, drops from +5: mean 340.0  median 241  P95 979  P99 1518  worst 3443

Python 3.13.16
```

成功率完全一樣，只是加上「+5 以上失敗退一級」，期望次數從 23.8 次變成 341.9 次，約是原來的 14 倍。從 +7 出發要 326.7 次，跟從 +0 出發差不多，因為高等級一失敗就往回掉，前面累積的進度保不住。兩萬個模擬玩家的平均 340.0，和公式的 341.9 吻合；中位數 241，P99 卻是 1,518 次。

保護道具取消了退階，強化次數回到 23.8，平均要用掉 12.6 張保護卷。商城定價時，可以把保護卷價格乘上 12.6，與「不用保護卷多強化 318 次」的材料成本比較，用來估計玩家會選哪一邊、願意付多少。

這裡的鏈只有十個狀態，用高斯消去法就夠了。狀態再多（例如強化等級乘上保底計數），一樣是解線性方程組，只是矩陣變大，可以改用稀疏矩陣的解法。

## 權重變更與機率公告

狀態和權重藏在伺服器裡，玩家只能看到結果。韓國公平交易委員會 2024 年 1 月 3 日公布對 Nexon 的處分，針對《新楓之谷》的方塊道具開罰 116 億韓元，依《電子商務消費者保護法》認定其以錯誤資訊招攬購買。根據 TechRadar 與 KED Global 的報導，方塊在 2010 年 5 月推出時各選項機率相同，同年 9 月改成部分選項較少出現，2011 年 8 月到 2021 年 3 月間某些高人氣選項的機率是 0；Nexon 在 2011 年 8 月還發過公告表示方塊的結構沒有變動。Inven Global 報導 Nexon 已提起上訴。

這個案例涉及權重表變更，和 RNG 認證無關，亂數本身可能完全正常。公告要從正式環境的設定產生，每次變更都留下紀錄，才能讓玩家看到的說明跟系統實際狀態一致，上一篇的掉落表也有相同的要求。

## 保底計數器的重置與保存

保底計數器是玩家的資產，處理起來得比一般遊戲狀態嚴格。命中後就歸零，或等命中「指定目標」才歸零，會得到不同的分布，規格和公告都要寫清楚。

活動池結束後，計數器是否帶到下一池，也會影響玩家要不要現在抽，改變繼承規則就等於改變玩家已經累積的資產。

客戶端逾時重送同一筆抽卡請求時，伺服器不能重複扣款，也不能讓保底計數多加一次。請求要帶冪等鍵，扣款、抽樣、更新計數器在同一個交易裡完成，做法跟〈[同一筆付款送了兩次：從 Santander 的聖誕節講冪等與重送](/posts/idempotency-and-retries/)〉相同。

存放位置也要考慮：計數器放在客戶端，玩家就能改；放在快取裡，則得確認不會因過期或重啟而歸零。

## 補充筆記

- 把「已經失敗幾次」當成狀態，就能逐抽算出保底的精確分布。平均成本相近，P99 仍可能差三倍以上，得另外看長尾。
- 0.6% 加 90 抽硬保底算出 1.435%，加上社群估計的軟保底則是 1.605%，與公告的 1.6% 相容；演算法尚未公開，不能據此確認模型。
- 強化退階可以用吸收型 Markov chain 計算，解 `(I - Q)·t = 1` 得到期望次數。本例加入退階後，期望次數由 23.8 增至 341.9，保護道具的價值也從這個差距估算。
- Nexon 的處分涉及權重變更卻未告知玩家，機率結構每次改動都要留紀錄並反映在公告上。保底計數器的重置、跨池繼承、重送與存放位置也要明訂，重送尤其不能重複扣款或多加計數。

## 延伸閱讀

- Genshin Impact Wiki〈[Wish](https://genshin-impact.fandom.com/wiki/Wish)〉：轉載遊戲內祈願說明的機率與 90 抽保底，並註明軟保底是玩家實測得出，社群維護的維基，機率以遊戲內公告為準。
- Wikipedia〈[Absorbing Markov chain](https://en.wikipedia.org/wiki/Absorbing_Markov_chain)〉：標準形式、基本矩陣 `N = (I - Q)^-1` 與期望步數 `t = N1`，百科條目，用來對照公式。
- Wikipedia〈[Gacha game](https://en.wikipedia.org/wiki/Gacha_game)〉：軟保底、硬保底與 sparking 的定義，以及日本 2012 年禁止 complete gacha 等監管沿革，百科條目。
- Korea JoongAng Daily〈[Antitrust agency slaps Nexon with $8.9 million fine for misleading users](https://www.koreajoongangdaily.com/business/antitrust-agency-slaps-nexon-with-89-million-fine-for-misleading-users/11374472)〉：2024-01-03 處分的金額與法律依據，新聞報導。
- TechRadar〈[Nexon fined almost $9 million](https://www.techradar.com/gaming/nexon-fined-almost-dollar9-million-for-allegedly-changing-probability-structure-of-certain-items-in-maplestory-without-informing-players)〉與 KED Global〈[Nexon fined over MapleStory in-game item selling](https://www.kedglobal.com/newsView/ked202401030012)〉：方塊機率結構變動的時間線，新聞報導。
- Inven Global〈[Nexon's MapleStory Appeals Fine](https://www.invenglobal.com/articles/20319/nexons-maplestory-appeals-fine-as-seoul-high-court-resumes-hearing-in-april)〉：上訴進度，新聞報導，判決結果以法院公告為準。

> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 範例在 Python 3.13.16 實測。