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.