Are Boardwalk & Park Place the Most Valuable Properties in Monopoly? Game simulation in Python

It was a nice fall evening with friends, when we broke out the Monopoly board, and proceeded to play the beloved game that most everyone in the western world has played at some time in their lives.  As we struggled to increase our property holdings, wheeling and dealing, cajoling and swearing at the dice, an interesting question arose:  Do the coveted blue properties of Boardwalk and Park Place at the end of the monopoly board, really deserve their reputation as the most valuable monopoly in the game?  It certainly feels like the end of the world (at least the gaming world) when one of your rivals start putting houses and eventually hotels thereon.  At first glance, it certainly commands the highest rents, but what are the other factors that might influence comparative value against some of the other property groups on the board?  How might we conduct some objective analysis that would answer this question?

With a little extra time on a rainy Sunday afternoon, it was time to have a shot at answering the question, and have a little fun in the process. 

To analyze a game, it made sense to simulate play.  This could be done in a straight-forward way with Python.  The simulation would need to include the following elements: 

A simple Import block gives us random number generation & csv file writing:

from random import randrange, shuffle
import csv

Now we need a list of property names, their associated position on the board (0-39), their color group, and rent with one hotel.  Rents for railroads & utilities was assumed to be as large as it could be (e.g. assuming all 4 railroads were owned by same person, it would be $200).  A Python dictionary was used, with the board position from 0-39 as the key, and a tuple with three items containing the Name, Color Group, and Hotel Rent for each:

NameDict =      {0: ("Go", "None",0),
1: ("Mediterranean Ave", "Purple",250),
2: ("Community Chest", "CardDraw",0),
3: ("Baltic Ave", "Purple",450),
4: ("Income Tax", "None",0),
5: ("Reading Railroad", "Railroad",200),
6: ("Oriental Ave", "LightBlue",550),
7: ("Chance", "CardDraw",0),
8: ("Vermont Ave", "LightBlue",550),
9: ("Connecticut Ave", "LightBlue",600),
10: ("Jail", "None",0),
11: ("St Charles Place", "Magenta",750),
12: ("Electric Company", "Utility",70),
13: ("States Ave", "Magenta",750),
14: ("Virginia Ave", "Magenta",900),
15: ("Pennsylvania Railroad", "Railroad",200),
16: ("St James Place", "Orange",950),
17: ("Community Chest", "CardDraw",0),
18: ("Tennessee Ave", "Orange",950),
19: ("New York Ave", "Orange",1000),
20: ("Free Parking", "None",0),
21: ("Kentucky Ave", "Red",1050),
22: ("Chance", "CardDraw",0),
23: ("Indiana Ave", "Red",1050),
24: ("Illinois Ave", "Red",1100),
25: ("B&O Railroad", "Railroad",200),
26: ("Atlantic Ave", "Yellow",1150),
27: ("Ventnor Ave", "Yellow",1150),
28: ("Water Works", "Utility",70),
29: ("Marvin Gardens", "Yellow",1200),
30: ("Go To Jail", "None",0),
31: ("Pacific Ave", "Green",1275),
32: ("North Carolina", "Green",1275),
33: ("Community Chest", "Card Draw",0),
34: ("Pennsylvania Ave", "Green",1400),
35: ("Short Line Railroad", "Railroad",200),
36: ("Chance", "CardDraw",0),
37: ("Park Place", "Blue",1500),
38: ("Luxury Tax", "None",0),
39: ("Boardwalk", "Blue",2000)}

Representation of the board, as a sequence of 40 possible locations from Go at #0, to Boardwalk at position #39.  This class does most of the heavy lifting for our simulation:

class Board:
def __init__(self):
self.CommunityChest = CardDeck(communityChestCardList)
self.CommunityChest.shuffleDeck()
self.ChanceCards = CardDeck(chanceCardList)
self.ChanceCards.shuffleDeck()

def GetNameGroup(self,position):
return NameDict[position]

def sendPlayerToPosition(self,player,position):
player.boardPosition = position
player.positionLog.append(position)
return player

def sendPlayerToJail(self,player):
player = self.sendPlayerToPosition(player, 10)
player.inJail = True
return player

def sendPlayerToGo(self,player):
player = self.sendPlayerToPosition(player, 0)
player.passGoCount += 1
return player

def drawCommunityChest(self, player):
draw = self.CommunityChest.drawCard()
if draw == "GoToJail":
player = self.sendPlayerToJail(player)
if draw == "AdvanceToGo":
player = self.sendPlayerToGo(player)
return player

def advancePlayer(self, player, position):
if player.boardPosition > position:
player.passGoCount += 1
player = self.sendPlayerToPosition(player, position)
return player

def drawChance(self, player):
draw = self.ChanceCards.drawCard()
if draw == "GoToJail":
player = self.sendPlayerToJail(player)
if draw == "AdvanceToGo":
player = self.sendPlayerToGo(player)
if draw == "AdvanceToIllinois":
player = self.advancePlayer(player,24)
if draw == "AdvanceToStCharles":
player = self.advancePlayer(player,11)
if draw == "AdvanceToUtility": #12, 28
if 12 < player.boardPosition < 28:
player = self.advancePlayer(player,28)
else:
player = self.advancePlayer(player,12)

if draw == "AdvanceToRailroad":
if 35 < player.boardPosition or player.boardPosition <= 5:
player = self.advancePlayer(player,5)
if 5 < player.boardPosition <= 15:
player = self.advancePlayer(player,15)
if 15 < player.boardPosition <= 25:
player = self.advancePlayer(player,25)
if 25 < player.boardPosition <= 35:
player = self.advancePlayer(player,35)
if draw == "GoBack3":
newPosition = player.boardPosition - 3
if newPosition < 0:
newPosition += 40
player = self.sendPlayerToPosition(player,newPosition)
if newPosition == 33: # landed on community chest
player = self.drawCommunityChest(player)
if draw == "AdvanceToReadingRR":
player = self.advancePlayer(player,5)
if draw == "AdvanceToBoardwalk":
player = self.advancePlayer(player,39)

return player

def movePlayer(self, player , numSteps):
player.rollCount += 1
if player.inJail:
if player.escapeTrys < 3: # attempt to roll doubles to escape
dice = Die()
dice.roll2()
if dice.isDouble:
player.escapeTrys = 0 # rolled doubles and get out
player.inJail = False
else:
player.escapeTrys += 1 # no doubles, stay in jail
return player
else: # did 3 turns and must get out
player.inJail = False
player.escapeTrys = 0
endPosition = player.boardPosition + numSteps
if endPosition >= 40:
endPosition -= 40
player.passGoCount += 1
player.positionLog.append(endPosition)
player.boardPosition = endPosition

# go to jail
if endPosition == 30:
player = self.sendPlayerToJail(player)

# community chest
if endPosition in [2,17,33]:
player = self.drawCommunityChest(player)

# chance
if endPosition in [7,22,36]:
player = self.drawChance(player)

return player

Then we need to simulate a dice roll as the sum of 2 equally-likely random integer samples from the range 1-6:

class Die:
def roll2(self):
roll1 = randrange(6) + 1
roll2 = randrange(6) + 1
if roll1 == roll2:
self.isDouble = True
else:
self.isDouble = False
return roll1 + roll2

We also need to be able to draw from the Community Chest, and Chance decks as our position on the board requires, only changing the board position of our simulated piece when ‘move’ cards are drawn (e.g. Go To Jail, Go Back 3 Spaces, etc.).  As can be seen from the code below, the CardDeck class can be initialized with either a list of Chance Cards, or a list of Community Chest cards.  The objects have methods for shuffling, and drawing a card:

class CardDeck:
currentCard = 0
def __init__(self, cardList ):
self.deck = cardList

def shuffleDeck(self):
shuffle(self.deck)

def drawCard(self):
card = self.deck[self.currentCard]
self.currentCard += 1
if self.currentCard == len(self.deck):
self.currentCard = 0
return card

chanceCardList = [
"AdvanceToGo",
"AdvanceToIllinois",
"AdvanceToStCharles",
"AdvanceToUtility",
"AdvanceToRailroad",
"Dividend",
"GetOutOfJailFree",
"GoBack3",
"GoToJail",
"Repairs",
"PoorTax",
"AdvanceToReadingRR",
"AdvanceToBoardwalk",
"ElectedChairman",
"LoanMatures",
"WonCrossword"
]


communityChestCardList = [
"AdvanceToGo",
"BankError",
"DoctorFee",
"SoldStock",
"GetOutOfJailFree",
"GoToJail",
"OperaNight",
"HolidayFund",
"TaxRefund",
"Birthday",
"LifeInsurance",
"HospitalFees",
"SchoolFees",
"ConsultantFee",
"StreetRepairs",
"BeautyContest",
"Inherit"
]

Notice the Player class is simple, but keeps track of whether or not the player is in jail, and how many times he has attempted to escape:

class Player:
rollCount = 0
passGoCount = 0
boardPosition = 0
positionLog = [0]
inJail = False
escapeTrys = 0

  • We can then simulate some arbitrary number of turns (e.g. 100,000), tally the frequency of visits to all properties and the resulting dollar rents that would result from fully developed (hotel) properties.  As you can see, the main program flow is simple and easy to follow, with most of the code dedicated to rendering the .csv file for Spotfire visualization:
myDice = Die()
steve = Player()
board = Board()

numberOfRolls = 100000
for i in range(numberOfRolls):
steve = board.movePlayer(steve,myDice.roll2())

# render output file
with open( "C:/Users/Public/monopoly.csv", "w") as csv_file:
writer = csv.writer(csv_file, delimiter=',')
writer.writerow(['roll','PropNum','PropName','PropGroup','Rent'])
roll = 1
for prop in steve.positionLog:
name,group,value = board.GetNameGroup(prop)
strList = [str(roll),str(prop),name,group,str(value)]
writer.writerow(strList)
roll += 1

Simulation Results

Running 100,000 dice rolls yielded the following insights, subject to the assumptions that all properties were fully developed, and that players would stay in jail as long as possible.   Charts below were visualized in Tibco Spotfire:

  1. The green property group is clearly the most valuable, with an average value of over $98 per roll.  The Red and Yellow property groups are very close in value, with our Blue property group (Boardwalk & Park Place) showing as the 4th most valuable property, and very close to the value of the Orange group.  The Magenta group represents a sharp drop in value.  Of interest at the lower end of the scale, is that collecting $200 by owning all 4 railroads is almost 50% more valuable ($20.20 per roll) than owning the two Purple Properties at the start of the board ($13.91):

Avg Rent per Roll by Property Color Group

Blue Property group is marked to the right. The breakout of the two related property values is shown in the left chart.

Boardwalk is the big winner here as can be seen in the left chart above.  Its $2000 rent with a hotel is tops.  Notice that Park Place though, is the 6th most valuable property, with all the green properties in the gap between.  Clearly what is going on here, is that the footprint of the monopolies with three properties, being 50% larger than our Blue pair, can overwhelm the higher rents associated with the Blues, and provide overall higher valuation.  

How can we make better decisions in our next Monopoly game, given this information? 

While Boardwalk has great rent, most players don’t know that the blue monopoly is only the 4th most valuable group on the board.  Given a chance to trade Board Walk or Park Place away to award a rival blue monopoly, for a chance at your own monopoly in the green, red, or yellow groups, take it!  Your rival will think you crazy, but you will know better! 

Posted in Simulation.