Index ¦ Archives ¦ Atom

Understanding "Obstgarten", the game

In [1]:
import random
import numpy as np
import matplotlib.pylab as plt

"Obstgarten" ("orchard")

is a board game (for kids).

Motivation

At some point we were genuinely wondering if the crow wins most of the times. Also, this has served as a greatly motivating example for statistically flavoured python classes I have been teaching.

Rules of the game

  • there are four trees, each tree has four fruit. There are red apples, green apples, pears, and plums (I didn't find a unicode symbol for plum, so it is peaches from now on)
  • there is a crow that needs to reach a gate. There are five steps necessary to reach the gate
  • players aim to harvest everything from all trees and aim to not let the crow reach the gate
  • There is a six sided dice that is rolled by the players. The six options of the dice are ๐ŸŽ, ๐Ÿ, ๐Ÿ, ๐Ÿ‘, ๐Ÿงบ, ๐Ÿฆ…
    • if any of the fruit ๐ŸŽ, ๐Ÿ, ๐Ÿ, ๐Ÿ‘ is rolled, the player is allowed to harvest a fruit as long as there the corresponding fruit in the tree
    • if the basked ๐Ÿงบ is rolled, the player is to pick a fruit from any fruit tree that has fruit left on it
    • if the crow ๐Ÿฆ… is rolled, the corw advances one step towards the gate
  • the crow wins, when it reaches the gate
  • the players win when all fruit are harvested

These are the options that can be rolled with the dice

In [2]:
dice_options = ['๐ŸŽ', '๐Ÿ', '๐Ÿ', '๐Ÿ‘', '๐Ÿงบ', '๐Ÿฆ…']
n_options = len(dice_options)
print(f"there are {n_options} options on the dice:")
for item in dice_options:
    print(item)
there are 6 options on the dice:
๐ŸŽ
๐Ÿ
๐Ÿ
๐Ÿ‘
๐Ÿงบ
๐Ÿฆ…

Rolling Dice in Python

generate n_rolls of the dice

In [3]:
n_rolls = 10
np.int_(np.round(np.random.random((n_rolls)) * n_options))
Out[3]:
array([4, 2, 1, 2, 0, 2, 3, 6, 3, 3])
In [4]:
n_rolls = 10
rolls = np.int_(np.round(np.random.random((n_rolls)) * n_options))
rolls
Out[4]:
array([6, 0, 4, 3, 4, 3, 1, 0, 3, 4])

check if the rolls of the dice are uniformly distributed

In [5]:
# roll dice
n_rolls = 10000
randomnumbers = np.int_(np.random.random((n_rolls)) * n_options)
# plot histogram
bins = np.arange(7)
plt.hist(randomnumbers, 
         bins=bins,
         density=True)
Out[5]:
(array([0.1649, 0.1645, 0.1672, 0.1684, 0.1708, 0.1642]),
 array([0, 1, 2, 3, 4, 5, 6]),
 <a list of 6 Patch objects>)

Start a Fresh Game

First: Setup a counter for the available fruit in each tree and for the number of steps left for the crow on its ladder

In [6]:
counter_available = {'๐ŸŽ':4,
                     '๐Ÿ':4, 
                     '๐Ÿ':4, 
                     '๐Ÿ‘':4,
                     '๐Ÿฆ…':5
                    }

based on experience, one game should be done after 50 rolls

In [7]:
n_rolls = 50
randomnumbers = np.int_(np.random.random((n_rolls)) * n_options)
In [8]:
randomnumbers
Out[8]:
array([5, 4, 5, 0, 3, 5, 4, 4, 3, 4, 4, 2, 0, 3, 3, 1, 3, 0, 2, 4, 2, 0,
       5, 1, 3, 1, 3, 5, 3, 1, 0, 5, 2, 5, 0, 0, 3, 4, 3, 5, 3, 3, 0, 5,
       3, 3, 2, 0, 4, 0])
In [9]:
print(type(dice_options))
doa = np.array(dice_options)
print(doa.shape)
print(type(doa))
<class 'list'>
(6,)
<class 'numpy.ndarray'>
In [10]:
doa
Out[10]:
array(['๐ŸŽ', '๐Ÿ', '๐Ÿ', '๐Ÿ‘', '๐Ÿงบ', '๐Ÿฆ…'], dtype='<U1')
In [11]:
randomnumbers
Out[11]:
array([5, 4, 5, 0, 3, 5, 4, 4, 3, 4, 4, 2, 0, 3, 3, 1, 3, 0, 2, 4, 2, 0,
       5, 1, 3, 1, 3, 5, 3, 1, 0, 5, 2, 5, 0, 0, 3, 4, 3, 5, 3, 3, 0, 5,
       3, 3, 2, 0, 4, 0])

this would reflect the outcome of n_rolls times rolling the dice in this game

In [12]:
doa[randomnumbers]
Out[12]:
array(['๐Ÿฆ…', '๐Ÿงบ', '๐Ÿฆ…', '๐ŸŽ', '๐Ÿ‘', '๐Ÿฆ…', '๐Ÿงบ', '๐Ÿงบ', '๐Ÿ‘', '๐Ÿงบ', '๐Ÿงบ', '๐Ÿ', '๐ŸŽ',
       '๐Ÿ‘', '๐Ÿ‘', '๐Ÿ', '๐Ÿ‘', '๐ŸŽ', '๐Ÿ', '๐Ÿงบ', '๐Ÿ', '๐ŸŽ', '๐Ÿฆ…', '๐Ÿ', '๐Ÿ‘', '๐Ÿ',
       '๐Ÿ‘', '๐Ÿฆ…', '๐Ÿ‘', '๐Ÿ', '๐ŸŽ', '๐Ÿฆ…', '๐Ÿ', '๐Ÿฆ…', '๐ŸŽ', '๐ŸŽ', '๐Ÿ‘', '๐Ÿงบ', '๐Ÿ‘',
       '๐Ÿฆ…', '๐Ÿ‘', '๐Ÿ‘', '๐ŸŽ', '๐Ÿฆ…', '๐Ÿ‘', '๐Ÿ‘', '๐Ÿ', '๐ŸŽ', '๐Ÿงบ', '๐ŸŽ'], dtype='<U1')
In [13]:
counter_available['๐Ÿ‘']
Out[13]:
4

this is playing the game once

In [14]:
for cur_i, cur_fruit in enumerate(doa[randomnumbers]):
    print(cur_fruit, cur_i)
    if cur_fruit == '๐Ÿงบ':
        # check which basket is still full
        counter_available_without_bird = counter_available.copy()
        del counter_available_without_bird['๐Ÿฆ…']
        v = list(counter_available_without_bird.values()) 
        k = list(counter_available_without_bird.keys()) 
        m = max(v)
        # randomly choose one if there are multiple maxima
        storage = []
        j=0
        for i in v:
            if i==m:
                storage.append(j)
            j+=1
        print(k[random.choice(storage)])
        counter_available[k[random.choice(storage)]] -= 1
    elif counter_available['๐Ÿฆ…'] == 0:
        print('the bird has won!')
        raise Exception
    else:
        print(counter_available)
        print(cur_fruit)
        counter_available[cur_fruit] -= 1
    
    counter_available_without_bird = counter_available.copy()
    del counter_available_without_bird['๐Ÿฆ…']
    v = list(counter_available_without_bird.values()) 
    if min(v)==0:
        print(counter_available_without_bird)
        winner = min(counter_available_without_bird, key=counter_available_without_bird.get) 
        print(winner + ' won!')
        break
๐Ÿฆ… 0
{'๐ŸŽ': 4, '๐Ÿ': 4, '๐Ÿ': 4, '๐Ÿ‘': 4, '๐Ÿฆ…': 5}
๐Ÿฆ…
๐Ÿงบ 1
๐Ÿ
๐Ÿฆ… 2
{'๐ŸŽ': 4, '๐Ÿ': 3, '๐Ÿ': 4, '๐Ÿ‘': 4, '๐Ÿฆ…': 4}
๐Ÿฆ…
๐ŸŽ 3
{'๐ŸŽ': 4, '๐Ÿ': 3, '๐Ÿ': 4, '๐Ÿ‘': 4, '๐Ÿฆ…': 3}
๐ŸŽ
๐Ÿ‘ 4
{'๐ŸŽ': 3, '๐Ÿ': 3, '๐Ÿ': 4, '๐Ÿ‘': 4, '๐Ÿฆ…': 3}
๐Ÿ‘
๐Ÿฆ… 5
{'๐ŸŽ': 3, '๐Ÿ': 3, '๐Ÿ': 4, '๐Ÿ‘': 3, '๐Ÿฆ…': 3}
๐Ÿฆ…
๐Ÿงบ 6
๐Ÿ
๐Ÿงบ 7
๐Ÿ
๐Ÿ‘ 8
{'๐ŸŽ': 2, '๐Ÿ': 3, '๐Ÿ': 3, '๐Ÿ‘': 3, '๐Ÿฆ…': 2}
๐Ÿ‘
๐Ÿงบ 9
๐Ÿ
๐Ÿงบ 10
๐Ÿ
๐Ÿ 11
{'๐ŸŽ': 2, '๐Ÿ': 2, '๐Ÿ': 2, '๐Ÿ‘': 2, '๐Ÿฆ…': 2}
๐Ÿ
๐ŸŽ 12
{'๐ŸŽ': 2, '๐Ÿ': 2, '๐Ÿ': 1, '๐Ÿ‘': 2, '๐Ÿฆ…': 2}
๐ŸŽ
๐Ÿ‘ 13
{'๐ŸŽ': 1, '๐Ÿ': 2, '๐Ÿ': 1, '๐Ÿ‘': 2, '๐Ÿฆ…': 2}
๐Ÿ‘
๐Ÿ‘ 14
{'๐ŸŽ': 1, '๐Ÿ': 2, '๐Ÿ': 1, '๐Ÿ‘': 1, '๐Ÿฆ…': 2}
๐Ÿ‘
{'๐ŸŽ': 1, '๐Ÿ': 2, '๐Ÿ': 1, '๐Ÿ‘': 0}
๐Ÿ‘ won!

Analyzing the Game

... by playing it n_games times

putting everything in to a function

... to be used for the analysis of the game

In [15]:
def play_orchard(n_games, counter_available_in):
    list_winners = []
    list_n_throws = []

    for cur_game in range(n_games):
        #print(cur_game,)
        counter_available = counter_available_in.copy()

        n_rolls = 50
        randomnumbers = np.int_(np.random.random((n_rolls)) * n_options)
        doa = np.array(dice_options)

        # the game
        for cur_i, cur_fruit in enumerate(doa[randomnumbers]):
            if cur_fruit == '๐Ÿงบ':
                # check which basket is still full
                counter_available_without_bird = counter_available.copy()
                del counter_available_without_bird['๐Ÿฆ…']
                v = list(counter_available_without_bird.values()) 
                k = list(counter_available_without_bird.keys()) 
                m = max(v)
                # randomly choose one if there are multiple maxima
                storage = []
                j=0
                for i in v:
                    if i==m:
                        storage.append(j)
                    j+=1
                counter_available[k[random.choice(storage)]] -= 1
            else:
                if counter_available[cur_fruit] > 0:
                    counter_available[cur_fruit] -= 1

            if counter_available['๐Ÿฆ…'] == 0:
                # print('the bird has won!')
                winner = '๐Ÿฆ…'
                list_winners.append(winner)
                list_n_throws.append(cur_i)
                break
            else:
                counter_available_without_bird = counter_available.copy()
                del counter_available_without_bird['๐Ÿฆ…']
                v = list(counter_available_without_bird.values()) 
                # Trees win if all trees have no more fruit
                if all(x == 0 for x in v):
                    list_winners.append('trees')
                    list_n_throws.append(cur_i)
                    break
    perc_win_trees = np.round(list_winners.count('trees') / n_games * 100, 0)
    perc_win_crow = np.round(list_winners.count('๐Ÿฆ…') / n_games * 100, 0)
    print(f"trees win {perc_win_trees}% of all games" )
    print(f"crow wins {perc_win_crow}% of all games")
    #print(list_winners.count('trees') + list_winners.count('๐Ÿฆ…'))

    return list_n_throws
    

Analyzing the Game

In [16]:
n_games = 100000
counter_available = {'๐ŸŽ':4,
                     '๐Ÿ':4, 
                     '๐Ÿ':4, 
                     '๐Ÿ‘':4,
                     '๐Ÿฆ…':5
                        }
list_n_throws = play_orchard(n_games, counter_available)
trees win 63.0% of all games
crow wins 37.0% of all games
In [17]:
plt.hist(np.array(list_n_throws),
         bins=np.arange(40),
        density=True)
plt.xlabel('number of dice-rolls until somebody wins')
plt.ylabel('relative frequency')
plt.grid(True)

Summary

In the default setup, the crow wins about in 37% of the games, the fruit harvester in 63% of the games played. Most games require about 20 times rolling the dice until somebody wins

What if there were six steps necessary for the crow to reach the gate

In [18]:
n_games = 100000
counter_available = {'๐ŸŽ':4,
                     '๐Ÿ':4, 
                     '๐Ÿ':4, 
                     '๐Ÿ‘':4,
                     '๐Ÿฆ…':6
                     }
list_n_throws = play_orchard(n_games, counter_available)
trees win 77.0% of all games
crow wins 23.0% of all games
In [19]:
plt.hist(np.array(list_n_throws),
         bins=np.arange(40),
        density=True)
plt.xlabel('number of dice-rolls until somebody wins')
plt.ylabel('relative frequency')
plt.grid(True)

In this altered setup, the crow wins about in 23% of the games, the players / fruit harvesters in 77% of the games played. Most games require about 22 times rolling the dice until somebody wins

What if there were four steps necessary for the crow to reach the gate

In [20]:
n_games = 100000
counter_available = {'๐ŸŽ':4,
                     '๐Ÿ':4, 
                     '๐Ÿ':4, 
                     '๐Ÿ‘':4,
                     '๐Ÿฆ…':4
                      }
list_n_throws = play_orchard(n_games, counter_available)
trees win 46.0% of all games
crow wins 54.0% of all games
In [21]:
plt.hist(np.array(list_n_throws),
         bins=np.arange(40),
        density=True)
plt.xlabel('number of dice-rolls until somebody wins')
plt.ylabel('relative frequency')
plt.grid(True)

This scenario would even out the chances of winning for the crow and the players (54% for the crow and 46% for the players. Also there would be slightly fewer dice-rolls necessary to achieve victory (on average slightly less than 20).

© Claus Haslauer. Built using Pelican. Theme by Giulio Fidente on github.