#!/usr/bin/env python import multiprocessing import random import sys LAND = 1 SPELL = 2 def riffle(deck): """Separate into two piles within 20 cards of each other's size, then interleave them. Return shuffled version """ cut = random.randint(-7, 7) + (len(deck) / 2) p1 = deck[:cut] p2 = deck[cut:] newdeck = [] while p1 and p2: newdeck.append(p1[0]) newdeck.append(p2[0]) p1 = p1[1:] p2 = p2[1:] if p1: newdeck.extend(p1) if p2: newdeck.extend(p2) return newdeck def imperfect_riffle(deck): """As above, but randomly choose between 1 and 5 cards that drop on each side's riffle """ cut = random.randint(-7, 7) + (len(deck) / 2) p1 = deck[:cut] p2 = deck[cut:] newdeck = [] while p1 and p2: p1_drops = random.randint(1, 4) p2_drops = random.randint(1, 4) newdeck.extend(p1[:p1_drops]) newdeck.extend(p2[:p2_drops]) p1 = p1[p1_drops:] p2 = p2[p2_drops:] if p1: newdeck.extend(p1) if p2: newdeck.extend(p2) return newdeck def thorough_riffle(deck): """Riffle 7 times, more than anyone ever riffle shuffles. """ return riffle(riffle(riffle(riffle(riffle(riffle(riffle(deck))))))) def good_riffle(deck): """Riffle 4 times, what I usually see thorough people do""" return riffle(riffle(riffle(riffle(deck)))) def bad_riffle(deck): """Riffle twice because you're in a hurry.""" return riffle(riffle(deck)) def thorough_imperfect_riffle(deck): """Riffle 7 times, more than anyone ever riffle shuffles. """ return imperfect_riffle(imperfect_riffle(imperfect_riffle( imperfect_riffle(imperfect_riffle(imperfect_riffle( imperfect_riffle(deck))))))) def good_imperfect_riffle(deck): """Riffle 4 times, what I usually see thorough people do""" return imperfect_riffle(imperfect_riffle(imperfect_riffle( imperfect_riffle(deck)))) def bad_imperfect_riffle(deck): """Riffle twice because you're in a hurry.""" return imperfect_riffle(imperfect_riffle(deck)) def single_pile(deck, n): """Pile shuffle deck into n piles, returns shuffled version """ piles = [] while True: for i in range(n): if not deck: break if len(piles) - 1 < i: piles.append([]) piles[i].append(deck.pop()) else: continue break newdeck = [] [ newdeck.extend(pile) for pile in piles ] return newdeck def pile_11(deck): return single_pile(deck, 11) def pile_7(deck): return single_pile(deck, 7) def fullpile(deck): nd = pile_11(deck) nd = imperfect_riffle(nd[:]) nd = pile_7(nd[:]) nd = imperfect_riffle(nd[:]) return nd def pseudorandom(deck): random.shuffle(deck) return deck def manascrewed(deck): """Count the number of land in the first 10 cards. If it's less than 3, count that as screwed. """ return deck[:10].count(LAND) <= 2 def manaflooded(deck): """Count the number of land in the first 10 cards. If it's more than 7, count that as flooded. """ return deck[:10].count(LAND) >= 8 def play(deck): """Play a game -- draw some cards, play land into one area and spells elsewhere. Every so often someone wins, so scoop all the cards we've seen up without randomizing them and put them back on top of the library to be shuffled. """ return land + spells + unplayed def tournament(shuffle): """At the end, report how many times we got screwed or flooded. """ games = 7 * 3 # 7 rounds, 3 games apiece # We just made a deck, it's got the land on top and the spells on # the bottom. deck = [SPELL] * 35 + [LAND]*25 screwed = 0 flooded = 0 for _ in range(games): # Shuffle up! deck = shuffle(deck) # So, we play some cards. We always scoop if we're screwed or # flooded and we know it, but otherwise we win or lose by the time # we see 12 - 30 cards go by if manascrewed(deck): screwed += 1 played, unplayed = deck[:11], deck[11:] elif manaflooded(deck): flooded += 1 played, unplayed = deck[:11], deck[11:] else: c = random.randint(12, 30) played, unplayed = deck[:c], deck[c:] # Then we take all the cards we saw and separate them into land # and spells, and put those piles on top of the library. Return # the deck to be shuffled. land = [ c for c in played if c == LAND ] spells = [ c for c in played if c == SPELL ] deck = land + spells + unplayed return (games, screwed, flooded) def test(args): """Do a bunch of tournaments. Print the screwed/flooded percentages. """ title, shuffle, n = args screwed = 0 flooded = 0 total = 0 for i in range(10): sys.stdout.flush() for _ in range(n/10): t, s, f = tournament(shuffle) total += t screwed += s flooded += f print title print "Flooded %.2f%% of the time" % ((float(flooded) / total) * 100) print "Screwed %.2f%% of the time" % ((float(screwed) / total) * 100) def comparison(n): """Do a comparison of each shuffle method. """ pool = multiprocessing.Pool(processes=4) result = pool.map_async(test, [["RNG-based shuffle (baseline)", pseudorandom, n], ["Great perfect riffle shuffle", thorough_riffle, n], ["Good perfect riffle", good_riffle, n], ["Normal (bad) perfect riffle", bad_riffle, n], ["Great imperfect riffle shuffle", thorough_imperfect_riffle, n], ["Good imperfect riffle", good_imperfect_riffle, n], ["Normal (bad) imperfect riffle", bad_imperfect_riffle, n], ["Thorough pile shuffle (11/riffle/7/riffle)", fullpile, n], ["11-card pile", pile_11, n], ["7-card pile", pile_7, n] ]) print "Jobs started, waiting for completion..." result.get() print "Jobs done." if __name__ == "__main__": comparison(10000)