# Tetris 7-Bag: The I-Piece Drought Bound and How to Count a Drought

> The 7-bag caps I-piece gaps at 12; pure random and idealised NES rerolls do not. In the NES model, drought odds are 14.0% after an I or 12.5% for any 12-piece window.

Source: https://sheng.page/en/posts/tetris-7-bag-and-how-to-count-a-drought/ · Published: 2026-10-28 · Author: Sheng

All three randomisers average one I piece every 7 pieces in the long run. Their tails differ: pure random and the idealised NES reroll allow droughts of unbounded length, while the 7-bag puts at most 12 other pieces between consecutive I pieces.

Even within one model, a “12-piece drought” needs a starting point. In the idealised NES model, the probability is about 14.0% immediately after an I, or 12.5% for an arbitrary window of 12 pieces. The reroll makes another I less likely immediately after an I, so a drought is more likely from that starting point.

## Three randomisers

Pure random gives every piece an independent, uniform draw from the seven types. Each has probability `1/7`, regardless of what came before.

The [idealised NES reroll](https://gamertagmythras.com/blog/tetris/tetris-randomizers-explained) draws uniformly from 0 to 7, then rerolls once from 0 to 6 if the result is 7 or matches the previous piece. Accept the second result, including a repeat. An immediate repeat therefore has probability `(2/8) × (1/7) = 1/28`, down from `1/7` under pure random.

The 7-bag shuffles one of each piece into a bag and deals the whole bag before shuffling another. The [Random Generator](https://tetris.wiki/Bag) originated in *The New Tetris* on the N64, where the bag held 63 pieces, nine of each type. The seven-piece version later became the Tetris Guideline standard.

Other approaches include TGM's history rerolls and TGM3's 35-piece pool.

## Why the 7-bag caps the gap at 12

Put an I first in one bag and last in the next. Six non-I pieces finish the first bag, and six more precede the next I, giving a [maximum gap of 12](https://tetris.wiki/Bag). A longer gap would require a bag to lack an I.

With independent, uniform shuffles, each of those positions has probability `1/7`, so `P(gap = 12) = 1/49 ≈ 0.0204`. The simulation produces `0.0200`. To obtain the rest of the distribution, count how many pairs of I positions in adjacent bags produce each gap.

Each bag contains one S and one Z. The longest run crosses a bag boundary, joining the pair at the end of one bag to the pair at the start of the next, making four consecutive S/Z pieces.

## Drought quantiles under the three randomisers

A gap is the number of non-I pieces between consecutive I pieces. Each I starts a new count, recorded when the next I arrives. The prefix before the first I and any unfinished gap at the end are excluded.

Save this as `drought.py` to generate two million pieces per randomiser, with seed 42 set once at the start. In a game, create `gen = bag7()` once and call `next(gen)` when a piece is needed. Keep using the same `gen`: recreating it on every draw discards the remaining bag, just as recreating the NES generator discards its history. The value `0` represents I; the other values each represent another piece type.

```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
```

Every mean gap is close to 6. Count the I that ends the gap as well, and that is one I every 7 pieces. The NES estimate of `5.98` reflects this sample; its idealised model has a stationary `P(I)` of exactly `1/7`.

The p95 values mean at least 95% of complete gaps contain no more than 19 non-I pieces under pure random, 17 under the NES reroll, and 11 under the 7-bag. The observed maxima of 82 and 77 are sample results; the 7-bag's maximum of 12 reaches the bound imposed by its rule.

For pure random the gap is geometrically distributed, giving `P(gap ≥ 12) = (6/7)^12 ≈ 0.1573`, close to the measured `0.1569`. [The geometric distribution and drop-rate quantiles](/en/posts/drop-rates-and-the-geometric-distribution/) covers how to read those quantiles.

## Counting a drought: two events, two numbers

Gamertag Mythras gives about 12% for twelve pieces without an I and about 3% for twenty; the simulation above gives roughly 13.9% and 3.4%. A stretch without an I can be counted in either of two ways:

- **after-I**: an I has just appeared, and none of the next k pieces is an I.
- **window**: choose k consecutive pieces in the stationary sequence; none is an I.

The idealised NES rule reduces to a Markov chain with two states: the previous piece was I, or it was non-I. After an I, the initial draw rejects both I and 7, each with probability `1/8`. The reroll returns I with probability `1/7`, giving `P(I | previous I) = (2/8) × (1/7) = 1/28`.

After a non-I piece, I can arrive directly with probability `1/8`. A draw matching the previous piece or landing on 7 also allows I through the reroll. Hence `P(I | previous non-I) = 1/8 + (2/8) × (1/7) = 9/56`, leaving `47/56` for another non-I. Writing p for the stationary probability of I gives `p = p × (1/28) + (1-p) × (9/56)`. Solving gives `1/7`, so:

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

The first factor accounts for the starting condition. After an I, the next piece is non-I with probability `27/28`. An arbitrary window begins with a non-I with probability `6/7`. Once that first non-I has appeared, both events use the same continuation probability.

`nes_events.py` compares the closed forms with Monte Carlo runs of five million pieces for each of three seeds; each delta subtracts the exact value from the simulation estimate using the rounded output:

```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
```

For twelve pieces, the closed forms give about 14.0% after-I and 12.5% window. For twenty, they give 3.5% and 3.1%. The [approximate 12% and 3% figures](https://gamertagmythras.com/blog/tetris/tetris-randomizers-explained) are closer to the window results. That is an inference from the numbers; the article does not specify the starting condition. At twelve pieces, changing the starting condition changes the probability by about 1.6 percentage points within the same model.

| 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 |

Each seed supplies roughly 710,000 complete gaps, giving an approximate standard error of `0.0004` for the twelve-piece after-I estimate. Seed 42 is about `0.0015` below the closed form, roughly `3.7σ`; seeds 7 and 1 are within `1.5σ`.

## How the NES hardware rerolls

The [NES piece-selection code](https://meatfighter.com/nintendotetrisai/#Picking_Tetriminos) uses a 16-bit Fibonacci LFSR that advances at least once per frame. The first draw adds the updated spawn counter to its high byte and takes the result modulo 8. A 7 or a repeat triggers a reroll. This advances the LFSR and takes the new high byte modulo 8, then adds the previous piece's spawn orientation ID and reduces the sum modulo 7.

The reroll maps eight inputs onto seven piece types, so one type receives an extra outcome, determined by the previous spawn orientation ID. Meat Fighter's estimate, which assumes independent, uniformly distributed PRNG inputs and weights the previous piece types equally, gives 14.73% for T and 13.84% for I. With I slightly less common than `1/7`, the mean drought is slightly longer. The chance of a long drought also depends on the order of the pieces.

The NES simulation keeps only the previous piece and uses a uniform reroll. Reproducing the hardware also requires the spawn counter and LFSR state, with the LFSR advanced according to the frames elapsed between spawns.

## Same mean, different quantiles

Pure random has a mean gap of 6 and a p95 of 19; the 7-bag's p95 is 11. A player keeping a well open for an I still needs somewhere to put the other pieces. A longer wait uses up room that may already be scarce.

The 7-bag samples without replacement within each bag, with exactly one I available. Soft pity, where the chance rises after failures, is a different design, covered in [pity systems as state machines](/en/posts/pity-systems-as-state-machines/).

## Choosing a randomiser

If the rules require independent draws, use pure random and allow room for the occasional long wait. Where history may influence the next piece, choose the 7-bag for a firm waiting bound, accepting its more predictable bag endings; the idealised NES reroll suits a game that needs fewer immediate repeats and can tolerate uncapped droughts. To screen candidate implementations, feed each generator into `gaps(gen, n)` and compare its p95 and p99 with the waiting lengths the game can accommodate. Reject candidates whose tails exceed those requirements. For a candidate implementing the bag rule, a gap beyond its bound is an implementation failure.

My own design boundary for gambling games is to keep each round independent, without adjusting probabilities based on past results.

## Notes

- Screen generators against the waiting lengths the game can accommodate. Similar means can conceal very different tails.
- Test the bag boundary with I first in one bag and last in the next. For independent, uniform shuffles, use `P(gap = 12) = 1/49` to check how often that boundary case occurs.
- Match the event to the waiting situation: twelve pieces without an I mean 14.0% after-I or 12.5% in an arbitrary window under the idealised NES rule.
- Seed 42's twelve-piece after-I estimate was roughly `3.7σ` low. When investigating a discrepancy like this, check whether it persists across seeds before changing the model.
- For an NES reproduction, retain the spawn orientation ID alongside the LFSR state and account for the frames between spawns.

## Further reading

- Tetris Wiki, [Random Generator](https://tetris.wiki/Bag): a short entry covering the I-gap bound and S/Z limit, with the 63-piece origin in *The New Tetris*.
- Gamertag Mythras, [Tetris randomizers explained](https://gamertagmythras.com/blog/tetris/tetris-randomizers-explained): a survey of randomisers across games, supplying the idealised NES rule and the approximate 12%/3% figures compared here.
- Meat Fighter, [Picking Tetriminos](https://meatfighter.com/nintendotetrisai/#Picking_Tetriminos): the relevant section of a longer NES disassembly analysis, explaining the LFSR and piece-selection code, including the reroll bias.

> Ideas and technical judgement by Sheng; drafted with Claude · examples run on a Mac with Python 3.15.0.