Build Tetris using React Native and Expo

Lesson 7 of 8

Next Piece Preview and the 7-Bag

A plain Math.random() piece picker is technically fair over a long enough game, but in the short term it can feel unfair, streaks of the same piece, or an agonizingly long drought without the I-piece you need. Modern Tetris games solve this with the 7-bag randomizer, and show players what's coming next.

The 7-bag algorithm

function shuffle<T>(items: T[]): T[] {
  const copy = [...items];
  for (let i = copy.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [copy[i], copy[j]] = [copy[j], copy[i]];
  }
  return copy;
}

function createBag(): string[] {
  return shuffle(['I', 'O', 'T', 'S', 'Z', 'J', 'L']);
}

The idea: shuffle all 7 shapes into a random order, a "bag", and deal pieces out of it one at a time. Once the bag is empty, shuffle a fresh set of all 7 and keep going. This guarantees you see every shape exactly once every 7 pieces, no droughts, no unfair streaks, while each individual bag's order is still genuinely random.

shuffle itself is the Fisher-Yates shuffle, the standard unbiased way to randomly order a list: walk backwards from the end, and for each position, swap it with a random earlier-or-equal position. [copy[i], copy[j]] = [copy[j], copy[i]] is a destructuring swap, JavaScript's idiom for exchanging two variables (or array slots) without a temporary variable.

Managing the piece queue

function usePieceQueue() {
  const [queue, setQueue] = useState<string[]>(() => [...createBag(), ...createBag()]);

  function takeNext(): string {
    const [next, ...rest] = queue;
    const refilled = rest.length <= 7 ? [...rest, ...createBag()] : rest;
    setQueue(refilled);
    return next;
  }

  return { next: queue[0], takeNext };
}

Starting with two bags concatenated together ([...createBag(), ...createBag()]) guarantees there's always at least one full bag's worth of pieces ahead to peek at for a "Next" preview, even right after a refill. takeNext removes the front piece, tops the queue back up with a fresh bag once it runs low, and returns the piece that's now active.

The "Next" preview

function NextPreview({ shapeName }: { shapeName: string }) {
  const shape = SHAPES[shapeName];
  return (
    <View style={styles.preview}>
      {shape.map((row, r) => (
        <View key={r} style={{ flexDirection: 'row' }}>
          {row.map((cell, c) => (
            <View key={c} style={[styles.miniCell, cell !== 0 && styles.filled]} />
          ))}
        </View>
      ))}
    </View>
  );
}

This reuses the exact same rendering pattern as the main Board component, a shape matrix is really just a tiny board, so the same nested-View-rows approach works for both.

NOTE

queue[0] (peeking without removing) is what the NextPreview component should render, only takeNext() (called from the tick handler when spawning) actually advances the queue.

📝 7-Bag and Next Piece Quiz

Passing score: 70%
  1. 1.What problem does the 7-bag randomizer solve compared to plain Math.random() piece selection?

  2. 2.The shuffle() function shown is the Fisher-Yates shuffle, the standard unbiased way to randomly order a list.

  3. 3.[copy[i], copy[j]] = [copy[j], copy[i]] is a ____ swap, exchanging two array slots without a temporary variable.