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?
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
.