# Tetris 7-bag：I 方塊乾旱的 12 顆上界與乾旱機率怎麼數

> 7-bag 把兩個 I 方塊之間的間隔限制在 12 顆內，純隨機與理想化 NES 重抽都沒有上限。理想化 NES 模型剛出 I 後，12 顆沒有 I 的機率約 14.0%，任取連續 12 顆則約 12.5%。

原文：https://sheng.page/posts/tetris-7-bag-and-how-to-count-a-drought/ · 發布：2026-10-28 · 作者：Sheng

三種發牌器長期平均都是每 7 顆一個 I，差別在等得最久時會遇到什麼：純隨機與理想化 NES 重抽都沒有乾旱上限，7-bag 則保證兩個 I 之間最多隔 12 顆。

連「12 顆沒有 I」也得先說從哪裡開始數，理想化 NES 模型剛出 I 後往下數，機率約 14.0%，任取連續 12 顆則約 12.5%。重抽規則會壓低剛出 I 後再出 I 的機率，從這個起點往下數，遇到乾旱的機率較高。

## 三種發牌器

純隨機最容易定義，每顆獨立抽一次，七種方塊各有 `1/7` 機率，上一顆出了什麼不影響下一顆。

[理想化 NES 重抽](https://gamertagmythras.com/blog/tetris/tetris-randomizers-explained)先均勻擲出 0 到 7，遇到 7 或與上一顆相同，就在 0 到 6 之間重擲一次，這次照單接受。因此連續重複的機率是 `(2/8) × (1/7) = 1/28`，比純隨機的 `1/7` 低，仍然可能連續拿到同一種方塊。

7-bag 每袋放七種方塊各一顆，洗好後依序發出，整袋發完才洗下一袋。[Random Generator](https://tetris.wiki/Bag) 最早為 N64《The New Tetris》開發，當時一袋有 63 顆，每種各 9 顆，後來七顆版本成為 Tetris Guideline 的標準。

其他設計還有 TGM 的歷史重抽與 TGM3 的 35 顆池。

## 7-bag 的 12 顆上界

把 I 放在前一袋第一顆、下一袋最後一顆，中間會隔著前一袋剩下的 6 顆，加上下一袋開頭的 6 顆，合計 [12 顆非 I](https://tetris.wiki/Bag)。每袋都有 I，間隔再長就違反規則。

每袋獨立均勻洗牌時，前一袋 I 在袋首的機率是 `1/7`，下一袋 I 在袋尾也是 `1/7`，所以 `P(gap = 12) = 1/49 ≈ 0.0204`，模擬量到 `0.0200`。列舉相鄰兩袋內 I 的位置，數出每種 gap 對應多少組位置，就能得到完整分布。

S／Z 連續出現也最多 4 顆，因為每袋只有 S、Z 各一顆，最長的連段會接在前一袋袋尾與下一袋袋首。

## 三種發牌器的乾旱分位數

gap 是「兩個 I 之間的非 I 顆數」，每次出 I 就重新開始數，等下一個 I 出現才記下一筆，開頭與結尾不完整的間隔不收進統計。

把程式存成 `drought.py`，三種發牌器各產生 200 萬顆，seed 42 只在程式開頭設定一次。接進遊戲時，用 `gen = bag7()` 建立發牌器，每次出塊呼叫 `next(gen)`，持續使用同一個 `gen`；每顆都重建會丟掉尚未發完的袋子，NES 重抽也會丟掉上一顆的紀錄。回傳值 `0` 代表 I，其餘值各對應一種方塊。

```python
# Gap between consecutive I pieces under three randomisers (2,000,000 pieces each, seed 42).
#   pure random: each piece uniform over 7, independent
#   NES (idealised): roll 0..7; if 7 or equal to the previous piece, roll again 0..6 uniformly
#                    (the real NES reroll is biased, see meatfighter.com; this is the textbook model)
#   7-bag: shuffle all seven pieces, deal them, repeat
# "gap" = number of non-I pieces between one I and the next, measured right after an I.
import random

random.seed(42)
N = 2_000_000
I = 0

def pure():
    while True:
        yield random.randrange(7)

def nes():
    prev = 7
    while True:
        r = random.randrange(8)
        if r == 7 or r == prev:
            r = random.randrange(7)
        prev = r
        yield r

def bag7():
    while True:
        bag = list(range(7))
        random.shuffle(bag)
        yield from bag

def gaps(gen, n):
    out, gap = [], None
    for _ in range(n):
        if next(gen) == I:
            if gap is not None:
                out.append(gap)
            gap = 0
        elif gap is not None:
            gap += 1
    return out

print(f"{'randomiser':12s} {'mean gap':>8} {'p95':>4} {'p99':>4} {'max':>4} {'P(gap>=12)':>11} {'P(gap>=20)':>11}")
for name, gen in (("pure random", pure()), ("NES reroll", nes()), ("7-bag", bag7())):
    g = sorted(gaps(gen, N))
    n = len(g)
    print(f"{name:12s} {sum(g) / n:8.2f} {g[int(.95 * n)]:4d} {g[int(.99 * n)]:4d} {g[-1]:4d} {sum(x >= 12 for x in g) / n:11.4f} {sum(x >= 20 for x in g) / n:11.4f}")
```

```text
randomiser   mean gap  p95  p99  max  P(gap>=12)  P(gap>=20)
pure random      5.99   19   29   82      0.1569      0.0453
NES reroll       5.98   17   26   77      0.1385      0.0341
7-bag            6.00   11   12   12      0.0200      0.0000
```

平均 gap 都接近 6，也就是把下一個 I 算進去，平均每 7 顆出現一次。NES 那列的 `5.98` 是這次抽樣結果，理想模型的穩態 `P(I)` 恰為 `1/7`。

p95 表示至少 95% 的完整 gap 不超過這個長度，純隨機是 19 顆非 I，NES 重抽是 17 顆，7-bag 是 11 顆。純隨機這次最長等了 82 顆非 I，NES 是 77，兩者都是抽樣最大值；7-bag 的 max 12 則碰到規則上界。

純隨機的 gap 服從幾何分布，`P(gap ≥ 12) = (6/7)^12 ≈ 0.1573`，與實測 `0.1569` 接近。幾何分布的分位數怎麼讀，見〈[掉落率 1%，為什麼挖一百次還是沒有](/posts/drop-rates-and-the-geometric-distribution/)〉。

## 乾旱怎麼數：兩個事件、兩個數字

Gamertag Mythras 的文章把接下來 12 顆沒有 I 寫成約 12%，模擬卻是約 13.9%；20 顆則是約 3% 與 3.4%。「沒有 I」可以數兩種事件：

- **after-I**：剛出現一個 I，接下來 k 顆都沒有 I。
- **window**：在穩態序列中任取連續 k 顆，這一段沒有 I。

理想化 NES 只要用「上一顆是 I」與「上一顆非 I」兩個狀態，就能算出這兩種機率。上一顆是 I 時，第一擲遇到 I 或 7 都要重抽，各占 `1/8`，重抽再拿到 I 的機率是 `1/7`，所以 `P(I | 上一顆 I) = (2/8) × (1/7) = 1/28`。

上一顆非 I 時，第一擲直接出 I 有 `1/8` 機率，另外還有「擲到上一顆」與「擲到 7」兩條重抽路徑，因此 `P(I | 上一顆非 I) = 1/8 + (2/8) × (1/7) = 9/56`。下一顆繼續非 I 的機率是 `47/56`。把穩態下出 I 的機率記作 p，它滿足 `p = p × (1/28) + (1-p) × (9/56)`，解出 `P(I) = 1/7`，兩個事件可寫成：

```text
after-I(k) = (27/28) × (47/56)^(k-1)
window(k)  = (6/7)   × (47/56)^(k-1)
```

after-I 已知起點剛出過 I，第一步沒有 I 的機率是 `27/28`；window 的第一顆沒有任何這種條件，在穩態下非 I 的機率是 `6/7`。後面都已經處於非 I 狀態，所以剩下的乘數相同。

`nes_events.py` 用三個 seed 各跑 500 萬顆，以 Monte Carlo 對照閉式解，delta 是輸出四捨五入後的模擬值減去閉式解：

```python
# Two different "drought" events under the idealised NES model, same 12 and 20 piece cut-offs:
#   after-I : an I has just appeared and the next k pieces contain no I
#   window  : pick any k consecutive pieces in the steady state; none is an I
# Exact values come from the two-state Markov chain: P(I | previous I) = 1/28,
# P(I | previous non-I) = 1/8 + 2/8 * 1/7 = 9/56, stationary P(I) = 1/7.
#   after-I : (27/28) * (47/56)^(k-1)
#   window  : (6/7)   * (47/56)^(k-1)
# Monte Carlo is run with several seeds so a single unlucky seed is visible.
import random

N = 5_000_000
P_II, P_IN = 1 / 28, 9 / 56

def simulate(seed):
    random.seed(seed)
    prev, seq = 7, []
    for _ in range(N):
        r = random.randrange(8)
        if r == 7 or r == prev:
            r = random.randrange(7)
        prev = r
        seq.append(r == 0)
    return seq

def after_i(seq, k):
    gaps, gap = [], None
    for is_i in seq:
        if is_i:
            if gap is not None:
                gaps.append(gap)
            gap = 0
        elif gap is not None:
            gap += 1
    return sum(g >= k for g in gaps) / len(gaps)

def window(seq, k):
    run = hits = total = 0
    for i, is_i in enumerate(seq):
        run = 0 if is_i else run + 1
        if i >= k - 1:
            total += 1
            hits += run >= k
    return hits / total

for k in (12, 20):
    print(f"k={k}: exact after-I={(1 - P_II) * (1 - P_IN) ** (k - 1):.5f}   exact window={(6 / 7) * (1 - P_IN) ** (k - 1):.5f}")
for seed in (42, 7, 1):
    seq = simulate(seed)
    print(f"  seed {seed:2d}: " + "   ".join(f"k={k}: after-I={after_i(seq, k):.5f} window={window(seq, k):.5f}" for k in (12, 20)))
```

```text
k=12: exact after-I=0.14035   exact window=0.12476
k=20: exact after-I=0.03455   exact window=0.03071
  seed 42: k=12: after-I=0.13885 window=0.12351   k=20: after-I=0.03397 window=0.03028
  seed  7: k=12: after-I=0.13975 window=0.12380   k=20: after-I=0.03409 window=0.03035
  seed  1: k=12: after-I=0.14071 window=0.12464   k=20: after-I=0.03433 window=0.03058
```

閉式解算出的 12 顆 after-I 約 14.0%，window 約 12.5%；20 顆則分別約 3.5% 與 3.1%。[約 12% 與 3% 的數字](https://gamertagmythras.com/blog/tetris/tetris-randomizers-explained)接近 window 的結果，這是依數值做的對照，原文沒有明確交代起算條件。同一模型光是換個起點，12 顆那項就差約 1.6 個百分點。

| seed | after-I，k = 12 | window，k = 12 | after-I，k = 20 | window，k = 20 |
| --- | ---: | ---: | ---: | ---: |
| 42 | -0.00150 | -0.00125 | -0.00058 | -0.00043 |
| 7 | -0.00060 | -0.00096 | -0.00046 | -0.00036 |
| 1 | +0.00036 | -0.00012 | -0.00022 | -0.00013 |

每個 seed 約有 71 萬個完整 gap，k = 12 的 after-I 比例標準誤約 `0.0004`。seed 42 低了約 `0.0015`，約為 `3.7σ`，seed 7 與 1 則都在 `1.5σ` 內。

## 真機 NES 的重抽流程

[NES Tetris 的選塊流程](https://meatfighter.com/nintendotetrisai/#Picking_Tetriminos)用 16-bit Fibonacci LFSR，每幀至少步進一次。第一擲將 LFSR 高位元組與更新後的 spawn counter 相加，取 mod 8，遇到 7 或與上一顆相同就重抽；重抽會先推進 LFSR，再將新高位元組取 mod 8，加上上一顆生成時的 orientation ID，最後取 mod 7。

重抽把八種輸入映成七種方塊，總有兩個輸入得到同一個結果，orientation ID 決定哪種方塊多拿到這份機率。Meat Fighter 把 PRNG 輸入視為獨立均勻，並對前一顆方塊採等權近似，估算 T 約占 14.73%，I 約占 13.84%，I 比理想模型的 `1/7` 少一點，平均乾旱也會略長；長乾旱的機率還取決於出塊順序。

前面的 NES 模擬只保留上一顆方塊，並採均勻重抽；要重現真機，還得保留 LFSR 與 spawn counter，按兩次出塊之間經過的幀數推進狀態。

## 平均一樣、分位數不一樣

純隨機的平均 gap 是 6，p95 是 19；7-bag 的 p95 是 11。玩家留著直井等 I 清行時，還得有空間安置接下來的其他方塊，等得越久，能安置其他方塊的空間就越吃緊。

7-bag 是每袋七種各一顆的無放回抽樣，與失敗越多、命中機率越高的軟保底屬於不同設計，後者見〈[用狀態機算抽卡保底與裝備強化](/posts/pity-systems-as-state-machines/)〉。

## 怎麼選發牌器

規則要求每次出塊獨立，就用純隨機，遊戲也得容得下偶爾很長的等待。允許依歷史調整分布時，需要等待上限就選 7-bag，代價是袋尾較容易預測；只想減少連續重複，又能接受乾旱沒有上限，可以選理想化 NES 重抽。篩選候選實作時，把發牌器接進 `gaps(gen, n)`，用完整 gap 的 p95、p99 對照遊戲能接受的等待長度，超出需求就淘汰該候選；若候選聲稱符合袋子規則，任何 gap 超過上界都應直接判定實作有誤。

我自己的設計紅線是博弈類不照歷史調機率，每局獨立。

## 補充筆記

- 篩選發牌器時，把完整 gap 的分位數接成淘汰條件，平均值接近仍可能等得太久。
- 測袋子邊界時，把 I 放在前一袋袋首與下一袋袋尾，檢查 gap 是否為 12；獨立均勻洗牌時，這種 gap 的出現比例應接近 `1/49`。
- 比較乾旱機率前先固定起點，理想化 NES 的 12 顆 after-I 約 14.0%，window 約 12.5%，要依玩家實際等待的情境選擇。
- k = 12 的 after-I 在 seed 42 低了約 `3.7σ`，查這種偏差時，先看它換 seed 後是否仍持續出現。
- 做 NES 重現工具時，把生成時的 orientation ID 與 LFSR 狀態一起保留下來，出塊間隔的幀數也要算進去。

## 延伸閱讀

- Tetris Wiki〈[Random Generator](https://tetris.wiki/Bag)〉：7-bag 的 12 顆間隔上限與 S／Z 連續上限，也記錄《The New Tetris》的 63 顆袋起源，篇幅短。
- Gamertag Mythras〈[Tetris randomizers explained](https://gamertagmythras.com/blog/tetris/tetris-randomizers-explained)〉：各代發牌器整理，本文依該文描述建立理想化 NES 模型，並對照其約 12%／3% 的說法。
- Meat Fighter〈[Picking Tetriminos](https://meatfighter.com/nintendotetrisai/#Picking_Tetriminos)〉：長篇 NES Tetris 反組譯分析中的一節，說明 LFSR 與實際選塊流程，以及重抽為何有偏差。

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