from otree.api import ( Currency, cu, currency_range, models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, ExtraModel, WaitPage, Page, read_csv, url_of_static_file, ) import units import shared_out doc = '' class C(BaseConstants): # built-in constants NAME_IN_URL = 'irrigation' PLAYERS_PER_GROUP = 4 NUM_ROUNDS = 9 # user-defined constants INITIAL_BALANCE = 5 MAX_INVESTMENT = 3 MAX_SPENDING = 5 MIN_INVESTMENT_FOR_WATER = 2 INVESTMENT_THRESHOLD_MAX = 8 MAX_GROUNDWATER = 12 WATER_MULTIPLIER = 2 WATER_OFFSET = 4 class Subsession(BaseSubsession): surface_water = models.IntegerField() class Group(BaseGroup): total_investment = models.IntegerField() groundwater_produced = models.IntegerField() total_water_available = models.IntegerField() water_outflow = models.IntegerField() class Player(BasePlayer): investment = models.IntegerField(initial=0, label='Your investment in groundwater (0-3 tokens)', max=C.MAX_INVESTMENT, min=0) card_choice = models.StringField(choices=['None', 'A1', 'A2', 'A3', 'A4', 'A5'], initial='None', label='Choose your activity card') water_received = models.IntegerField() card_success = models.BooleanField() round_payoff = models.IntegerField() cumulative_balance = models.IntegerField() water_input_needed = models.IntegerField() water_output_released = models.IntegerField() card_cost = models.IntegerField() card_output = models.IntegerField() selected_round_for_payment = models.IntegerField() # built-in hook function(s) (called automatically by oTree) # def creating_session(subsession: Subsession): for player in subsession.get_players(): player.cumulative_balance = C.INITIAL_BALANCE subsession.surface_water = subsession.session.config['surface_water'] # # the below function(s) are user-defined, not called by oTree # def calculate_groundwater(total_investment): if total_investment < C.MIN_INVESTMENT_FOR_WATER: return 0 elif total_investment <= C.INVESTMENT_THRESHOLD_MAX: return C.WATER_MULTIPLIER * total_investment - C.WATER_OFFSET else: return C.MAX_GROUNDWATER def get_card_properties(card_name): return card_data()[card_name] def card_data(): return { "A1": {"water_input": 0, "cost": 2, "water_output": 2, "financial_output": 1}, "A2": {"water_input": 0, "cost": 1, "water_output": 1, "financial_output": 1}, "A3": {"water_input": 2, "cost": 1, "water_output": 1, "financial_output": 3}, "A4": {"water_input": 2, "cost": 1, "water_output": 0, "financial_output": 4}, "A5": {"water_input": 3, "cost": 2, "water_output": 0, "financial_output": 6}, "None": {"water_input": 0, "cost": 0, "water_output": 0, "financial_output": 0} } def process_water_flow(group: Group): water_available = group.total_water_available for player in group.get_players(): card_props = get_card_properties(player.card_choice) player.water_input_needed = card_props['water_input'] player.card_cost = card_props['cost'] player.card_output = card_props['financial_output'] player.water_output_released = card_props['water_output'] if water_available >= player.water_input_needed: player.card_success = True water_available -= player.water_input_needed water_available += player.water_output_released player.water_received = player.water_input_needed else: player.card_success = False player.water_received = water_available previous_balance = player.in_round(player.round_number - 1).cumulative_balance if player.round_number > 1 else C.INITIAL_BALANCE if player.card_success: player.round_payoff = player.card_output - player.card_cost - player.investment else: player.round_payoff = 0 - player.card_cost - player.investment player.cumulative_balance = previous_balance + player.round_payoff group.water_outflow = water_available # class Investment(Page): form_model = 'player' form_fields = ['investment'] @staticmethod def vars_for_template(player: Player): return dict( round_number=player.round_number, position=player.id_in_group, balance=player.in_round(player.round_number - 1).cumulative_balance if player.round_number > 1 else C.INITIAL_BALANCE ) class AfterInvestment(WaitPage): wait_for_all_groups = True @staticmethod def after_all_players_arrive(subsession: Subsession): for group in subsession.get_groups(): group.total_investment = sum([p.investment for p in group.get_players()]) group.groundwater_produced = calculate_groundwater(group.total_investment) group.total_water_available = subsession.surface_water + group.groundwater_produced class CardChoice(Page): form_model = 'player' form_fields = ['card_choice'] @staticmethod def vars_for_template(player: Player): cards = card_data() return dict( position=player.id_in_group, cards=cards, groundwater=player.group.groundwater_produced, total_water=player.group.total_water_available ) @staticmethod def error_message(player: Player, values): card_choice = values['card_choice'] card_props = get_card_properties(card_choice) previous_balance = player.in_round(player.round_number - 1).cumulative_balance if player.round_number > 1 else C.INITIAL_BALANCE total_cost = player.investment + card_props['cost'] if total_cost > C.MAX_SPENDING: return f"Total spending ({total_cost} tokens) exceeds maximum of {C.MAX_SPENDING} tokens" if total_cost > previous_balance: return f"Insufficient balance. You have {previous_balance} tokens but need {total_cost} tokens" class AfterCardChoice(WaitPage): wait_for_all_groups = True @staticmethod def after_all_players_arrive(subsession: Subsession): for group in subsession.get_groups(): process_water_flow(group) class Results(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): import random if player.round_number == C.NUM_ROUNDS: player.selected_round_for_payment = random.randint(1, C.NUM_ROUNDS) player.participant.payoff = player.in_round(player.selected_round_for_payment).round_payoff return True @staticmethod def vars_for_template(player: Player): return dict( position=player.id_in_group, round_number=player.round_number, total_rounds=C.NUM_ROUNDS ) page_sequence = [Investment, AfterInvestment, CardChoice, AfterCardChoice, Results]