Project Euler Problem 84

In the game, Monopoly, the standard board is set up in the following way: !0084monopolyboard.png A player starts on the

Project Euler Problem 84

Solution

Answer: 101524

Let the Monopoly board squares be numbered $0$–$39$:

  • GO $=0$
  • JAIL $=10$
  • G2J $=30$
  • CC at $2,17,33$
  • CH at $7,22,36$

We must compute the stationary distribution of the Markov process when using two 4-sided dice, including:

  1. ordinary movement,
  2. triples of doubles $\to$ jail,
  3. CC/CH card effects,
  4. G2J,
  5. recursive effects such as “Go back 3 squares” landing on CC.

The required answer is the concatenation of the three most probable final squares.


Mathematical analysis

The clean way to solve this is to model Monopoly as a Markov chain.

A state must contain:

  • current board position $p\in{0,\dots,39}$,
  • number of consecutive doubles $d\in{0,1,2}$.

Thus there are:

$$40 \times 3 = 120$$

states.


1. Dice probabilities

With two 4-sided dice, each die is $1,2,3,4$.

There are $16$ equally likely outcomes.

The sums occur with probabilities:

Sum Ways
2 1
3 2
4 3
5 4
6 3
7 2
8 1

Doubles are:

$$(1,1),(2,2),(3,3),(4,4)$$

so:

$$P(\text{double})=\frac{4}{16}=\frac14$$

If a player already has two consecutive doubles and rolls another double, they go directly to jail without moving.


2. Community Chest

For CC squares:

  • 14 cards do nothing,
  • 1 card $\to$ GO,
  • 1 card $\to$ JAIL.

Thus:

$$P(\text{GO})=\frac1{16},\quad P(\text{JAIL})=\frac1{16},\quad P(\text{stay})=\frac{14}{16}$$


3. Chance cards

For CH squares:

10 cards move the player:

  • GO
  • JAIL
  • C1
  • E3
  • H2
  • R1
  • next R
  • next R
  • next U
  • back 3 squares

and 6 cards do nothing.

Thus each listed movement has probability $1/16$, except:

  • next R appears twice $\Rightarrow 2/16$.

4. Recursive movement handling

The subtle point is:

  • “Go back 3 squares” can land on a CC square.
  • That CC square may then move the player again.

Example:

$$36 \xrightarrow{\text{back 3}} 33$$

and $33$ is CC.

Therefore movement resolution must be recursive until a terminal square is reached.


5. Transition matrix

Define:

$$T_{ij}=P(\text{state }i\to \text{state }j)$$

This is a $120\times120$ stochastic matrix.

The stationary distribution $\pi$ satisfies:

$$\pi T = \pi$$

We can obtain it numerically by repeated multiplication:

$$\pi_{n+1}=\pi_n T$$

until convergence.

Finally, sum over the three double-count states for each board square:

$$P(p)=\sum_{d=0}^2 \pi(p,d)$$

Then sort probabilities.


Python implementation

from collections import defaultdict

# ------------------------------------------------------------
# Board constants
# ------------------------------------------------------------

GO = 0
JAIL = 10
G2J = 30

CC = {2, 17, 33}
CH = {7, 22, 36}

RAILWAYS = [5, 15, 25, 35]
UTILITIES = [12, 28]

# ------------------------------------------------------------
# Helpers for Chance cards
# ------------------------------------------------------------

def next_railway(pos):
    for r in RAILWAYS:
        if r > pos:
            return r
    return RAILWAYS[0]

def next_utility(pos):
    for u in UTILITIES:
        if u > pos:
            return u
    return UTILITIES[0]

# ------------------------------------------------------------
# Resolve all special square effects recursively.
#
# Returns a dictionary:
#   final_position -> probability
# ------------------------------------------------------------

def resolve_square(pos):

    # Go To Jail
    if pos == G2J:
        return {JAIL: 1.0}

    # Community Chest
    if pos in CC:
        out = defaultdict(float)

        out[GO] += 1/16
        out[JAIL] += 1/16
        out[pos] += 14/16

        return dict(out)

    # Chance
    if pos in CH:
        out = defaultdict(float)

        # 6 cards do nothing
        out[pos] += 6/16

        # Direct movement cards
        out[GO] += 1/16
        out[JAIL] += 1/16
        out[11] += 1/16      # C1
        out[24] += 1/16      # E3
        out[39] += 1/16      # H2
        out[5] += 1/16       # R1

        # Next railway (2 cards)
        out[next_railway(pos)] += 2/16

        # Next utility
        out[next_utility(pos)] += 1/16

        # Back 3 squares
        back = (pos - 3) % 40

        # Must recursively resolve
        for p, prob in resolve_square(back).items():
            out[p] += prob * (1/16)

        return dict(out)

    # Ordinary square
    return {pos: 1.0}

# ------------------------------------------------------------
# Build state indexing
# state = (position, consecutive_doubles)
# ------------------------------------------------------------

states = []
index = {}

for p in range(40):
    for d in range(3):
        index[(p, d)] = len(states)
        states.append((p, d))

N = len(states)

# ------------------------------------------------------------
# Transition matrix
# ------------------------------------------------------------

T = [[0.0] * N for _ in range(N)]

# Enumerate all dice outcomes
dice = []

for a in range(1, 5):
    for b in range(1, 5):
        dice.append((a, b))

for i, (pos, doubles_count) in enumerate(states):

    for a, b in dice:

        prob = 1 / 16
        is_double = (a == b)

        # Third consecutive double -> jail
        if is_double and doubles_count == 2:
            j = index[(JAIL, 0)]
            T[i][j] += prob
            continue

        # New doubles count
        new_doubles = doubles_count + 1 if is_double else 0

        # Move normally
        new_pos = (pos + a + b) % 40

        # Resolve square effects
        outcomes = resolve_square(new_pos)

        for final_pos, p2 in outcomes.items():

            # After movement effects, doubles count persists
            # unless sent to jail
            if final_pos == JAIL:
                nd = 0
            else:
                nd = new_doubles

            j = index[(final_pos, nd)]
            T[i][j] += prob * p2

# ------------------------------------------------------------
# Power iteration for stationary distribution
# ------------------------------------------------------------

dist = [0.0] * N
dist[index[(GO, 0)]] = 1.0

for _ in range(500):
    new_dist = [0.0] * N

    for i in range(N):
        for j in range(N):
            new_dist[j] += dist[i] * T[i][j]

    dist = new_dist

# ------------------------------------------------------------
# Combine probabilities by board square
# ------------------------------------------------------------

square_probs = [0.0] * 40

for (pos, d), idx in index.items():
    square_probs[pos] += dist[idx]

# Sort descending
ranking = sorted(
    range(40),
    key=lambda x: square_probs[x],
    reverse=True
)

top3 = ranking[:3]

answer = "".join(f"{x:02d}" for x in top3)

print(answer)

Code walkthrough

Board definitions

We define:

GO = 0
JAIL = 10
G2J = 30

and the CC/CH locations.


Recursive square resolution

The core function is:

resolve_square(pos)

It returns all possible final destinations and their probabilities.

Examples:

Community Chest

{GO:1/16, JAIL:1/16, pos:14/16}

Chance

Includes all movement cards.

The tricky part:

back = (pos - 3) % 40
for p, prob in resolve_square(back).items():

This correctly handles:

  • CH3 $\to$ back 3 $\to$ CC3
  • then CC3 may send player elsewhere.

State construction

We track:

(position, consecutive_doubles)

with 120 total states.


Triple doubles

This implements the special rule:

if is_double and doubles_count == 2:
    go directly to jail

before movement occurs.


Stationary distribution

We repeatedly apply:

new_dist[j] += dist[i] * T[i][j]

which converges to equilibrium probabilities.


Small sanity checks

For ordinary 6-sided Monopoly, known top squares are:

  1. JAIL
  2. E3
  3. GO

The problem statement gives modal string:

$$102400$$

Our algorithm reproduces the same ordering when switched to 6-sided dice, validating correctness.

With 4-sided dice:

  • doubles occur more often,
  • triple-doubles therefore happen more often,
  • jail becomes even more dominant.

This matches intuition.


Final verification

The computed top three squares for 4-sided dice are:

  1. JAIL = 10
  2. R2 = 15
  3. E3 = 24

giving:

$$10,15,24$$

formatted as a six-digit string:

$$101524$$

Answer: 101524