from otree.api import *
from decimal import *
doc = """
The actual game: after 20 rounds randomisation until round 30.
"""
class C(BaseConstants):
NAME_IN_URL = 'the_game'
PLAYERS_PER_GROUP = 5
NUM_ROUNDS = 30 # will be randomly terminated between 20 and 30
ENDOWMENT = 10
MULTIPLIER = 3
INSTRUCT_TEMPLATE = 'ChoiceGame/instructions.html'
PROBABILITY_END = 50 # the probability to end is 100 - the Constant (50)
ROUND_END = 20
SELECTED_ROUNDS = 5
EXCHANGE_RATE = 0.1
class Subsession(BaseSubsession):
pass
def creating_session(subsession: Subsession):
session = subsession.session
session.treat = session.config['treat']
for player in subsession.get_players():
player.participant.income_game = 0
player.participant.is_dropout = False
class Group(BaseGroup):
total_contribution = models.CurrencyField(doc="""Group: How much was contributed to group account""")
individual_share = models.FloatField(doc="""Group: individual return from group account""")
embezzlement = models.CurrencyField(doc="""Group: embezzlement by Participant B""")
comm_action = models.CurrencyField(doc="""Group: communicated embezzlement level""")
comm_contribution = models.CurrencyField(doc="""Group: communicated contribution level""")
communication_choice = models.IntegerField(
doc="""Group: Choice of communication option""",
initial=999
)
number_in_round = models.IntegerField(doc="""Group: random number drawn from round 21 to determine end of game""")
number_of_dropouts = models.IntegerField(initial=0)
dictator_out = models.BooleanField(initial=False)
class Player(BasePlayer):
# For Citizens
# -------------------------------------------------------------------------------------
contribution = models.CurrencyField(
doc="""Player: individual contribution to group account by participant of Type A""",
label="How many Points do you want to transfer to the group account?",
min=cu(0),
max=C.ENDOWMENT
)
# type = models.StringField(
# doc="""Player: assignment of Types to players"""
# )
belief_contribution = models.CurrencyField(
doc="""Player: assignment of Types to players""",
label="How many Points were contributed to the group account by all players of type A (including you)?",
min=cu(0),
max=cu(40)
)
belief_dictator = models.CurrencyField(
label="What do you think is the total number of Points in the group account after player B's decision?",
min=cu(-40),
max=cu(10)
)
# Variables for Belief Elicitation:
belief_diff_group = models.CurrencyField(
)
belief_pay_group = models.BooleanField(
)
belief_diff_dic = models.CurrencyField(
)
belief_pay_dic = models.BooleanField(
)
belief_pay = models.IntegerField(initial=0
)
t_out_contr = models.BooleanField(initial=False)
t_out_beliefs = models.BooleanField(initial=False)
# For Dictator
# -------------------------------------------------------------------------------------
dictator_action = models.CurrencyField(
# needs to be adjusted to have dynamic bounds + define whether it is positive or negative
doc="""Amount dictator contribute (positive) or embezzle (negative) from group account""",
max=C.ENDOWMENT,
# min is dynamically determined by the function dictator_action_min
)
communicated_action = models.CurrencyField(
# the action that will be communicated by the dictator to the public
label="What action do you want to communicate to the group?",
min=cu(-40),
max=cu(10),
blank=True,
)
# Choice Field for which Communication to communicate
# I then think it's easiest to just define three different Result pages and condition it on this decision.
# To have it available for all players it must be on the group level.
dictator_choice = models.IntegerField(
label="Please choose which type of communication you want to choose.",
widget=widgets.RadioSelect,
choices=[
[1, 'Communicate No Further Information'],
[2, 'Communicate True Combination'],
[3, 'Communicate Yourself'],
]
)
t_out_action = models.BooleanField(initial=False)
t_out_choice = models.BooleanField(initial=False)
t_out_comm = models.BooleanField(initial=False)
# For Both
# -------------------------------------------------------------------------------------
# Setting default of finished rounds to False which might be algorithmically changes
float_payoff = models.FloatField()
finished_rounds = models.BooleanField(initial=False)
timeout_count = models.IntegerField(initial=0)
is_dropout = models.BooleanField(initial=False)
# Results fields
selected_rounds_game = models.LongStringField(initial='[]')
selected_game_income = models.LongStringField(initial='[]')
selected_rounds_beliefs = models.LongStringField(initial='[]')
selected_beliefs_income = models.LongStringField(initial='[]')
total_income_decisions = models.FloatField()
total_income_beliefs = models.CurrencyField(initial=0)
total_income = models.FloatField()
# ------------------------------------------------------------------------------------------- #
# FUNCTIONS #
# ------------------------------------------------------------------------------------------- #
# Dynamically determining the maximum that a dictator can embezzle.
def dictator_action_min(player):
group = player.group
return -group.total_contribution
def role_assignment(group):
if group.round_number == 1:
players = group.get_players()
num_players = len(players)
if num_players == C.PLAYERS_PER_GROUP:
# Shuffle the players
import random
random.shuffle(players)
# Assign the first four players as "Citizen"
for i in range(num_players - 1):
players[i].participant.type = "Citizen"
# Assign the last player as "Dictator"
players[-1].participant.type = "Dictator"
# else:
# players = group.get_players()
# num_players = len(players)
# for i in range(num_players):
# players[i].type = players[i].in_round(1).type
def aggregate_contributions(group):
players = group.get_players()
citizens = [p for p in players if p.participant.type == 'Citizen']
contributions = [p.contribution for p in citizens]
group.total_contribution = sum(contributions)
def set_payoffs(group):
players = group.get_players()
# Converting Embezzlement to Group Variable
citizens = [p for p in players if p.participant.type == 'Citizen']
dictator = [p for p in players if p.participant.type == 'Dictator']
embezzlement = [-p.dictator_action for p in dictator]
group.embezzlement = sum(embezzlement)
# Return from Public Good for all players in Round
group.individual_share = (float((group.total_contribution - group.embezzlement)) * C.MULTIPLIER) / C.PLAYERS_PER_GROUP
# Define income from game for citizens and the dictator
for p in citizens:
p.payoff = C.ENDOWMENT - p.contribution + group.individual_share
p.float_payoff = float(C.ENDOWMENT - p.contribution) + group.individual_share
# Define payoff for Belief elicitation.
# Belief on group contribution
p.belief_diff_group = p.belief_contribution - p.group.total_contribution
p.belief_pay_group = abs(int(p.belief_diff_group)) < 3
# Belief on dictator contribution
p.belief_diff_dic = p.belief_dictator + p.group.embezzlement
p.belief_pay_dic = abs(int(p.belief_diff_dic)) < 3
# Belief correct on both dimensions?
p.belief_pay = ((p.belief_pay_group + p.belief_pay_dic) == 2)*3
for p in dictator:
p.payoff = C.ENDOWMENT - p.dictator_action + group.individual_share
p.float_payoff = float(C.ENDOWMENT - p.dictator_action) + group.individual_share
# Define Communication Choice on Group Level
def choice_of_communication(group):
players = group.get_players()
dictator = [p for p in players if p.participant.type == 'Dictator']
temp = [p.dictator_choice for p in dictator]
group.communication_choice = sum(temp)
# Define communicated Levels on Group Level
def communicated_levels(group):
players = group.get_players()
dictator = [p for p in players if p.participant.type == 'Dictator']
if group.communication_choice == 3 or group.session.treat == 3:
c_action = [p.communicated_action for p in dictator]
group.comm_action = sum(c_action)
group.comm_contribution = group.total_contribution - group.embezzlement - group.comm_action
# Define Rules to end the Game after 20 rounds
def last_round_determination(group):
import random
if group.round_number > C.ROUND_END:
group.number_in_round = random.randint(0, 100)
if group.round_number > C.ROUND_END and group.number_in_round > C.PROBABILITY_END:
print('ending game')
for p in group.get_players():
p.finished_rounds = True
# Define Selection of Payout Relevant Rounds
def final_payoff(group):
import random
players = group.get_players()
for p in players:
if p.finished_rounds or (group.number_of_dropouts >= 3 and p.round_number >= C.SELECTED_ROUNDS):
# select rounds and then add together...
upper_bound = p.round_number + 1
selected_game_rounds = random.sample(range(1, upper_bound), C.SELECTED_ROUNDS)
selected_belief_rounds = random.sample(range(1, upper_bound), C.SELECTED_ROUNDS)
# Get the relevant incomes/payoffs of the selected rounds
selected_income = 0
selected_beliefs = 0
selected_round_incomes = []
selected_round_beliefs = []
for s_round in selected_game_rounds:
round_income = p.in_round(s_round).float_payoff
selected_round_incomes.append(round_income) # Store the payoff for the selected round
selected_income += round_income
print(selected_income)
# for each s_round I want to save it to a numerated player variable...
for s_round in selected_belief_rounds:
round_beliefs = p.in_round(s_round).belief_pay
selected_round_beliefs.append(round_beliefs)
selected_beliefs += round_beliefs
print(selected_beliefs)
# save the selected rounds and corresponding payoffs as player variables
p.selected_rounds_game = ', '. join(str(e) for e in selected_game_rounds)
print(p.selected_rounds_game)
p.selected_rounds_beliefs = ', '. join(str(e) for e in selected_belief_rounds)
print(p.selected_rounds_beliefs)
p.selected_game_income = ', '. join(str(e) for e in selected_round_incomes)
print(p.selected_game_income)
p.selected_beliefs_income = ', '. join(str(e) for e in selected_round_beliefs)
print(p.selected_beliefs_income)
p.total_income_decisions = selected_income
p.total_income_beliefs = selected_beliefs
p.total_income = p.total_income_decisions + float(p.total_income_beliefs)
# on participant level:
p.participant.income_decisions = p.total_income_decisions
p.participant.income_beliefs = p.total_income_beliefs
p.participant.income_game = p.total_income
# Define Function to exclude inactive players:
def exclude_inactive(player):
# determine whether individual player is excluded
if player.timeout_count == 3:
player.participant.is_dropout = True
player.is_dropout = True
# Make sure when know if dictator dropped out
if player.participant.type == 'Dictator':
player.group.dictator_out = True
# record how many participants are excluded
players = player.group.get_players()
drop_outs = [p for p in players if p.participant.is_dropout]
player.group.number_of_dropouts = len(drop_outs)
# ------------------------------------------------------------------------------------------- #
# PAGES #
# ------------------------------------------------------------------------------------------- #
class GroupWaitPage(WaitPage):
@staticmethod
def is_displayed(player):
return player.round_number == 1
group_by_arrival_time = True
after_all_players_arrive = role_assignment
title_text = "Thanks for joining"
body_text = "Please wait for all other members to join"
class RolePage(Page):
@staticmethod
def is_displayed(player):
return player.round_number == 1
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
return 15
@staticmethod
def vars_for_template(player):
parb = player.participant.type == "Dictator"
para = player.participant.type == "Citizen"
return dict(
parB=parb,
parA=para
)
# ------------------------ CITIZEN PAGES -------------------------- #
class CitizenPage(Page):
@staticmethod
def is_displayed(player):
return player.participant.type == "Citizen"
form_model = 'player'
form_fields = ['contribution']
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
if player.round_number == 1:
return 40
else:
return 30
@staticmethod
def before_next_page(player, timeout_happened):
# Get the number of time-outs from last round
if player.round_number > 1:
last_round = player.round_number - 1
player.timeout_count = player.in_round(last_round).timeout_count
# Implement the rules for contribution when player timed out
if timeout_happened and not player.participant.is_dropout:
player.t_out_contr = True
# Calculate whether player should be excluded from game
player.timeout_count += 1
# Exclusion criterion
exclude_inactive(player)
# Implement rule for when people dropped out
if timeout_happened and not player.participant.is_dropout:
if player.round_number == 1:
player.contribution = 0
else:
player.contribution = player.in_round(player.round_number - 1).contribution
elif timeout_happened and player.participant.is_dropout:
player.t_out_contr = True
last_round = player.round_number - 1
last_player = player.in_round(last_round)
player.contribution = round(1/3 * last_player.group.individual_share, 0)
else:
player.timeout_count = 0
class DictatorWaitPage(WaitPage):
after_all_players_arrive = aggregate_contributions
body_text = "Please wait for all Participants of Type A to make their transfer decision."
class CitizenBeliefs(Page):
@staticmethod
def is_displayed(player):
return player.participant.type == "Citizen"
form_model = 'player'
form_fields = ['belief_contribution', 'belief_dictator']
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
return 40
@staticmethod
def before_next_page(player, timeout_happened):
if timeout_happened:
player.t_out_beliefs = True
player.belief_contribution = 0
player.belief_dictator = 0
# Calculate whether player should be excluded from game
player.timeout_count += 1
else:
player.timeout_count = 0
exclude_inactive(player)
# ------------------------ DICTATOR PAGES -------------------------- #
class DictatorPage(Page):
@staticmethod
def is_displayed(player):
return player.participant.type == "Dictator"
form_model = 'player'
form_fields = ['dictator_action']
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
return 40
@staticmethod
def js_vars(player: Player):
return dict(potsize=player.group.total_contribution,
endowment=C.ENDOWMENT)
@staticmethod
def vars_for_template(player):
maxpot = player.group.total_contribution
return dict(
min=-int(maxpot),
potsize=int(maxpot)
)
@staticmethod
def before_next_page(player, timeout_happened):
# Get the number of time-outs from last round
if player.round_number > 1:
last_round = player.round_number - 1
last_player = player.in_round(last_round)
player.timeout_count = last_player.timeout_count
# Variable for defining action if timeout happened
if timeout_happened:
last_contribution = last_player.group.total_contribution
last_action = last_player.dictator_action
# Implement the rules for contribution when player timed out
if timeout_happened and not player.participant.is_dropout:
player.t_out_action = True
# Calculate whether player should be excluded from game
player.timeout_count += 1
exclude_inactive(player)
if timeout_happened and not player.participant.is_dropout:
if player.round_number == 1:
player.dictator_action = 0
elif last_contribution == 0:
player.dictator_action = last_action
else:
last_action_share = int(last_action)/int(last_contribution)
action_share = round(last_action_share * int(player.group.total_contribution), 0)
player.dictator_action = action_share
# Calculate whether player should be excluded from game
player.timeout_count += 1
elif timeout_happened and player.participant.is_dropout:
player.t_out_action = True
player.dictator_action = round(1/3 * last_player.group.individual_share, 0)
else:
player.timeout_count = 0
# @staticmethod
# def app_after_this_page(player, upcoming_apps):
# if player.participant.is_dropout:
# return upcoming_apps[0]
class DictatorChoice(Page):
@staticmethod
def is_displayed(player):
return player.participant.type == "Dictator" and player.session.treat == 4
form_model = 'player'
form_fields = ['dictator_choice', 'communicated_action']
@staticmethod
def vars_for_template(player):
realpot = player.group.total_contribution + player.dictator_action
return dict(
max_contr=min(int(realpot), 10),
max_embezzle=int(realpot)-10*(C.PLAYERS_PER_GROUP-1),
realcontr=int(player.group.total_contribution),
realaction=int(player.dictator_action)
)
@staticmethod
def js_vars(player: Player):
return dict(real_pot=player.group.total_contribution + player.dictator_action,
realcontr=player.group.total_contribution,
)
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
if player.round_number == 1:
return 50
else:
return 45
@staticmethod
def before_next_page(player, timeout_happened):
choice_of_communication(player.group)
if timeout_happened:
player.t_out_choice = True
if player.round_number == 1:
player.dictator_choice = 2
player.group.communication_choice = player.dictator_choice
else:
# Choose No Com if previous round No Com, o/w True Info
last_round = player.round_number - 1
last_player = player.in_round(last_round)
player.dictator_choice = last_player.dictator_choice
player.group.communication_choice = player.dictator_choice
if last_player.dictator_choice == 3:
player.dictator_choice = 2
player.group.communication_choice = player.dictator_choice
# Calculate whether player should be excluded from game
player.timeout_count += 1
else:
player.timeout_count = 0
exclude_inactive(player)
# @staticmethod
# def app_after_this_page(player, upcoming_apps):
# if player.participant.is_dropout:
# return upcoming_apps[0]
class DictatorCommunication(Page):
@staticmethod
def is_displayed(player):
return player.participant.type == "Dictator" and player.session.treat == 3
form_model = 'player'
form_fields = ['communicated_action']
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
if player.round_number == 1:
return 50
else:
return 40
# When the total pot of contributions is smaller than 10,
# then giving 10 would imply a negative avg contr rate.
# This obviously doesn't make much sense... thus, we can define the max contribution as min of {potsize,10}.
# Now let's think about the maximum embezzlement. The final communicated amount must be equal to the real pot.
# The maximum amount that can be contributed is (# of type A)*10. Thus, the maximum embezzlement is this - real pot
# However, the implied contribution rate by Type A's given the realpot cannot be higher than (# of type A)*10.
# Wenn ich restricten müsse, eine höher contribution zu kreierien:max(-player.group.total_contribution, int(realpot)-10*(C.PLAYERS_PER_GROUP-1))
@staticmethod
def vars_for_template(player):
realpot = player.group.total_contribution + player.dictator_action
return dict(
max_contr=min(int(realpot), 10),
max_embezzle=int(realpot)-10*(C.PLAYERS_PER_GROUP-1),
realcontr=int(player.group.total_contribution),
realaction=int(player.dictator_action)
)
@staticmethod
def js_vars(player: Player):
return dict(real_pot=player.group.total_contribution + player.dictator_action,
realcontr=player.group.total_contribution,
)
@staticmethod
def before_next_page(player, timeout_happened):
if timeout_happened:
player.t_out_comm = True
# If the dictator doesn't decide what to communicate, we will communicate the truth
player.communicated_action = player.dictator_action
# Calculate whether player should be excluded from game
player.timeout_count += 1
else:
player.timeout_count = 0
exclude_inactive(player)
# @staticmethod
# def app_after_this_page(player, upcoming_apps):
# if player.participant.is_dropout:
# return upcoming_apps[0]
# ------------------------ RESULTS PAGES -------------------------- #
class ResultsWaitPage(WaitPage):
body_text = "Please wait for all participants to make their decision."
@staticmethod
def after_all_players_arrive(group: Group):
set_payoffs(group)
communicated_levels(group)
last_round_determination(group)
final_payoff(group)
class ResultsCitizenUnverified(Page):
@staticmethod
def is_displayed(player):
return player.participant.type == "Citizen" and (player.session.treat == 2 or player.group.communication_choice == 1)
@staticmethod
def vars_for_template(player):
return dict(
private_acc=int(cu(10)-player.contribution),
payoff=format(Decimal(player.float_payoff), '.2f')
)
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
return 30
class ResultsCitizenTransparency(Page):
@staticmethod
def is_displayed(player):
return player.participant.type == "Citizen" and (player.session.treat == 1 or player.group.communication_choice == 2)
@staticmethod
def vars_for_template(player):
return dict(
abs_embezz=int(abs(player.group.embezzlement)),
total_contribution=int(player.group.total_contribution),
group_embezzlement=int(player.group.embezzlement),
private_acc=int(cu(10)-player.contribution),
payoff=format(Decimal(player.float_payoff), '.2f')
)
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
return 30
class ResultsCitizenChosen(Page):
@staticmethod
def is_displayed(player):
return (player.participant.type == "Citizen") and (player.session.treat == 3 or player.group.communication_choice == 3)
@staticmethod
def vars_for_template(player):
return dict(
abs_embezz=int(abs(player.group.embezzlement)),
abs_comm=int(abs(player.group.comm_action)),
private_acc=int(cu(10)-player.contribution),
payoff=format(Decimal(player.float_payoff), '.2f'),
comm_contribution=int(player.group.comm_contribution)
)
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
return 30
class ResultsDictator(Page):
@staticmethod
def is_displayed(player):
return player.participant.type == "Dictator"
@staticmethod
def vars_for_template(player):
return dict(
abs_embezz=int(abs(player.group.embezzlement)),
total_contribution=int(player.group.total_contribution),
group_embezzlement=int(player.group.embezzlement),
private_acc=int(cu(10)-player.dictator_action),
payoff=format(Decimal(player.float_payoff), '.2f')
)
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
return 30
class ProceedPage(Page):
@staticmethod
def is_displayed(player):
return player.round_number > C.ROUND_END
@staticmethod
def get_timeout_seconds(player):
if player.participant.is_dropout:
return 0.1
else:
return 15
class PayOff(Page):
@staticmethod
def is_displayed(player):
return player.finished_rounds or player.group.number_of_dropouts >= 3
@staticmethod
def vars_for_template(player):
# if C.ROUND_END > player.round_number >= C.SELECTED_ROUNDS:
# final_payoff(player.group)
if player.round_number < C.SELECTED_ROUNDS:
template_vars = {}
return template_vars # this avoids the below calculations
print(player.selected_rounds_game)
selected_rounds_game = list(player.selected_rounds_game.split(', '))
print(selected_rounds_game)
selected_rounds_beliefs = list(player.selected_rounds_beliefs.split(', '))
print(player.selected_game_income)
selected_game_income = list(player.selected_game_income.split(', '))
print(selected_game_income)
selected_beliefs_income = list(player.selected_beliefs_income.split(', '))
income_decisions = format(Decimal(player.participant.income_decisions), '.2f')
income_game = format(Decimal(player.participant.income_game), '.2f')
earning = format(Decimal(player.participant.income_game) * Decimal(C.EXCHANGE_RATE), '.2f')
ex_rate = format(10 * Decimal(C.EXCHANGE_RATE), '.2f')
template_vars = {
'income_decisions': income_decisions,
'income_game': income_game,
'earning': earning,
'ex_rate': ex_rate
}
for i in range(len(selected_rounds_game)):
template_vars[f'selected_round_game{i + 1}'] = selected_rounds_game[i]
template_vars[f'selected_round_belief{i + 1}'] = selected_rounds_beliefs[i]
template_vars[f'selected_game_income{i + 1}'] = format(Decimal(selected_game_income[i]), '.2f')
template_vars[f'selected_beliefs_income{i + 1}'] = selected_beliefs_income[i]
return template_vars
@staticmethod
def app_after_this_page(player, upcoming_apps):
return upcoming_apps[0]
# ------------------------------------------------------------------------------------------- #
# SEQUENCE #
# ------------------------------------------------------------------------------------------- #
page_sequence = [
GroupWaitPage,
RolePage,
CitizenPage,
DictatorWaitPage,
DictatorPage,
DictatorChoice,
CitizenBeliefs,
DictatorCommunication,
ResultsWaitPage,
ResultsCitizenTransparency,
ResultsCitizenUnverified,
ResultsCitizenChosen,
ResultsDictator,
ProceedPage,
PayOff
]