Search code examples
pythonrandom

random.sample producing same output when iterated over inside function


I'm trying to write a poker hand evaluator with a monte carlo simulation.

When simulating game outcomes (e.g. dealing cards) inside a function using random.sample randomization works fine.

def simulate(hands, board):
    # create deck
    suits = ['d','s','c','h']
    ranks = ['A','2','3','4','5','6','7','8','9','T','J','Q','K']
    cards = []
    for r in ranks:
        for s in suits:
            cards.append(r+s)
    
    # shuffle deck
    deck = random.sample(cards,len(cards)) 
    # remove board and player cards from deck
    deck = list(filter(lambda x: x not in board, deck))
    for hand in hands:
          deck = list(filter(lambda x: x not in hand, deck))
    
    # deal turn and river
    while len(board) < 5:
        card = deck.pop(0)
        board.append(card)
    return board
for i in range(2):
    outcome = simulate([['Ah', 'Ac'], ['7s', '6s']], ['2d', '5s', '8s'])
    print(outcome)
Output:
['2d', '5s', '8s', '9s', 'Jc']
['2d', '5s', '8s', '4s', '3s']

If I run this inside a for loop it works fine but once I put this into another function randomization fails and I keep getting the same result.

def monte_carlo(hands, board, samples=5):
    for i in range(samples):
        outcome = simulate(hands, board)
        print(outcome)

monte_carlo([['Ah', 'Ac'], ['7s', '6s']], ['2d', '5s', '8s'])

Output:
Board  ['2d', '5s', '8s', '2c', '4d']
None
Board  ['2d', '5s', '8s', '2c', '4d']
None
Board  ['2d', '5s', '8s', '2c', '4d']
None
Board  ['2d', '5s', '8s', '2c', '4d']
None
Board  ['2d', '5s', '8s', '2c', '4d']

What is the reason for this behaviour?


Solution

  • This is a tricky little piece around the list type and the fact it's mutable. If you change this line of code: outcome = simulate(hands, board) to outcome = simulate(hands, [b for b in board]) you'll get the desired result.

    The [b for b in board] just creates a new list which has the same elements as board. Importantly, it is a different list behind the scenes in python. New memory is associated with it. If you don't do this, the board that is being used in each iteration of the loop is a reference to the exact same list. To visualise it, add this debug into the code:

    def monte_carlo(hands, board, samples=5):
        print("NOW MY BOARD HAS VALUE: ", board)
        for i in range(samples):
            outcome = simulate(hands, board)
            print(outcome)
    

    You'll see that that debug shows the value of board changing in time, even though the board that's being printed is outside the simulate function. Because lists are mutable, all references to the list point to the exact same memory. It's not like each time you run simulate(hands, board) you're sending in a brand new version of board, exactly as it was when you called monte_carlo(hands, board). No, you're sending in the exact same list. And because inside simulate you're changing the list (i.e. adding elements), those changes also exist in the monte_carlo method's reference to board.