from otree.api import *
import random
from scenario_text import SCENARIOS
import time
class C(BaseConstants):
NAME_IN_URL = 'time_choice'
PLAYERS_PER_GROUP = None
NUM_ROUNDS = 3
TOTAL_MINUTES_PART2 = 16
MIN_PUZZLE_MIN = 5
MIN_SLIDER_MIN = 1
# Pilot uses 5 slider rates (from session config)
PILOT_RATE_IDS = [1, 2, 3, 4, 5]
class Subsession(BaseSubsession):
def creating_session(self):
for pl in self.get_players():
part = pl.participant
# Randomize scenario order once per participant for both main and pilot
if 'scenario_order' not in part.vars:
order = [1, 2, 3]
random.shuffle(order)
part.vars['scenario_order'] = order
if 'scenario_text' not in part.vars:
part.vars['scenario_text'] = SCENARIOS
class Group(BaseGroup):
pass
def is_pilot(player) -> bool:
return bool(player.session.config.get('sessionPilot', False))
def slider_rate_for(session, rate_id: int) -> float:
# Map rate_id to the correct session config key
if rate_id == 1:
key = 'sliders_rate'
else:
key = f'sliders_rate{rate_id}'
v = session.config.get(key)
if v is None:
raise RuntimeError(f"Missing {key} in session config.")
return float(v)
def clamp_default_puzzle_minutes() -> int:
max_p = C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN
# With total=3 and min slider=1, max puzzle is 2. Default to 2.
return max(C.MIN_PUZZLE_MIN, min(10, max_p))
def build_ranking_table_force_you_5th():
"""
Returns (your_rank, table_rows) such that:
- "Vous" is always rank 5
- Opponents are P1..P9
- Cells are placeholders: "P1 performance", "P1 productivité", etc.
- If show_opponents is False, opponent performance/productivity are blank
"""
order = ["P1", "P2", "P3", "P4", "Vous", "P5", "P6", "P7", "P8", "P9"]
rows = []
your_rank = None
for rank, label in enumerate(order, start=1):
if label == "Vous":
your_rank = rank
rows.append(dict(
rank=rank,
time= "Votre décision",
participant="Vous",
performance="Votre performance",
productivity="Votre productivité",
))
else:
rows.append(dict(
rank=rank,
participant=label,
time= "10 minutes",
performance=f"La performance de {label}",
productivity=f"La productivité de {label}"
))
return your_rank, rows
def build_ranking_table_force_you_5th_NoScores():
"""
Returns (your_rank, table_rows) such that:
- "Vous" is always rank 5
- Opponents are P1..P9
- Cells are placeholders: "P1 performance", "P1 productivité", etc.
- If show_opponents is False, opponent performance/productivity are blank
"""
order = ["P1", "P2", "P3", "P4", "Vous", "P5", "P6", "P7", "P8", "P9"]
rows = []
your_rank = None
for rank, label in enumerate(order, start=1):
if label == "Vous":
your_rank = rank
rows.append(dict(
rank=rank,
participant="Vous",
time="Votre décision",
performance="Votre performance",
productivity="Votre productivité",
))
else:
rows.append(dict(
rank=rank,
participant=label,
time="10 minutes",
performance="",
productivity="",
))
return your_rank, rows
class Player(BasePlayer):
# Comprehension questions
comp_decisions = models.IntegerField(
choices=[1, 2, 3, 4, 5],
label="Pour combien de scénarios allez-vous devoir prendre une décision ?",
)
comp_scenarios = models.IntegerField(
choices=[1, 2, 3, 4, 5],
label="Combien de scénarios seront mis en oeuvre au final ?",
)
comp_priming = models.BooleanField(
choices=[[True, "Vrai"], [False, "Faux"]],
label="Les résultats dans ce type d’exercices de logique sont souvent liés à la réussite scolaire et aux revenus futurs.",
)
comp_reflexion = models.BooleanField(
choices=[[True, "Vrai"], [False, "Faux"]],
label="Vos décisions doivent être vivement réfléchies car l’une d’entre elles déterminera le déroulement de la Partie 2 de l’expérience.",
)
# Comprehension tracking
comp_total_mistakes = models.IntegerField(initial=0)
comp_attempts = models.IntegerField(initial=0)
# Main: one allocation per round
puzzle_minutes = models.IntegerField(
min=C.MIN_PUZZLE_MIN,
max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN,
blank=True,
)
slider_minutes = models.IntegerField(blank=True)
scenario_id = models.IntegerField()
scenario_order_str = models.StringField()
scenario_label = models.StringField(blank=True)
# Pilot: five allocations on one page per scenario (puzzle minutes for each rate)
puzz_r1 = models.IntegerField(min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True)
puzz_r2 = models.IntegerField(min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True)
puzz_r3 = models.IntegerField(min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True)
puzz_r4 = models.IntegerField(min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True)
puzz_r5 = models.IntegerField(min=C.MIN_PUZZLE_MIN, max=C.TOTAL_MINUTES_PART2 - C.MIN_SLIDER_MIN, blank=True)
# Beliefs (unchanged)
belief_puzz1 = models.IntegerField(min=0)
belief_puzz2 = models.IntegerField(min=0)
belief_puzz3 = models.IntegerField(min=0)
timechoice_start = models.FloatField(initial=0)
timechoice_time_spent = models.FloatField(initial=0)
class Instructions(Page):
@staticmethod
def is_displayed(player):
# Show instructions once for everyone except session1
return (not player.session.config.get('session1', False)) and player.round_number == 1
@staticmethod
def vars_for_template(player):
return dict(
is_pilot=bool(player.session.config.get('sessionPilot', False)),
is_session1=bool(player.session.config.get('session1', False)),
piece_rate= player.session.config.get('piece_rate'),
num_correct = player.participant.vars.get('part1_num_correct'),
correct_per_min = player.participant.vars.get('part1_correct_per_min')
)
class Instructions2(Page):
allow_back_button = True
@staticmethod
def is_displayed(player):
# Show instructions once for everyone except session1
return (not player.session.config.get('session1', False)) and player.round_number == 1
@staticmethod
def vars_for_template(player):
return dict(
is_pilot=bool(player.session.config.get('sessionPilot', False)),
is_session1=bool(player.session.config.get('session1', False)),
piece_rate= player.session.config.get('piece_rate'),
num_correct = player.participant.vars.get('part1_num_correct'),
correct_per_min = player.participant.vars.get('part1_correct_per_min')
)
class Comprehension(Page):
allow_back_button = True
form_model = 'player'
form_fields = [
'comp_decisions',
'comp_scenarios',
'comp_priming',
'comp_reflexion',
]
@staticmethod
def is_displayed(player):
return (not player.session.config.get('session1', False)) and player.round_number == 1
@staticmethod
def error_message(player: Player, values):
correct_decisions = 3
correct_scenarios = 1
correct_priming = True
correct_reflexion = True
errors = {}
mistakes_this_attempt = 0
if values.get('comp_decisions') != correct_decisions:
errors['comp_decisions'] = "Réponse incorrecte."
mistakes_this_attempt += 1
if values.get('comp_scenarios') != correct_scenarios:
errors['comp_scenarios'] = "Réponse incorrecte."
mistakes_this_attempt += 1
if values.get('comp_priming') is not correct_priming:
errors['comp_priming'] = "Réponse incorrecte."
mistakes_this_attempt += 1
if values.get('comp_reflexion') is not correct_reflexion:
errors['comp_reflexion'] = "Réponse incorrecte."
mistakes_this_attempt += 1
# If wrong: record mistakes + block progress with field errors
if errors:
player.comp_total_mistakes += mistakes_this_attempt
player.comp_attempts += 1
return errors
return None
class TimeChoice(Page):
@staticmethod
def get_form_fields(player: Player):
if is_pilot(player):
return ['puzz_r1', 'puzz_r2', 'puzz_r3', 'puzz_r4', 'puzz_r5']
return ['puzzle_minutes']
form_model = 'player'
@staticmethod
def vars_for_template(player: Player):
total = C.TOTAL_MINUTES_PART2
min_p = C.MIN_PUZZLE_MIN
min_s = C.MIN_SLIDER_MIN
max_p = total - min_s
part = player.participant
order = part.vars.get('scenario_order')
if order is None:
order = [1, 2, 3]
random.shuffle(order)
part.vars['scenario_order'] = order
scenario_id = int(order[player.round_number - 1])
scenario = SCENARIOS[scenario_id]
player.scenario_id = scenario_id
player.scenario_label = scenario["label"]
player.scenario_order_str = "-".join(str(x) for x in order)
start_default = clamp_default_puzzle_minutes()
# Build slider rates list for pilot display
rates = []
if is_pilot(player):
for rid in C.PILOT_RATE_IDS:
rates.append(dict(
rate_id=rid,
slider_rate=slider_rate_for(player.session, rid),
field_name=f"puzz_r{rid}",
start_value=start_default,
))
if not player.timechoice_start:
player.timechoice_start = time.time()
# Main start value
see = player.field_maybe_none('puzzle_minutes')
start_puzzle_minutes = see if see is not None else start_default
# For display
your_rank, table_rows = build_ranking_table_force_you_5th()
your_rankNoScores, table_rowsNoScores = build_ranking_table_force_you_5th_NoScores()
return dict(
is_pilot=is_pilot(player),
total_minutes=total,
min_puzzle=min_p,
min_slider=min_s,
max_puzzle=max_p,
scenario_id=scenario_id,
scenario_label=scenario["label"],
scenario_title=scenario.get("title", f"Scénario {scenario['label']}"),
scenario_rules_html=scenario.get("rules_html", ""),
scenario_visuel_html=scenario.get("visuel_html", ""),
scenario_order=order,
round_number=player.round_number,
# main
start_puzzle_minutes=start_puzzle_minutes,
# pilot
pilot_rates=rates,
your_rank=your_rank,
table_rows=table_rows,
your_rankNoScores = your_rankNoScores,
table_rowsNoScores = table_rowsNoScores,
num_correct=player.participant.vars.get('part1_num_correct'),
correct_per_min=player.participant.vars.get('part1_correct_per_min'),
piece_rate=player.session.config.get('piece_rate')
)
@staticmethod
def error_message(player: Player, values):
total = C.TOTAL_MINUTES_PART2
min_slider = C.MIN_SLIDER_MIN
min_puzzle = C.MIN_PUZZLE_MIN
max_puzzle = total - min_slider
if is_pilot(player):
for rid in C.PILOT_RATE_IDS:
v = values.get(f"puzz_r{rid}")
if v is None:
continue
if v < min_puzzle or v > max_puzzle:
return f"Veuillez choisir une valeur entre {min_puzzle} et {max_puzzle}."
return
v = values.get("puzzle_minutes")
if v is not None and (v < min_puzzle or v > max_puzzle):
return f"Veuillez choisir une valeur entre {min_puzzle} et {max_puzzle}."
@staticmethod
def before_next_page(player: Player, timeout_happened):
p = player.participant
total_min = C.TOTAL_MINUTES_PART2
sid = int(player.scenario_id)
if is_pilot(player):
# Store 5 allocations for this scenario, keyed by rate
for rid in C.PILOT_RATE_IDS:
puzzle_min = int(getattr(player, f"puzz_r{rid}"))
slider_min = total_min - puzzle_min
# Store SECONDS
p.vars[f'scenario_{sid}_puzzle_minutes_r{rid}'] = puzzle_min * 60
p.vars[f'scenario_{sid}_slider_minutes_r{rid}'] = slider_min * 60
# For convenience/backward compatibility, also store baseline (r1) into old keys
p.vars[f'scenario_{sid}_puzzle_minutes'] = int(getattr(player, "puzz_r1")) * 60
p.vars[f'scenario_{sid}_slider_minutes'] = (total_min - int(getattr(player, "puzz_r1"))) * 60
else:
puzzle_min = int(player.puzzle_minutes)
slider_min = total_min - puzzle_min
player.slider_minutes = slider_min
p.vars[f'scenario_{sid}_puzzle_minutes'] = puzzle_min * 60
p.vars[f'scenario_{sid}_slider_minutes'] = slider_min * 60
p.vars['scenario_order_str'] = player.scenario_order_str
player.timechoice_time_spent = time.time() - player.timechoice_start
class Beliefs(Page):
form_model = 'player'
form_fields = ['belief_puzz1', 'belief_puzz2', 'belief_puzz3']
@staticmethod
def is_displayed(player: Player):
return player.round_number == C.NUM_ROUNDS
@staticmethod
def vars_for_template(player: Player):
p = player.participant
def minutes_for(sid: int):
puzzle_seconds = p.vars.get(f'scenario_{sid}_puzzle_minutes')
slider_seconds = p.vars.get(f'scenario_{sid}_slider_minutes')
title = SCENARIOS[sid]["title"]
rappels_html = SCENARIOS[sid].get("rappels_html", "")
puzzle_minutes = int(puzzle_seconds) // 60 if puzzle_seconds is not None else None
slider_minutes = int(slider_seconds) // 60 if slider_seconds is not None else None
return puzzle_minutes, slider_minutes, title, rappels_html
pz1, sl1, title1, rappels_html1 = minutes_for(1)
pz2, sl2, title2, rappels_html2 = minutes_for(2)
pz3, sl3, title3, rappels_html3 = minutes_for(3)
return dict(
label1=title1,
label2=title2,
label3=title3,
# reminders pop-up (you called it rappels in template, but in SCENARIOS it's rules_html)
rappels1_html=rappels_html1,
rappels2_html=rappels_html2,
rappels3_html=rappels_html3,
puzzle_minutes_1=pz1, slider_minutes_1=sl1,
puzzle_minutes_2=pz2, slider_minutes_2=sl2,
puzzle_minutes_3=pz3, slider_minutes_3=sl3,
is_pilot=bool(player.session.config.get('sessionPilot', False)),
slider_rate=player.session.config.get('sliders_rate'),
num_correct=player.participant.vars.get('part1_num_correct'),
correct_per_min=player.participant.vars.get('part1_correct_per_min'),
)
class Transition(Page):
@staticmethod
def is_displayed(player: Player):# show only for pilot participants, and only once (after round 3)
return player.round_number == C.NUM_ROUNDS
@staticmethod
def vars_for_template(player: Player):
return dict(
is_pilot=bool(player.session.config.get('sessionPilot', False)),
)
page_sequence = [Instructions, Instructions2, Comprehension,TimeChoice, Beliefs, Transition]