Update: Looking for a chance to dive into C , I decided to incorporate this concept (including multple shuffle types) into a Python C Extension Module

I attend a local Python Users Group and had a fun task recently.  With a group of others we implemented a function that will shuffle a list imperfectly, attempting to simulate the imperfect nature of a human shuffling a deck of cards.

Considerations

Predictable behaviours that make a human shuffle imperfect

  • Once a deck of cards is divided to each hand, the amount of cards in each hand is not (likely) equal.
  • As each thumb releases from it's half of the deck, sometimes more than one card drops from a single hand at a time.
  • As the deck in each hand gets more and more thin, the probability of larger chucks of cards dropping from each hand increases.

Our approach (algorithm)

  1. Imperfectly divide the list (deck) in half with a set variance
  2. As long as either hand still has cards
    1. (Re-)Evaluate the currently selected hand ( allowing for it to potentially remain the same )
    2. Drop the card from the current hand
    3. append remaining cards from hand that still has cards

Implementation

@staticmethod
def humanoid_shuffle(items, num_shuffles=6):
    # how many times items can be pulled
    # from the same list consecutively
    MAX_STREAK = 10

    # divide list roughly in half
    num_items = len(items)
    margin_of_error = random.randint(0, int(.1 * num_items))
    end_range = int(num_items / 2 + margin_of_error)

    # list up to 0 - end_range
    first_half = items[:end_range]

    # list after end_range - len(items)
    second_half = items[end_range:]

    split_lists = (first_half, second_half)
    mixed = []
    streak = current_list_index = 0

    # while both lists still contain items
    while first_half and second_half:
        # calc the percentage of remaining total items
        remaining = (1 - float(len(mixed)) / num_items)

        # if we happen to generate a random value
        # less than the remaining percentage
        # which will be continually be decreasing
        #(along with the probability)
        # or
        # if MAX_STREAK is exceeded
        if random.random() < remaining or streak > MAX_STREAK:
            # switch which list is being used to pull items from
            current_list_index = 1 ^ current_list_index
            # reset streak counter
            streak = 0

            # pop the selected list onto the new (shuffled) list
            mixed.append(split_lists[current_list_index].pop())

            # increment streak of how many consecutive
            # times a list has remained selected
            streak += 1

    # add any remaining items
    mixed.extend(first_half)
    mixed.extend(second_half)

    num_shuffles -= 1
    # if we still have shuffles to do
    if num_shuffles:
        # rinse and repeat
        mixed = humanoid_shuffle(mixed, num_shuffles)

    # finally return fully shuffled list
    return mixed