Skip to content

Karuba Junior

Karuba Junior is a cooperative tile-placement adventure game. Having lost twice in a row I wanted to calculate the odds of winning this game.

As expected for a child games the rules are dead simple. The players take turns in drawing a card and using it to extend or end one of the 4 starting roads. Tigers and treasures end a road. Forks and crossroads add one and two additional roads, respectively, as long as they are placed in such a way that none of the roads are blocked by another card, which in practice is always possible. The game is won if all 3 treasures are found. The game is lost if there is no open road left, or if the pirates advanced a total of 9 fields, which happens by drawing the corresponding pirate cards.

Let's find the odds of winning the game through Monte Carlo. We need 3 counters: the number of treasures found, the number of open roads, and the number of pirate moves. For each card we define how the counters change in form of a 3-component vector. Then we can accumulate the changes in the random order they are drawn and determine which win/loss condition occurs first.

There are 28 cards: * 3 treasures * 3 tigers * 11 straight and curved roads * 3 forks * 1 crossroads * 6 pirate cards: 3 cards with one movement point, 2 two's and 1 three

import numpy as np

# card = (#treasure, #roads, #pirates)
cards = np.concatenate([
    np.repeat([[1, -1, 0]], 3, axis=0),  # treasure
    np.repeat([[0, -1, 0]], 3, axis=0),  # tiger
    np.repeat([[0, 0, 0]], 11, axis=0),  # simple road
    np.repeat([[0, 1, 0]], 4, axis=0),  # fork
    np.repeat([[0, 2, 0]], 1, axis=0),  # crossroad
    np.repeat([[0, 0, 1]], 3, axis=0),  # pirate 1
    np.repeat([[0, 0, 2]], 2, axis=0),  # pirate 2
    np.repeat([[0, 0, 3]], 1, axis=0),  # pirate 3
])

def simulate():
    """Simulate a game and determine the win or loss condition"""
    np.random.shuffle(cards)

    # all counter start from 0
    (treasures, roads, pirates) = cards.cumsum(axis=0).T

    # round when all 3 treasures found
    i_treasure = np.where(treasures == 3)[0][0]

    # round when pirates arrive at the beach
    i_pirates = np.where(pirates >= 9)[0][0]

    # check if all roads are blocked
    if (roads == -4).any():
        i_roads = np.where(roads <= -4)[0][0]
    else:
        i_roads = np.inf

    # note: the case that the third treasure also closes the last road is correctly registered as a win
    return np.argmin([i_treasure, i_roads, i_pirates])
n = 100000
res = [simulate() for i in range(n)]
frequency = np.bincount(res) / n
print('Probability of outcomes')
print(f'Win:                  p={frequency[0]:.3f}')
print(f'Loss (roads blocked): p={frequency[1]:.3f}')
print(f'Loss (pirates):       p={frequency[2]:.3f}')
Probability of outcomes
Win:                  p=0.508
Loss (roads blocked): p=0.052
Loss (pirates):       p=0.441

So we have a ~50% win probability. Pirates are the most likely reason for losing. Losing due to blocked roads happens rarely if played correctly, but this is a game for 4-8 year olds after all.