Everybody (I hope) knows the game of Monopoly:
In a game a person’s token moves along the board, mostly after the player throws two dice and moves as many fields as the sum of the dice. If he throws a “double” he goes again. Occasionally he lands on Chance or Community Chest and picks a card that may direct him to a specific field. Finally, if he throws three “doubles” in a row or lands on “Go to Jail” he does just that. If he lands on a field he can buy the property if it is not already owned or pay rent to the owner if it is.
After a while in the game a player might own all the properties of the same “color”, and he is said to have a “monopoly”. Then he can begin to build houses, up to a hotel, on this property, and get much more rent from the other players.
Much of the game is “automatic”, that is the player really does not make any decisions. Even the buying of property is (almost) automatic because it is understood that the more property he has, the better off he is. The one time strategy comes into play is when two players have the properties to form two monopolies, but need to “trade” some of them. As an example say player “A” owns New York Ave, Tennessee Ave (orange) and Indiana Ave (red). “B” owns St. James Place (orange), Kentucky Ave and Illinois Ave (red). So if they exchanged St. James Place for Indiana Ave, both would have a monopoly and can build houses. The question is, should they?
One thing is clear: the “red” properties have a higher rent than the orange ones ($950 vs $1050). But rent is only paid if somebody steps on the property, so we also need to know the probability of that happening. This would be easy (all would be equally likely) except for those Chance and Community Chest cards, and the “Go to Jail” option. Notice that everytime somebody gets out of jail, the probability to step on an orange field is
P(6, 8 or 9) = (5+7+8)/36 = 0.55
So how can we find these probabilities? Really, the only way to do this is via a simulation.
How do we set this up as a simulation? First we need to get the “board into R”. The board has 40 fields. Each field has a name (“Go”, " Mediterreanen Ave“, ..) . Many, but not all have a color. Finally there is the rent to be paid (for a hotel). So, we will represent the board as a vector of length 40. In monopoly this setup is done at the beginning:
monopoly <- function (n = 1e+05) {
fields <- c("Go", "Mediterranean", "Community Chest",
"Baltic", "Income Tax", "Reading", "Oriental",
"Chance", "Vermont", "Connecticut", "Jail",
"St. Charles", "Electic Company", "States",
"Virginia", "Pennsylvania RR", "St. James",
"Community Chest", "Tennessee", "New York",
"Free Parking", "Kentucky", "Chance", "Indiana",
"Illinois", "R&B", "Atlantic", "Ventnor",
"Water Works", "Marvin Gardens", "Go to Jail",
"Pacific", "North Carolina", "Community Chest",
"Pennsylvania", "Short", "Chance",
"Park", "Luxery Tax", "Boardwalk")
colors <- c("Go", "Purple", "Community Chest",
"Purple", "Income Tax", "RR", "Light Blue",
"Chance", "Light Blue", "Light Blue", "Jail",
"Wine", "Utility", "Wine", "Wine", "RR", "Orange",
"Community Chest", "Orange", "Orange",
"Free Parking", "Red", "Chance", "Red", "Red", "RR",
"Yellow", "Yellow", "Utility", "Yellow",
"Go to Jail", "Green", "Green", "Community Chest",
"Green", "RR", "Chance", "Blue", "Luxery Tax","Blue")
rent <- c(0, 250, 0, 450, 0, 200, 550, 0, 550, 600, 0,
750, 10, 750, 900, 200, 950, 0, 950, 1000, 0, 1050,
0, 1050, 1100, 200, 1150, 1150, 10, 1200, 0, 1275,
1275, 0, 1400, 200, 0, 1500, 0, 2000)
properties <- 1:10
names(properties) <- c("Purple", "Light Blue", "Wine",
"Orange", "Red", "Yellow", "Green", "Blue", "RR",
"Utility")
chancemove <- function(x) {
z <- sample(1:16, 1)
if (z == 1) {
if (x < 3) {
x <- x + 39 - 3
}
else {
x <- x - 3
}
}
if (z == 2)
x <- 0
if (z == 3)
x <- 39
if (z == 4)
x <- 5
if (z == 5)
x <- 11
if (z == 6)
x <- 24
if (z == 7)
x <- 10
if (z == 8) {
x <- 12
if (x > 21 & x < 39)
x <- 28
}
if (z == 9) {
if (x > 0 & x < 9)
x <- 5
if (x > 10 & x < 19)
x <- 15
if (x > 20 & x < 29)
x <- 25
if (x > 30 & x < 39)
x <- 35
}
x
}
communitymove <- function(x) {
z <- sample(1:15, 1)
if (z == 1)
x <- 0
if (z == 2)
x <- 10
x
}
visits <- rep(0, 40)
rounds <- 0
jail.visit <- TRUE
double <- 0
x <- 0
for (i in 1:n) {
oldx <- x
dice <- sample(1:6, size = 2, replace = T)
if (dice[1] == dice[2])
double <- double + 1
else double <- 0
if (double == 3) {
x <- 10
jail.visit <- F
}
x <- (x + sum(dice))%%40
if (x == 10) {
jail.visit <- TRUE
}
else {
if (fields[x + 1] == "Chance")
x <- chancemove(x)
if (fields[x + 1] == "Community Chest")
x <- communitymove(x)
if (x == 30)
x <- 10
if (x == 10)
jail.visit <- F
}
if (x == 10 & !jail.visit) {
for (j in 1:3) {
visits[x + 1] <- visits[x + 1] + 1
dice <- sample(1:6, size = 2, replace = T)
if (dice[1] == dice[2])
break
}
x <- x + sum(dice)
}
visits[x + 1] <- visits[x + 1] + 1
if (oldx >= x)
rounds <- rounds + 1
}
print(paste("Number of Rounds:", rounds), quote=FALSE)
names(visits) <- fields
payed <- rent * visits
payed[c(12, 28) + 1] <- 70 * visits[c(12, 28) + 1]
p <- visits/sum(visits)
print("Percentage of Visits:", quote=FALSE)
print(round(sort(p, decreasing = T) * 100, 3), quote=FALSE)
print(" ", quote=FALSE)
for (i in names(properties)) properties[i] <- sum(payed[colors ==
i])
print("Mean Rent per Round by Monopoly:", quote=FALSE)
print(properties/rounds, quote=FALSE)
return(list(p = p, rent = properties/rounds))
}
How do we represent the position of a token on the board? One way would be as one of the numbers 1-40, but another option is to use 0-39 instead. An advantage of this is it makes the move counting easier, because R has a modulus function built in. So say we are on field 35, and roll (2, 4). Then the next field is
(35 + 2 + 4)%%40
## [1] 1
which is Baltic Ave.
But there is also a problem: In R vectors cannot be indexed by 0, fields[0] would give an error message. fields[1] is “Go”, not “Baltic Ave”. So if we use x = 0-39, we need to use fields[x+1] to find out where we are.
So the basic move is done with the R commands:
dice <- sample(1:6, size=2,replace=TRUE) # Throw the dice
x <- (x+sum(dice))%%40 # Move along board
After a move we need to check whether we are on
If we are on x=30 we go to Jail (x=10). If we are on “Community Chest” we pick a card and move as directed, which is done by the function communitymove. Similarly for “Chance”.
Note that there are 15 kinds of cards in the cummunity chest, but only two lead to a move of the token, Similarly 9 of the 16 chance cards lead to a move. Because we are interested in the probabilities of visiting streets we can ignore the other cards.
We need to include one more item in our simulation: if a player lands in jail he has two options: he can get out right away (paying $50 or using a card) or he can throw the dice. If they come up doubles he gets out, moving the sum of the doubles. On the third round he gets out for sure (paying $50). Of course, if he is just “visiting” the jail he moves on normaly.
What should a player do? Clearly in the early rounds he wants to get out of jail as quickly as possible (so he has a chance of buying more property) but in the later rounds it is much better to stay in jail (where rent is free). Because in our simulation we are interested in the later stages of a game, this will be our policy.
So the idea is to have one token start at Go (x=0) and let it move over the board according to the rules for many many rounds.
What should we keep track off? Obviously we need to know where we have been, which is done in
visits <- rep(0, 40)
We also keep track of the “rounds”, that is how often we moved around the board. This happens everytime x gets “reset” by the mod function, so if we call “oldx” the current position, “x” the next position then if oldx>x we have gone around once more.
Finally we keep track of the money paid, in
rent <- rep(0, 40)
Here we assume that every property has a hotel, all the railroads are owned by the same person and all the “Utilities” are also owned by the same person.
What should we print out? We are interested in the fields that make money, and their probabilities, so we sort these by the probabilities and show the result. We also show the “rent per round”, this for the different monopolies.
tmp <- monopoly()
## [1] Number of Rounds: 20950
## [1] Percentage of Visits:
## Jail Illinois New York Tennessee
## 10.993 2.963 2.875 2.874
## Go Free Parking Electic Company R&B
## 2.845 2.832 2.824 2.762
## St. James St. Charles Atlantic Reading
## 2.680 2.616 2.608 2.587
## Kentucky Ventnor Indiana Pacific
## 2.587 2.580 2.559 2.553
## Virginia North Carolina Boardwalk Water Works
## 2.509 2.507 2.507 2.481
## Marvin Gardens Pennsylvania RR Community Chest Pennsylvania
## 2.450 2.378 2.367 2.350
## Short Income Tax Connecticut Community Chest
## 2.305 2.276 2.218 2.183
## Oriental States Vermont Baltic
## 2.162 2.145 2.103 2.089
## Luxery Tax Park Mediterranean Community Chest
## 2.079 2.036 2.024 1.814
## Chance Chance Chance Go to Jail
## 1.354 0.980 0.948 0.000
## [1]
## [1] Mean Rent per Round by Monopoly:
## Purple Light Blue Wine Orange Red Yellow
## 75.69 192.41 305.05 426.57 453.37 466.14
## Green Blue RR Utility
## 509.86 422.24 104.99 19.43
tmp
## $p
## Go Mediterranean Community Chest Baltic
## 0.028447 0.020238 0.018141 0.020895
## Income Tax Reading Oriental Chance
## 0.022765 0.025866 0.021615 0.009476
## Vermont Connecticut Jail St. Charles
## 0.021032 0.022181 0.109928 0.026157
## Electic Company States Virginia Pennsylvania RR
## 0.028237 0.021451 0.025090 0.023777
## St. James Community Chest Tennessee New York
## 0.026796 0.023668 0.028738 0.028748
## Free Parking Kentucky Chance Indiana
## 0.028319 0.025866 0.013544 0.025592
## Illinois R&B Atlantic Ventnor
## 0.029632 0.027617 0.026084 0.025802
## Water Works Marvin Gardens Go to Jail Pacific
## 0.024808 0.024497 0.000000 0.025528
## North Carolina Community Chest Pennsylvania Short
## 0.025072 0.021825 0.023503 0.023047
## Chance Park Luxery Tax Boardwalk
## 0.009804 0.020357 0.020785 0.025072
##
## $rent
## Purple Light Blue Wine Orange Red Yellow
## 75.69 192.41 305.05 426.57 453.37 466.14
## Green Blue RR Utility
## 509.86 422.24 104.99 19.43
Here are the streets sorted by the probabilities:
df <- data.frame(Property=names(tmp$p),
Probability=round(as.numeric(tmp$p), 4))
df <- df[order(df$Probability, decreasing = TRUE), ]
row.names(df) <- NULL
kable.nice(df)
Property | Probability | |
---|---|---|
1 | Jail | 0.1099 |
2 | Illinois | 0.0296 |
3 | Tennessee | 0.0287 |
4 | New York | 0.0287 |
5 | Go | 0.0284 |
6 | Free Parking | 0.0283 |
7 | Electic Company | 0.0282 |
8 | R&B | 0.0276 |
9 | St. James | 0.0268 |
10 | St. Charles | 0.0262 |
11 | Atlantic | 0.0261 |
12 | Reading | 0.0259 |
13 | Kentucky | 0.0259 |
14 | Ventnor | 0.0258 |
15 | Indiana | 0.0256 |
16 | Pacific | 0.0255 |
17 | Virginia | 0.0251 |
18 | North Carolina | 0.0251 |
19 | Boardwalk | 0.0251 |
20 | Water Works | 0.0248 |
21 | Marvin Gardens | 0.0245 |
22 | Pennsylvania RR | 0.0238 |
23 | Community Chest | 0.0237 |
24 | Pennsylvania | 0.0235 |
25 | Short | 0.0230 |
26 | Income Tax | 0.0228 |
27 | Connecticut | 0.0222 |
28 | Community Chest | 0.0218 |
29 | Oriental | 0.0216 |
30 | States | 0.0215 |
31 | Vermont | 0.0210 |
32 | Baltic | 0.0209 |
33 | Luxery Tax | 0.0208 |
34 | Park | 0.0204 |
35 | Mediterranean | 0.0202 |
36 | Community Chest | 0.0181 |
37 | Chance | 0.0135 |
38 | Chance | 0.0098 |
39 | Chance | 0.0095 |
40 | Go to Jail | 0.0000 |
How about the monopolies?
df <- data.frame(Monopoly=names(tmp$rent),
Rent=round(as.numeric(tmp$rent)))
df <- df[order(df$Rent, decreasing = TRUE), ]
row.names(df) <- NULL
kable.nice(df)
Monopoly | Rent | |
---|---|---|
1 | Green | 510 |
2 | Yellow | 466 |
3 | Red | 453 |
4 | Orange | 427 |
5 | Blue | 422 |
6 | Wine | 305 |
7 | Light Blue | 192 |
8 | RR | 105 |
9 | Purple | 76 |
10 | Utility | 19 |
Notice that owning the four railroads is worth more than owning the “Purple” monopoly ($105 vs $76 per round).
So, how about our original question, should players “A” and “B” exchange St. James Place for Indiana Ave? If they do “A” then has the orange monoploy and “B” has the red one. So “A” should make about $425 per round and “B” should make about $458. On average in each round “B” makes $33 more than “A”. So this looks like “A” should not trade at all!
Well, there are other considerations. For example, say the players agree to play for another 10 rounds, and “B” offers “A” an extra $330. Now it is a fair trade.
Another thing to consider is the amount of money “A” and “B” have. For example, if “B” is just about broke but “A” has plenty of money, “B” should not trade because he won’t be able to build any houses and so he won’t get any rent anyway.
Let’s assume instead “A” has $a and “B” has $b. Then a trade would be fair if either of the two is equally likely to win at some point in the future. How can we find out what the probability of say “A” winning is? Well if “A” starts with $a then
then after one round
his wealth will increase by rent[“Orange”] with probability p[“Orange”] (if “B” lands on an orange field)
his wealth will decrease by rent[“Red”] with probability p[“Red”] (if he lands on red)
his wealth remains the same (if neither lands on the others property)
Similar of course for B.
Note that the construction costs are $250 per hotel in “row 1”, $500 in “row 2” and so on.
So now we can run a simulation, playing many games as above until one or the other is broke, in
monopoly1 <-
function (a, b, A = "Orange", B = "Red", N = 1000)
{
fields = c("Go", "Mediterranean", "Community Chest", "Baltic",
"Income Tax", "Reading", "Oriental", "Chance", "Vermont",
"Connecticut", "Jail", "St. Charles", "Electic Company",
"States", "Virginia", "Pennsylvania RR", "St. James",
"Community Chest", "Tennessee", "New York", "Free Parking",
"Kentucky", "Chance", "Indiana", "Illinois", "R&B", "Atlantic",
"Ventnor", "Water Works", "Marvin Gardens", "Go to Jail",
"Pacific", "North Carolina", "Community Chest", "Pennsylvania",
"Short", "Chance", "Park", "Luxery Tax", "Boardwalk")
colors = c("Go", "Purple", "Community Chest", "Purple", "Income Tax",
"RR", "Light Blue", "Chance", "Light Blue", "Light Blue",
"Jail", "Wine", "Utility", "Wine", "Wine", "RR", "Orange",
"Community Chest", "Orange", "Orange", "Free Parking",
"Red", "Chance", "Red", "Red", "RR", "Yellow", "Yellow",
"Utility", "Yellow", "Go to Jail", "Green", "Green",
"Community Chest", "Green", "RR", "Chance", "Blue", "Luxery Tax",
"Blue")
rent = c(0, 250, 0, 450, 0, 200, 550, 0, 550, 600, 0, 750,
10, 750, 900, 200, 950, 0, 950, 1000, 0, 1050, 0, 1050,
1100, 200, 1150, 1150, 10, 1200, 0, 1275, 1275, 0, 1400,
200, 0, 1500, 0, 2000)
p = c(0.02929, 0.02059, 0.01787, 0.02088, 0.02236, 0.02682,
0.0214, 0.00968, 0.02215, 0.0219, 0.10723, 0.02568, 0.02817,
0.02178, 0.0248, 0.02356, 0.02729, 0.02344, 0.02855,
0.02821, 0.02852, 0.026, 0.01376, 0.02583, 0.02976, 0.02719,
0.02556, 0.02581, 0.02524, 0.02499, 0, 0.02547, 0.02472,
0.02213, 0.0236, 0.02409, 0.00962, 0.02038, 0.02058,
0.02508)
cost = rep(1:4 * 250, rep(10, 4))
a = a - sum(cost[colors == A])
b = b - sum(cost[colors == B])
if (a < 0 | b < 0) {
print("No money to build!")
return(NA)
}
A.properties = fields[colors == A]
B.properties = fields[colors == B]
Winner = rep(0, N)
start.a = a
start.b = b
for (i in 1:N) {
a = start.a
b = start.b
repeat {
x = sample(1:40, size = 1, prob = p)
for (j in B.properties) if (fields[x] == j) {
a = a - rent[x]
b = b + rent[x]
}
y = sample(1:40, size = 1, prob = p)
for (j in A.properties) if (fields[x] == j) {
a = a + rent[x]
b = b - rent[x]
}
if (a <= 0 | b <= 0)
break
}
Winner[i] = ifelse(a <= 0, 0, 1)
}
mean(Winner)
}
This routine is written to be easily understood, but it is a little slow. We need to run this a number of times, and so it is important to speed it up a bit. This is done in
monopoly2 <- function (a, b, A = "Orange", B = "Red",
N = 10000, Show = FALSE)
{
fields <- c("Go", "Mediterranean", "Community Chest",
"Baltic", "Income Tax", "Reading", "Oriental",
"Chance", "Vermont", "Connecticut", "Jail",
"St. Charles", "Electic Company", "States",
"Virginia", "Pennsylvania RR", "St. James",
"Community Chest", "Tennessee", "New York",
"Free Parking", "Kentucky", "Chance", "Indiana",
"Illinois", "R&B", "Atlantic", "Ventnor",
"Water Works", "Marvin Gardens", "Go to Jail",
"Pacific", "North Carolina", "Community Chest",
"Pennsylvania", "Short", "Chance",
"Park", "Luxery Tax", "Boardwalk")
colors <- c("Go", "Purple", "Community Chest",
"Purple", "Income Tax", "RR", "Light Blue",
"Chance", "Light Blue", "Light Blue", "Jail",
"Wine", "Utility", "Wine", "Wine", "RR", "Orange",
"Community Chest", "Orange", "Orange",
"Free Parking", "Red", "Chance", "Red", "Red", "RR",
"Yellow", "Yellow", "Utility", "Yellow",
"Go to Jail", "Green", "Green", "Community Chest",
"Green", "RR", "Chance", "Blue", "Luxery Tax","Blue")
rent <- c(0, 250, 0, 450, 0, 200, 550, 0, 550, 600, 0,
750, 10, 750, 900, 200, 950, 0, 950, 1000, 0, 1050,
0, 1050, 1100, 200, 1150, 1150, 10, 1200, 0, 1275,
1275, 0, 1400, 200, 0, 1500, 0, 2000)
names(rent) = fields
p <- c(0.02929, 0.02059, 0.01787, 0.02088, 0.02236,
0.02682, 0.0214, 0.00968, 0.02215, 0.0219,
0.10723, 0.02568, 0.02817, 0.02178, 0.0248,
0.02356, 0.02729, 0.02344, 0.02855, 0.02821,
0.02852, 0.026, 0.01376, 0.02583, 0.02976,
0.02719, 0.02556, 0.02581, 0.02524, 0.02499,
0, 0.02547, 0.02472, 0.02213, 0.0236, 0.02409,
0.00962, 0.02038, 0.02058, 0.02508)
cost = rep(1:4 * 250, rep(10, 4))
A.properties <- fields[colors == A]
B.properties <- fields[colors == B]
A.win <- 0
a <- a - sum(cost[colors == A])
b <- b - sum(cost[colors == B])
if (Show) {
print("Money after building Hotels:")
print(c(a, b))
print("Rent to be paid:")
print(rent[c(A.properties, B.properties)])
}
if (a < 0 | b < 0) {
print("No money to build!")
return(NA)
}
a <- rep(a, N)
b <- rep(b, N)
repeat {
n <- length(a)
x <- sample(1:40, size=n, replace=TRUE, prob=p)
for (j in B.properties) {
visit <- ifelse(fields[x] == j, TRUE, FALSE)
a[visit] <- a[visit] - rent[j]
b[visit] <- b[visit] + rent[j]
}
y <- sample(1:40, size=n, replace=TRUE, prob=p)
for (j in A.properties) {
visit <- ifelse(fields[y] == j, TRUE, FALSE)
a[visit] <- a[visit] + rent[j]
b[visit] <- b[visit] - rent[j]
}
if (min(a) < 0 | min(b) < 0) {
if (length(b[b < 0]) > 0)
A.win <- A.win + length(b[b < 0])
}
still.playing <- ifelse(a>=0 & b>=0, TRUE, FALSE)
if (sum(still.playing) == 0)
break
a <- a[still.playing]
b <- b[still.playing]
}
A.win/N
}
Here we essentially run all N simulations simultaneously.
Playing around with this routine we can find out what a fair trade value is.
Say both have $3000, then
monopoly2(3000, 3000)
## [1] 0.6753
“A” wins with probability 66%. With a little bit of trial and error we can find that if as part of the trade “A” gives “B” $225, then they both have the same probability of going broke:
monopoly2(3000-225, 3000+225)
## [1] 0.5192
This is a very strange result: the orange properties are before the red ones, with lower rents, yet A should pay B? The explanation is this: B needs to pay more money ($750 more) to build his hotels, and so he can get broke by just a few visits to A. If both started out with $6000, it is B who needs to pay A, some $300.
In the example above we have used trial and error to find the right trade-value. Can we write a routine that does this for us? Here is an idea:
find the probability of A winning without any money changing hands, call it p0
if p0 > 0.5 A needs to pay B, if p0 < 0.5 B needs to pay A.
say p0 > 0.5, let m be some amount of money (for example 10% of $a, let pm be the probability of A winning for a trade-value of m. If pm < 0.5 the correct trade value is in [0, m], otherwise raise m and try again.
once we have found an interval [m1, m2] that contains the fair trade-value, check the midpoints and half the intervals.
if p0 < 0.5 do the same the other way around.
stop when pm = 0.5 (just about)
This is an example of one of my favorite algorithms, the bisection algorithm. here is the basic idea:
so we change low to mid and run again. This algorithm works great if the function is monotone. It is very slow but extrememly stable. In our case because we don’t have an explicit expression for the function (and therefore no derivative) it’s probably as good as we can do.
There is one difficulty: our routine is a simulation and will give slightly different values in different runs. Here is what we need to consider:
Let’s say we do N = 10000 runs. On each run A either wins or looses. Let the rv Zi = 1 if A wins, 0 otherwise. Then Z1, ..,Z N is a sequence of independent Bernoulli rv’s with success probability pm. Therefore
so if we run a simulation that gives pm \(\in\) [0.485,0.515] the “true” pm might just be 0.5 and the corresponding m is a fair-trade value.
This is implemented in
monopoly3(5000, 3000, Show=TRUE)
## [1] "m= 0 pm= 0.766"
## [1] "m= 500 , pm= 0.6134"
## [1] "m= 1000 , pm= 0.5505"
## [1] "m= 1500 , pm= 0.4085"
## [1] "m= 1250 , pm= 0.4548"
## [1] "m= 1125 , pm= 0.5188"
## [1] "m= 1187.5 , pm= 0.4945"
## A should pay B: $ 1188
## [1] 1188
This routine is not yet very good. To see whether your routine has problems it is often a good idea to run “extreme” cases. For example, try
monopoly3(10000,3000,"Purple","Green", Show=T)
## [1] "m= 0 pm= 0.352"
## [1] "No money to build!"
## Error in if (abs(pm - 0.5) < 0.015) {: missing value where TRUE/FALSE needed
but the error message makes no sense, to begin with A and B had enough money to build. Any idea what’s wrong with my routine?
So, next time you play monopoly, bring your laptop and at the right time run our little routine.
If you want to know more about strategy in Monopoly, check out these websites:
[Amnesta] (http://www.amnesta.net/other/monopoly)
[Collins] (http://www.tkcs-collins.com/truman/monopoly/monopoly.shtml)