from otree.api import * doc = """ Intergenerational/evolutionary game. Each player receives the result from the previous player. Can be adapted to things other than money, like editing a message, (i.e. "broken telephone" game), etc. Just change value_in and value_out from CurrencyField to StringField, etc. """ class C(BaseConstants): NAME_IN_URL = 'intergenerational' PLAYERS_PER_GROUP = None NUM_ROUNDS = 5 TIMEOUT_SECONDS = 30 MIN_AMOUNT = cu(10) MAX_AMOUNT = cu(100) MULTIPLIER = 1.1 class Subsession(BaseSubsession): pass class Group(BaseGroup): live_player_id = models.IntegerField() # first player in the chain gets a donation of the max value. cur_value = models.CurrencyField(initial=Constants.max_amount) num_joined = models.IntegerField(initial=0) num_decided = models.IntegerField(initial=0) class Player(BasePlayer): num_joined = models.IntegerField() value_in = models.CurrencyField() endowment = models.CurrencyField() value_out = models.CurrencyField() expiry = models.FloatField() timeout_happened = models.BooleanField(initial=False) is_in_queue = models.BooleanField(initial=False) is_live = models.BooleanField(initial=False) def get_or_none(instance, field): try: return getattr(instance, field) except TypeError: return None class Intro(Page): @staticmethod def before_next_page(player: Player, timeout_happened): group = player.group group.num_joined += 1 player.num_joined = group.num_joined player.is_in_queue = True def live_method(player: Player, data): import time print('in live_method', data) group = player.group my_id = player.id_in_group broadcast = {} # this should get run whether the current player is the live one or not live_player_id = get_or_none(group, 'live_player_id') if live_player_id: # test if the other player is expired live_player = group.get_player_by_id(live_player_id) if live_player.expiry < time.time(): live_player.is_live = False live_player.timeout_happened = True group.live_player_id = None # make him reload so he gets marked as late broadcast[live_player.id_in_group] = dict(finished=True) if not (player.is_in_queue or player.is_live): return {my_id: dict(finished=True)} if player.is_live and 'value_out' in data: value = cu(data['value_out']) if not Constants.min_amount <= value <= player.endowment: return {my_id: dict(error='invalid amount')} player.value_out = value player.payoff = player.endowment - player.value_out player.is_in_queue = False player.is_live = False # set the next player group.cur_value = value group.num_decided += 1 group.live_player_id = None return {0: dict(reload=True)} if player.is_in_queue and get_or_none(group, 'live_player_id') is None: waiting_players = [p for p in group.get_players() if p.is_in_queue] # the next player(s) may have already dropped out. # so if this code relies on the next player being the one running this live method, # it will never advance through the chain. next_player = min(waiting_players, key=lambda p: p.num_joined) next_player.expiry = time.time() + Constants.timeout_seconds next_player.is_live = True group.live_player_id = next_player.id_in_group next_player.value_in = group.cur_value next_player.endowment = next_player.value_in * Constants.multiplier next_player.is_in_queue = False broadcast[next_player.id_in_group] = dict(reload=True) if player.is_live: broadcast[my_id] = dict( value_in=player.value_in, endowment=player.endowment, is_first=group.num_decided == 0 ) else: broadcast[my_id] = dict(wait=True) return broadcast class MyPage(Page): @staticmethod def is_displayed(player: Player): return player.is_in_queue or player.is_live @staticmethod def js_vars(player: Player): return dict(timeout_seconds=Constants.timeout_seconds) live_method = live_method @staticmethod def error_message(player: Player, values): if player.is_in_queue or player.is_live: return "Not ready to move on" class Results(Page): pass page_sequence = [Intro, MyPage, Results]