# Monopoly Walker (#188)

**URL:** <https://www.ruby-forum.com/t/monopoly-walker-188/157926>\
**Category:** Ruby\
**Created:** [January 16, 2009, 4:49pm UTC](https://www.ruby-forum.com/t/monopoly-walker-188/157926 "2009-01-16T16:49:48Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![mansfiem](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/m/fbc32d/32.png) [@mansfiem](https://www.ruby-forum.com/u/mansfiem)\
**Post date:** [January 16, 2009, 4:49pm UTC](https://www.ruby-forum.com/t/monopoly-walker-188/157926/1 "2009-01-16T16:49:48Z")

</div>

Okay, this will be the last quiz I host for Ruby Q… I hope to  
find a new quizmaster in the coming week.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

The three rules of Ruby Q. 2:

1. Please do not post any solutions or spoiler discussion for this  
quiz until 48 hours have passed from the time on this message.

2. Support Ruby Q. 2 by submitting ideas as often as you can!  
Visit [http://splatbang.com/rubyquiz/](http://splatbang.com/rubyquiz/).

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem  
helps everyone on Ruby T. follow the discussion. Please reply to  
the original quiz message, if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

## Monopoly Walker

Your task this week is to simulate players walking about a Monopoly  
board. You are not implementing the whole game; rather, you are to  
simulate and track just the players’ movement. Throw dice, move  
tokens. Keep track of the properties where players land and how often.

Your output should be a table showing the relative frequency each  
property is landed upon. Now, if you only paid attention to the dice,  
this should be pretty even across the board. To make it slightly more  
interesting, you do need to pay attention to the “Go to Jail” space,  
as well as the Chance and Community Chest cards.

For the cards, don’t worry about money tracking or anything like that;  
again, we’re only interested in movement, and the few Chance and  
Community Chest cards that affect movement _should_ have an impact on  
the relative landing frequency.

Some helpful links:

- [board properties and layout](http://monopoly.wikia.com/wiki/Monopoly_(Game)#Board) (scroll down to the “Board”  
section)
- [Chance cards](http://monopoly.wikia.com/wiki/Chance)
- [Community Chest cards](http://monopoly.wikia.com/wiki/Community_Chest)

If there is version differences, use information from these pages  
pertaining to the Standard (American edition) as of Sept 2008.

---

<div class="post-metadata">

**Author:** ![mansfiem](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/m/fbc32d/32.png) [@mansfiem](https://www.ruby-forum.com/u/mansfiem)\
**Post date:** [January 18, 2009, 5:32pm UTC](https://www.ruby-forum.com/t/monopoly-walker-188/157926/2 "2009-01-18T17:32:07Z")

</div>

On Fri, Jan 16, 2009 at 7:48 AM, Matthew M. [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) wrote:

> ## Monopoly Walker
> 
> Your task this week is to simulate players walking about a Monopoly board.  
> You are not implementing the whole game; rather, you are to simulate and  
> track just the players’ movement. Throw dice, move tokens. Keep track of the  
> properties where players land and how often.

My solution is pretty straightforward, I have a Property class and a  
Board class. The board holds an array of properties. The interesting  
thing comes when some properties have a special move, like “Advance to  
St. Charles Place”. I used some Procs to keep track of the extra move  
behavior. The simulation part involves throwing the dice, then  
following all moves until the token stays put (sometimes you can land  
on Chance, go back three spaces, land on Community Chest, then advance  
to GO).

The first argument to the program states the number of dice throws to  
simulate, if none is given 100000 is used.

# Ruby Q. #188

# Monopoly Walker

# Daniel M.

# Get some dice action going on

class Fixnum  
def d(sides)  
sum = 0  
self.times {sum += Kernel.rand(sides) + 1}  
return sum  
end  
end

# Cells keep track of their own hit count and any crazy bonus moves

class Property  
@@property\_count = 0  
attr\_accessor :count

def initialize(name, block)  
@count = 0  
@name = name.gsub(‘\_’, ’ ')  
@position = @@property\_count  
@@property\_count += 1  
@move\_block = block  
end

# Record that the token landed on this cell

# Return any bonus move (new location)

def land  
@count += 1  
# Sometimes cells have a bonus move, this returns  
# the new location, could be the same if no bonus move.  
@move\_block.call(@position)  
end

# Print out this cells name and count

def to\_s  
“(#{”%02d" % @position}) #{@name}#{spacing}- #{@count}"  
end

# Arbitrary spacing to format the output cleanly

def spacing  
s = " "  
(21 - @name.size).times do  
s += " "  
end  
s  
end  
end

class Board  
PROPERTY\_NAMES = %w[GO Mediterranean Community\_Chest Baltic  
Income\_Tax Reading\_Railroad Oriental Chance Vermont Connecticut  
Jail/Just\_Visiting St.\_Charles\_Place Electric\_Company States  
Virginia Pennsylvania\_Railroad St.\_James\_Place Community\_Chest  
Tennessee New\_York  
Free\_Parking Kentucky Chance Indiana Illinois B&O\_Railroad  
Atlantic Ventnor Water\_Works Marvin\_Gardins  
Go\_To\_Jail Pacific North\_Carolina Community\_Chest Pennsylvania  
Short\_Line\_Railroad Chance Park\_Place Luxury\_Tax Boardwalk]

# Some Board positions

GO\_POSITION = 0  
READING\_POSITION = 5  
JAIL\_POSITION = 10  
ST\_CHARLES\_POSITION = 11  
ELECTRIC\_COMPANY\_POSITION = 12  
ILLINOIS\_POSITION = 24  
WATER\_WORKS\_POSITION = 28  
BOARDWALK\_POSITION = 39

CHANCE\_CARDS = 15  
COMMUNITY\_CHEST\_CARDS = 16  
BOARD\_SIZE = 40

COMMUNITY\_CHEST\_EFFECT = Proc.new do |cur\_pos|  
# Simulate 16 card Community chest deck  
case Kernel.rand(COMMUNITY\_CHEST\_CARDS)  
when 0  
GO\_POSITION  
when 1  
JAIL\_POSITION  
else  
# This card does not have an effect on position  
cur\_pos  
end  
end

CHANCE\_EFFECT = Proc.new do |cur\_pos|  
case Kernel.rand(CHANCE\_CARDS)  
when 0  
GO\_POSITION  
when 1  
ILLINOIS\_POSITION  
when 2  
# Nearest Utility  
if (cur\_pos \>= WATER\_WORKS\_POSITION) || (cur\_pos \<  
ELECTRIC\_COMPANY\_POSITION)  
ELECTRIC\_COMPANY\_POSITION  
else  
WATER\_WORKS\_POSITION  
end  
when 3…4  
# Nearest Railroad  
case cur\_pos  
when 5…14  
15  
when 15…24  
25  
when 25…34  
35  
else  
READING\_POSITION  
end  
when 5  
ST\_CHARLES\_POSITION  
when 6  
# Go back three spaces  
cur\_pos - 3  
when 7  
JAIL\_POSITION  
when 8  
READING\_POSITION  
when 9  
BOARDWALK\_POSITION  
else  
# This card does not have an effect on position  
cur\_pos  
end  
end

# Roll 2d6

def roll  
2.d 6  
end

def initialize  
# Stay put Proc, used in most regular cells  
stay\_put = Proc.new {|cur\_pos| cur\_pos}

```
proc_for_name = {
  "Community_Chest" => COMMUNITY_CHEST_EFFECT,
  "Chance" => CHANCE_EFFECT,
  "Go_To_Jail" => Proc.new do |cur_pos|
    JAIL_POSITION
  end
}

@properties = PROPERTY_NAMES.map do |name|
  # Create the property and give it it's bonus move behavior proc
  Property.new(name, proc_for_name[name] || stay_put)
end

```

end

def simulate(moves)  
@moves = moves  
position = 0

```
@moves.times do
  position += roll

  # Land on the properties and keep following the cards until we 

```

stay put  
while( position != (new\_position = (@properties[position %  
BOARD\_SIZE]).land) ) do  
position = new\_position  
# Track the extra moves  
@moves += 1  
end  
end  
end

# Displays the results of the simulation

# Permanently alters the cells making them unsuitable for

# further simulation.

def display  
puts “Total hits in board order after #{@moves} turns:”  
puts @properties  
puts “------”

```
puts "Sorted by Relative Frequency: "
# Sort and display results
@properties.sort{|a, b| b.count <=> a.count }.each do |property|
  property.count = "%1.4f%" % (property.count * 100 / @moves.to_f)
  puts property
end
puts "------"

```

end  
end

board = Board.new

board.simulate((ARGV[0] || 100000).to\_i)  
board.display

* * *

Sorted by Relative Frequency:  
(10) Jail/Just Visiting - 5.0612%  
(00) GO - 4.4737%  
(05) Reading Railroad - 3.8067%  
(01) Mediterranean - 3.5400%  
(03) Baltic - 3.3674%  
(04) Income Tax - 3.3528%  
(02) Community Chest - 3.2069%  
(06) Oriental - 2.7579%  
(24) Illinois - 2.6526%  
(19) New York - 2.5675%  
(25) B&O Railroad - 2.5140%  
(08) Vermont - 2.4062%  
(17) Community Chest - 2.3746%  
(21) Kentucky - 2.3462%  
(18) Tennessee - 2.3341%  
(16) St. James Place - 2.3292%  
(20) Free Parking - 2.3227%  
(22) Chance - 2.3154%  
(11) St. Charles Place - 2.3130%  
(09) Connecticut - 2.3090%  
(15) Pennsylvania Railroad - 2.3057%  
(28) Water Works - 2.3033%  
(26) Atlantic - 2.2676%  
(31) Pacific - 2.1947%  
(39) Boardwalk - 2.1923%  
(33) Community Chest - 2.1898%  
(30) Go To Jail - 2.1817%  
(12) Electric Company - 2.1525%  
(23) Indiana - 2.1404%  
(07) Chance - 2.1193%  
(32) North Carolina - 2.1145%  
(29) Marvin Gardins - 2.1023%  
(27) Ventnor - 2.0861%  
(34) Pennsylvania - 2.0610%  
(14) Virginia - 2.0350%  
(35) Short Line Railroad - 2.0026%  
(13) States - 1.8964%  
(36) Chance - 1.8778%  
(38) Luxury Tax - 1.7376%  
(37) Park Place - 1.6882%

---

<div class="post-metadata">

**Author:** ![mansfiem](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/m/fbc32d/32.png) [@mansfiem](https://www.ruby-forum.com/u/mansfiem)\
**Post date:** [January 22, 2009, 10:34pm UTC](https://www.ruby-forum.com/t/monopoly-walker-188/157926/3 "2009-01-22T22:34:52Z")

</div>

On Jan 22, 2009, at 3:26 PM, Matthew M. wrote:

> And thanks for everyone during my stint as quizmaster. I look  
> forward to seeing more great quizzes in the future from quizmaster,  
> version 3.0! 😃

Thanks Matthew. Great run.

James Edward G. II

---

<div class="post-metadata">

**Author:** ![mansfiem](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/m/fbc32d/32.png) [@mansfiem](https://www.ruby-forum.com/u/mansfiem)\
**Post date:** [January 22, 2009, 10:28pm UTC](https://www.ruby-forum.com/t/monopoly-walker-188/157926/4 "2009-01-22T22:28:56Z")

</div>

Writing a simulator for a complete Monopoly game isn’t overly complex,  
but it does require a lot of attention to detail in order to  
accurately reflect the game rules. Writing a simulator for just the  
movement portion of the game should be much simpler – you can ignore  
property purchases and auctions, money tracking, rent, hotels, etc.

What makes such a simulator non-trivial is the possibility of jumping  
around. If the only way to move around the board was via a dice rolls,  
the expected pattern to landing on properties would be even; that is,  
no one property would be more valuable than any other. However, when  
the Community Chest and Chance cards are added in, along with the  
Jail, the distribution is no longer even. When running the submission  
from _Daniel M._ for 10,000,000 iterations, the top ten properties  
show up as:

Jail/Just Visiting - 5.0660%  
GO - 4.4057%  
Reading Railroad - 3.7458%  
Mediterranean - 3.4747%  
Income Tax - 3.3711%  
Baltic - 3.3506%  
Community Chest - 3.2478%  
Oriental - 2.8945%  
Illinois - 2.6351%  
New York - 2.5123%

Now, four of those properties cannot be owned. The other six amount to  
almost 20% of property landings. And, interestingly, two of the  
highest properties are Mediterranean and Baltic, which form a monopoly.

I’ll note here that I believe Daniel’s simulation to be a good start,  
but has some problems. I found one bug. It does not simulate the  
rolling of doubles to escape Jail, which would have an impact on the  
twelve properties that follow. Also, I’m not certain the handling of  
Community Chest and Chance cards is mathematically accurate, but may  
be reasonably close. Additionally, the human factor is completely  
removed here, which may be significant.

In any case, while you may want to improve the script before preparing  
for your next game of Monopoly, we can certainly look at what Daniel  
has done. Let’s begin with the overall simulation:

class Board  
# …  
def simulate(moves)  
@moves = moves  
position = 0

```
  @moves.times do
    position += roll

    # Land on the properties and keep following the cards until we

```

stay put  
while( position != (new\_position = (@properties[position %  
BOARD\_SIZE]).land) ) do  
position = new\_position  
# Track the extra moves  
@moves += 1  
end  
end  
end  
# …  
end

board = Board.new  
board.simulate((ARGV[0] || 100000).to\_i)

One parameter is pulled from the command line to be the number of  
simulation steps (i.e. dice rolls) to make, defaulting to 100,000 is  
no argument is provided. The board is created and `simulate` called.

Inside, we loop, calculating the next position, finding the  
corresponding property, and calling `land` on that property. `land`  
will return new position, often itself, unless some condition causes  
the player to move elsewhere. If that happens (and so `position` will  
not equal `new_position`), we update `position` and increase `@moves`,  
just to keep track of how many moves were made overall (compared to  
how many rolls, the original parameter). When we look at `land`, we’ll  
see the bookkeeping for tracking landing counts.

There is a bug here, however: the calculation of `position`. In most  
cases, when you don’t move beyond the roll, `land` will return the  
index into `@properties`: that is, `position % BOARD_SIZE`. Usually,  
this will be the same as `position`, except when passing Go (e.g. 46 !  
= 6). In such a case, the move count will be incremented  
inappropriately, and `land` will be called once too often. To fix,  
change the loop to:

```
@moves.times do
  position += roll
  position %= BOARD_SIZE

  # Land on the properties and keep following the cards until we

```

stay put  
while( position != (new\_position = (@properties[position]).land) )  
do  
position = new\_position  
# Track the extra moves  
@moves += 1  
end  
end

A seemingly minor bug, but this is why Baltic, Mediterranean, and  
Oriental showed up near the top of the distribution; they are the  
properties that would be hit more frequently when moving past Go. When  
this bug is fixed, the top ten distribution of properties is:

Jail/Just Visiting - 5.4544%  
Illinois - 2.9668%  
GO - 2.9018%  
New York - 2.8461%  
B&O Railroad - 2.8458%  
Reading Railroad - 2.7957%  
Community Chest - 2.7122%  
Pennsylvania Railroad - 2.7024%  
Tennessee - 2.6937%  
Free Parking - 2.6587%

Now we see Illinois Avenue, B&O Railroad and GO are closer to the top,  
which are the three most landed on properties according to most  
sources I’ve seen, including the Monopoly wiki. (Not sure why Jail is  
so high… and New York would drop in rank once in-Jail rolls are  
handled correctly).

Let’s now look a bit at the `Property` class, that which tracks how  
often a player lands on the property.

class Property  
@@property\_count = 0  
attr\_accessor :count

```
def initialize(name, block)
  @count = 0
  @name = name.gsub('_', ' ')
  @position = @@property_count
  @@property_count += 1
  @move_block = block
end

# Record that the token landed on this cell
# Return any bonus move (new location)
def land
  @count += 1
  # Sometimes cells have a bonus move, this returns
  # the new location, could be the same if no bonus move.
  @move_block.call(@position)
end
#...

```

end

The basics of this class is pretty simple: a `@count` data member is  
initialized to zero at creation, and incremented once for each call to  
`land`. `attr_accessor` provides a way to get the count later. `@name`  
is also initialized at creation.

`@move_block` is also assigned at creation; this is a code block that,  
given a position, will return another position. The idea here is that  
some spots on the board (such as Chance, Community Chest, and Go to  
Jail) will immediately move the player somewhere else. Calling this  
block (provided elsewhere) will return the new position. In most  
cases, where the player does not move, the `stay_put` block is used;  
given the current position, it returns that same position – the  
player will stay in one place.

stay\_put = Proc.new {|cur\_pos| cur\_pos}

My main concern with the `Property` class is the duplication of effort  
found in `@@property_count`. The idea is to have each newly created  
property receive a unique index, stored in `@position`. However, this  
information is already provided externally by the `PROPERTY_NAMES`  
constant array, which dictates the order in which properties are  
created. Whenever you have two data “masters”, you run the risk that  
they disagree. My revision would be to lose `@@property_count` and  
pass an extra parameter into the initializer.

class Property  
#…  
def initialize(pos, name, block)  
@count = 0  
@position = pos  
@name = name.gsub(’\_’, ’ ')  
@move\_block = block  
end  
#…  
end

Also, I would like to change `attr_accessor` to `attr_reader`, but the  
`count` field is written to later in the code. However, it is reused  
for a purpose other than the count; it would be better to provide a  
separate data member, appropriately named, rather than overlap use of  
`count`. Or, better yet, calculate the frequency on the fly, since  
it’s a simple calculation that doesn’t need to be stored.

The last thing I’ll look at here is one of the code blocks used to  
handle special movement around the board. There are a few of them, but  
let’s look at the block for handling Chance cards. (The other blocks  
are reasonably similar.)

CHANCE\_EFFECT = Proc.new do |cur\_pos|  
case Kernel.rand(CHANCE\_CARDS)  
when 0  
GO\_POSITION  
when 1  
ILLINOIS\_POSITION  
when 2  
# Nearest Utility  
if (cur\_pos \>= WATER\_WORKS\_POSITION) || (cur\_pos \<  
ELECTRIC\_COMPANY\_POSITION)  
ELECTRIC\_COMPANY\_POSITION  
else  
WATER\_WORKS\_POSITION  
end  
when 3…4  
# Nearest Railroad  
case cur\_pos  
when 5…14  
15  
when 15…24  
25  
when 25…34  
35  
else  
READING\_POSITION  
end  
when 5  
ST\_CHARLES\_POSITION  
when 6  
# Go back three spaces  
cur\_pos - 3  
when 7  
JAIL\_POSITION  
when 8  
READING\_POSITION  
when 9  
BOARDWALK\_POSITION  
else  
# This card does not have an effect on position  
cur\_pos  
end  
end

Each time the block is called, a “card” is chosen at rand, and the  
player’s new position is returned. In many cases (i.e. the `else`  
statement), the current position is returned; that is, there is no  
addition movement beyond the where the player is located.

In most other cases, constants (e.g. ILLINOIS\_POSITION) are used to  
provide the new location. The `case` statement is a decent,  
straightforward mechanism for sorting this out. (I can imagine other  
ways to do this, but I leave those as an exercise for the reader. Ha.)

What I will mention here are how those constants are initialized, and  
also the use of some hardcoded numbers. For the latter, the approach  
that worked for the “nearest utility” case would be suitable for the  
railroads. (Personally, I’d probably turn it into a mathematical  
formula.) But even assuming we turn the hardcoded numbers into  
constants, how are those defined?

GO\_POSITION = 0  
ILLINOIS\_POSITION = 24  
BOARDWALK\_POSITION = 39

Now, normally, this would be the place to put the literal integers;  
however, as mentioned before, this is another “master” in generating  
board position numbers. All this information is present in the array  
`PROPERTY_NAMES`. To make use of that master array, rather than  
providing redundant information, I would do this:

GO\_POSITION = PROPERTY\_NAMES.index(“GO”)  
ILLINOIS\_POSITION = PROPERTY\_NAMES.index(“Illinois”)  
BOARDWALK\_POSITION = PROPERTY\_NAMES.index(“Boardwalk”)

Likewise,

BOARD\_SIZE = PROPERTY\_NAMES.size

instead of:

BOARD\_SIZE = 40

Note, while the property names are being repeated here, it is (in a  
way) not redundant information, since it is not acting as an authority  
for property names (as the literal integers were). Also note, with the  
flexibility of Ruby, this could be made even less redundant and more  
compact, but that’s not something I’m going to get into here.

Thanks for the submission, Daniel! It was good fun to see your approach.

And thanks for everyone during my stint as quizmaster. I look forward  
to seeing more great quizzes in the future from quizmaster, version  
3.0! 😃

---

<div class="post-metadata">

**Author:** ![mansfiem](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/m/fbc32d/32.png) [@mansfiem](https://www.ruby-forum.com/u/mansfiem)\
**Post date:** [January 23, 2009, 9:20am UTC](https://www.ruby-forum.com/t/monopoly-walker-188/157926/5 "2009-01-23T09:20:32Z")

</div>

On Thu, Jan 22, 2009 at 3:26 PM, Matthew M. [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) wrote:

> (Personally, I’d probably turn it into a mathematical formula.) But even

For the railroads, maybe use ruby’s truncation of integers…

(x + 5) / 10 \* 10 + 5

…which is weird to simplify because of the truncation, of course.

For the Monopoly board layout, I’ve been a little confused trying to  
come up with a mathematical pattern for it, starting with a folded  
board and moving on to numbers and fractals. If there’s a pattern  
there, I’m officially not and never even came close to be “rain man”  
🙂

It may be an overlaid clockwise vs. counter-clockwise position setup  
to make the moves more random, but I still don’t see it in this game.  
Darn! I really wanted to master this quiz elegantly!

I’ll do this one on my own time, I think, and thank you, Matthew for  
hosting the quiz. Especially this one, because it made me rethink my  
own version of the Monopoly game.

Cheers,  
Todd
