#1 by Floria
Hi all, I am working on a continuous-time public goods game, where players could change the contribution, but the payoff is accumulated at each specific time (e.g. 10s, 20s, 30s,...). I understand that the livepage works for the dynamics. The problem I face is how to synchronise the time within group, e.g., ideally, the contributions at 10s should affect the payoff, requiring a check if the current time on the client side is 10s. However, there may be discrepancies at client side timers, so the exact time of determining payoff may vary across group members. For example, it is possible that for player 1, the payoff is affected by the contributions at 10.1s, while for player 2, the payoff is affected by the contributions at 10.5s. I am fine with all group members' payoffs being affected by contributions at the same time point, even if this time point is not exactly at 10s. I was wondering if there are any approaches to achieve this. Thanks in advance!
#2 by BonnEconLab
> For example, it is possible that for player 1, the payoff is affected by the contributions at 10.1s, while for player 2, the payoff is affected by the contributions at 10.5s. The only way to avoid this, I think, is by using the server time. That is, you would have to live with the fact that there are both systematic and random delays between the clients’ time and the server time (and probably explain this to your participants). That is, a submission by one participant with a slow internet connection after 9.95 seconds and a submission by another participant with a faster internet connection after 10.03 seconds may have the same effect. Also, two submissions at the same time may have different effects, depending on network speed. I don’t know how one could implement a client-side-only timer that is both accurate and non-manipulable. What I mean is that if you implement a timer via JavaScript, then a participant’s timer will start anew each time they refresh the page. Hence, at least when the page loads, you will have to synchronize the client-side time with some universal time anyway. I would thus suggest to use a live method to transmit the offers to oTree and let oTree do all calculations of payoffs based on receipt of the offers according to the server time. To show the participants a timer that is as accurate as possible, you can synchronize the clients’ time with the server time whenever the live method is called.
#3 by Floria
Thanks @BonnEconLab! Yes the server side timer works synchronously for group members. I am still unclear about how to make the server check if the current time is the exact time specified (e.g. 10s, 20s, 30s,...). The live method cannot closely monitor every second as it is invoked only when the data is sent from the client side. The only solution I can think of is to have the client side send messages back to the server frequently to invoke the live method often. But it seems a heavy work and I am looking for some elegant approaches.
#4 by BonnEconLab
> I am still unclear about how to make the server check if the current time is the exact time specified (e.g. 10s, 20s, 30s,...). I don’t really understand the question. It would be enough to check what the current server time is whenever the live method is triggered by a participant, wouldn’t it? And whenever the live method is triggered you can also automatically send back information to everyone else in the same group. See https://otree.readthedocs.io/en/latest/live.html.
#5 by BonnEconLab
Here is some code that transmits input to the server as soon as a “transmit” button is clicked and then displays that input, including the time at which it was made, on the other group members’ screens on the fly: ***** __init__.py ***** from otree.api import * import time doc = """ An app to demonstrate live send, receive, and display of participants' inputs. """ class C(BaseConstants): NAME_IN_URL = 'zzz_live_send_receive_display' PLAYERS_PER_GROUP = 3 NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): some_input = models.StringField(initial="") round_started = models.FloatField() # FUNCTIONS # None # PAGES class Initialization(Page): timeout_seconds = 2 @staticmethod def before_next_page(player, timeout_happened): round_started = time.time() for p in player.group.get_players(): p.round_started = round_started class MyPage(Page): form_model = "player" form_fields = ["some_input"] @staticmethod def js_vars(player): return{ "secs_since_round_started": round(time.time() - player.round_started, 1), } @staticmethod def live_method(player, data): if "some_input" in data: new_interval = time.time() - player.round_started player.some_input = str(data["some_input"]) all_inputs = {} for p in player.group.get_players(): others_inputs = {} for o in p.group.get_players(): if o.id_in_group != p.id_in_group: others_inputs.update({o.id_in_group: o.some_input}) all_inputs.update({p.id_in_group: [new_interval, p.some_input, others_inputs]}) print(all_inputs) # For debugging only return all_inputs # PAGE SEQUENCE page_sequence = [ Initialization, MyPage, ] ***** MyPage.html ***** {{ block title }} Transmit, receive, and display input on the fly {{ endblock }} {{ block content }} <div class="alert alert-warning"> Round started <span id="timer">0</span> seconds ago. </div> {{ formfields }} <p>My own most recent input: <b id="my_input">none yet.</b></p> <p>My partners’ most recent input: <b id="partner_input"></b></p> <p id="transmit_button_container"> <button id="transmit_button" type="button" onclick="sendInput()" class="btn btn-warning">Transmit input</button> </p> <p style="margin-top: 200px;"> <button class="btn btn-danger"><b>Leave page</b></button> </p> {{ endblock }} {{ block scripts }} <script> var lastSubmitTime = Date.now(); var secs_since_round_started = js_vars.secs_since_round_started; document.getElementById("timer").innerHTML = secs_since_round_started.toFixed(1); // CAVEAT: At least Chrome on Mac pauses setInterval if the browser tab is not the frontmost tab! setInterval(() => { secs_since_round_started += 0.1; document.getElementById("timer").innerHTML = secs_since_round_started.toFixed(1); }, 100) function liveRecv(data) { document.getElementById("my_input").innerHTML = data[1] + " at " + data[0].toFixed(1) + " seconds after start of this round. "; for (const [key, value] of Object.entries(data[2])) { document.getElementById("partner_input").innerHTML += "<br>Player " + key + "’s input: " + value + " at " + data[0].toFixed(1) + " seconds after start of this round."; }; }; function sendInput() { liveSend({ "some_input": document.getElementById('id_some_input').value, }); }; </script> {{ endblock }}