Unbiased Random Permutation¶
An OOP card game model with a provably unbiased shuffle using Fisher-Yates.
Problem¶
Design classes for Card, CardDeck, and Player with standard operations (build, draw, shuffle, show hand). The shuffle must produce a uniform distribution over all permutations -- no bias toward any particular ordering.
Approach¶
The naive approach (swap each element with a random position from 0 to n-1) produces n^n outcomes mapped onto n! permutations, which cannot divide evenly. The result is a biased shuffle.
Fisher-Yates (Knuth) shuffle fixes this:
- Walk backwards from index n-1 to 1.
- At each index
i, pick a random indexjin[0, i]. - Swap
cards[i]andcards[j].
This generates exactly n! equally likely outcomes. To verify, the implementation includes an experiment that runs 600,000 shuffles on a small deck and checks that outcome frequencies are uniformly distributed.
Data structure choice: A list preserves insertion order (important for a hand of cards) and supports O(1) index access for the swap operation. A set would lose ordering.
Implementation¶
import random
from collections import namedtuple
from statistics import pstdev, mean
SUITS = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
VALUES = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'Jack', 'Queen', 'King']
Card = namedtuple('Card', ['suite', 'value'])
class DeckNotBuilt(Exception):
pass
class CardDeck:
def __init__(self, deck_size=52):
self.cards = []
self.deck_size = deck_size
def build(self):
self.cards = [Card(suite, value) for suite in SUITS
for value in VALUES[:self.deck_size // 4]]
return self.cards
def shuffle(self):
"""Fisher-Yates: O(n) in-place, uniform distribution."""
n = len(self.cards)
for i in range(n - 1, 0, -1):
j = random.randint(0, i)
self.cards[i], self.cards[j] = self.cards[j], self.cards[i]
def draw(self):
if not self.cards:
raise DeckNotBuilt("No cards in the deck")
return self.cards.pop()
class Player:
def __init__(self, name):
self.name = name
self.hand = []
def add_card_to_hand(self, card):
self.hand.append(card)
def show_hand(self):
for card in self.hand:
print(card)
def run_experiment(deck, num_shuffles):
"""Shuffle many times and record outcome frequencies."""
outcomes = {}
for _ in range(num_shuffles):
deck.shuffle()
deck_state = tuple(deck.cards)
outcomes[deck_state] = outcomes.get(deck_state, 0) + 1
return outcomes
def display_histogram(outcomes):
values = outcomes.values()
m = mean(values)
std = pstdev(values)
print(f"\nMean = {m:.2f}, Standard Deviation = {std:.2f}, Variance = {std**2:.2f}")
print("\n****************** Histogram *******************")
for outcome, frequency in sorted(outcomes.items(), key=lambda x: x[1]):
print(f"{outcome}: {'*' * (frequency // 500)}")
def main():
deck = CardDeck()
deck.build()
outcomes = run_experiment(deck, 600000)
print(f"Total unique shuffles recorded: {len(outcomes)}")
display_histogram(outcomes)
if __name__ == '__main__':
main()
Complexity¶
- Time: O(n) per shuffle -- single pass with constant-time swaps
- Space: O(1) extra -- shuffle is in-place
Takeaway¶
Fisher-Yates is the gold standard for unbiased permutation and shows up anywhere you need fair random sampling -- shuffling training data between epochs, bootstrap sampling, or generating random test splits. If your shuffle is biased, your model's evaluation is biased too.