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)
- Imperfectly divide the list (deck) in half with a set variance
- 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