from otree.api import ( Currency, cu, currency_range, models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, ExtraModel, WaitPage, Page, read_csv, ) import units import shared_out doc = '' class C(BaseConstants): NAME_IN_URL = 'c1_bret_otai_trial' PLAYERS_PER_GROUP = None NUM_ROUNDS = 2 NUM_ROUNDS_trial = 2 GRID_SIZE = 100 BOMB_POSITION = 0 POINTS_PER_BOX = 500 FANCY_TIMEOUT_SECONDS = 60 BONUS_BOXES = 2 CLOSE_THRESHOLD = 5 REVEAL_DELAY_MS = 50 BOMB_REVEAL_DELAY_MS = 500 BOXES_PER_SECOND = 2 MS_PER_SECOND = 1000 TIMER_PAD_THRESHOLD = 10 SECONDS_PER_MINUTE = 60 VARIANT_STANDARD = 'standard' VARIANT_FANCY = 'fancy' VARIANT_SPEED = 'speed' VARIANT_STOPSTART = 'stopstart' SPEED_PHASE_1_BPS = 2 SPEED_PHASE_2_BPS = 2 SPEED_PHASE_3_BPS = 2 SPEED_PHASE_1_DURATION = 10 SPEED_PHASE_2_DURATION = 20 VARIANT_LUCKY = 'lucky' LUCKY_NUMBERS_COUNT = 4 LUCKY_NUMBERS_COST = cu(0.1) MIN_LUCKY_NUMBER = 40 EXTRA_BOXES = 5 WAITING_DELAY_SECONDS = 5 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): boxes_collected_otai_trial = models.IntegerField() bomb_position = models.IntegerField() bomb_exploded = models.BooleanField() payoff_trial_boxes = models.IntegerField() bonus_boxes_used = models.IntegerField() variant = models.StringField() stopped_by_player = models.BooleanField() bought_lucky_numbers = models.BooleanField(initial=False, label='Ya, Saya ingin memilih Angka Keberuntungan') lucky_number_1 = models.IntegerField(label='Lucky number 1', max=C.GRID_SIZE, min=C.MIN_LUCKY_NUMBER) lucky_number_2 = models.IntegerField(label='Lucky number 2', max=C.GRID_SIZE, min=C.MIN_LUCKY_NUMBER) lucky_number_3 = models.IntegerField(label='Lucky number 3', max=C.GRID_SIZE, min=C.MIN_LUCKY_NUMBER) lucky_number_4 = models.IntegerField(label='Lucky number 4', max=C.GRID_SIZE, min=C.MIN_LUCKY_NUMBER) lucky_number_saved_player = models.BooleanField(initial=False) paying_round_trial = models.IntegerField() paying_round_trial_payoff = models.CurrencyField() payoff_trial = models.CurrencyField(initial=0) def creating_session(subsession: Subsession): import random for player in subsession.get_players(): player.variant = subsession.session.config['variant'] player.bomb_position = random.randint(1, C.GRID_SIZE) player.boxes_collected_otai_trial = 0 player.bomb_exploded = False player.payoff_trial_boxes = 0 player.bonus_boxes_used = 0 player.stopped_by_player = False # choose paying round once if subsession.round_number == 1: player.participant.vars['round_to_pay_trial'] = random.randint(1, C.NUM_ROUNDS_trial) def get_lucky_numbers(player: Player): if player.variant == C.VARIANT_LUCKY and player.bought_lucky_numbers: return [player.lucky_number_1, player.lucky_number_2, player.lucky_number_3, player.lucky_number_4] return [] class Instructions(Page): form_model = 'player' @staticmethod def is_displayed(player: Player): return Instructions and player.round_number == 1 @staticmethod def vars_for_template(player: Player): return dict( is_standard=player.variant == C.VARIANT_STANDARD, is_fancy=player.variant == C.VARIANT_FANCY, is_speed=player.variant == C.VARIANT_SPEED, is_stopstart=player.variant == C.VARIANT_STOPSTART, is_lucky=player.variant == C.VARIANT_LUCKY, speed_phase_2_duration=C.SPEED_PHASE_2_DURATION - C.SPEED_PHASE_1_DURATION, min_lucky_number=C.MIN_LUCKY_NUMBER, extra_boxes=C.EXTRA_BOXES, ) class LuckyNumbersChoice(Page): form_model = 'player' form_fields = ['bought_lucky_numbers'] @staticmethod def is_displayed(player: Player): return player.variant == C.VARIANT_LUCKY class SelectLuckyNumbers(Page): form_model = 'player' form_fields = ['lucky_number_1', 'lucky_number_2', 'lucky_number_3', 'lucky_number_4'] @staticmethod def is_displayed(player: Player): return player.variant == C.VARIANT_LUCKY and player.bought_lucky_numbers @staticmethod def error_message(player: Player, values): numbers = [values['lucky_number_1'], values['lucky_number_2'], values['lucky_number_3'], values['lucky_number_4']] for num in numbers: if num < C.MIN_LUCKY_NUMBER or num > C.GRID_SIZE: return f'All numbers must be between {C.MIN_LUCKY_NUMBER} and {C.GRID_SIZE}' if len(numbers) != len(set(numbers)): return 'All lucky numbers must be different' class Task(Page): form_model = 'player' @staticmethod def js_vars(player: Player): return dict( bomb_position=player.bomb_position, grid_size=C.GRID_SIZE, variant=player.variant, fancy_timeout_seconds=C.FANCY_TIMEOUT_SECONDS, bonus_boxes=C.BONUS_BOXES, close_threshold=C.CLOSE_THRESHOLD, reveal_delay_ms=C.REVEAL_DELAY_MS, bomb_reveal_delay_ms=C.BOMB_REVEAL_DELAY_MS, boxes_per_second=C.BOXES_PER_SECOND, ms_per_second=C.MS_PER_SECOND, speed_phase_1_bps=C.SPEED_PHASE_1_BPS, speed_phase_2_bps=C.SPEED_PHASE_2_BPS, speed_phase_3_bps=C.SPEED_PHASE_3_BPS, speed_phase_1_duration=C.SPEED_PHASE_1_DURATION, speed_phase_2_duration=C.SPEED_PHASE_2_DURATION, lucky_numbers=get_lucky_numbers(player), seconds_per_minute=C.SECONDS_PER_MINUTE, extra_boxes=C.EXTRA_BOXES, waiting_delay_seconds=C.WAITING_DELAY_SECONDS, total_rounds_trial=C.NUM_ROUNDS_trial, points_per_box=C.POINTS_PER_BOX, # <-- ADD THIS ) @staticmethod def vars_for_template(player: Player): return dict( total_rounds_trial=C.NUM_ROUNDS_trial ) @staticmethod def before_next_page(player: Player, timeout_happened): # recompute payoff safely if player.bomb_exploded: player.payoff_trial_boxes = 0 else: player.payoff_trial_boxes = player.boxes_collected_otai_trial player.payoff_trial = player.payoff_trial_boxes * C.POINTS_PER_BOX if player.bought_lucky_numbers: player.payoff_trial -= C.LUCKY_NUMBERS_COST @staticmethod async def live_method(player: Player, data): if data['type'] == 'collect': boxes = data['boxes'] box_id = data.get('box_id') player.boxes_collected_otai_trial = boxes lucky_numbers = get_lucky_numbers(player) bomb_protected = player.bomb_position in lucky_numbers # If bomb already triggered earlier, keep it if not player.bomb_exploded: if box_id == player.bomb_position: if bomb_protected: player.bomb_exploded = False player.lucky_number_saved_player = True player.payoff_trial_boxes = boxes else: player.bomb_exploded = True player.payoff_trial_boxes = 0 player.lucky_number_saved_player = False else: player.payoff_trial_boxes = boxes player.payoff_trial = player.payoff_trial_boxes * C.POINTS_PER_BOX if player.bought_lucky_numbers: player.payoff_trial = player.payoff_trial - C.LUCKY_NUMBERS_COST response = { 'bomb_exploded': player.bomb_exploded, 'bomb_protected': bomb_protected, } yield {player.id_in_group: response} elif data['type'] == 'bonus': player.bonus_boxes_used = data['bonus_used'] # update total boxes player.boxes_collected_otai_trial = data['final_boxes'] # recompute payoff correctly player.payoff_trial_boxes = player.boxes_collected_otai_trial player.payoff_trial = player.payoff_trial_boxes * C.POINTS_PER_BOX if player.bought_lucky_numbers: player.payoff_trial -= C.LUCKY_NUMBERS_COST elif data['type'] == 'stop': player.stopped_by_player = True class Results(Page): form_model = 'player' @staticmethod def vars_for_template(player: Player): return dict( points_per_box=C.POINTS_PER_BOX, boxes_collected_otai_trial=player.boxes_collected_otai_trial, payoff_trial=player.payoff_trial, total_rounds_trial=C.NUM_ROUNDS_trial ) class MultiRoundResults(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS_trial @staticmethod def vars_for_template(player: Player): paying_round_trial_number = player.participant.vars['round_to_pay_trial'] paying_round_trial = player.in_round(paying_round_trial_number) # Save to dataset player.paying_round_trial = paying_round_trial_number # Set final payoff of the task player.participant.vars['final_payoff_trial'] = paying_round_trial.payoff_trial return dict( player_in_all_rounds=player.in_all_rounds(), round_to_pay_trial=paying_round_trial_number, paying_round_trial_payoff=paying_round_trial.payoff_trial, bomb=paying_round_trial.bomb_exploded, total_rounds_trial=C.NUM_ROUNDS_trial ) page_sequence = [Instructions, LuckyNumbersChoice, SelectLuckyNumbers, Task, Results, MultiRoundResults]