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 = '' # ========================================================= # LANGUAGE SETUP # ========================================================= class C(BaseConstants): NAME_IN_URL = 'd1_bret_otai_lucky_trial' PLAYERS_PER_GROUP = None NUM_ROUNDS = 2 NUM_ROUNDS_lucky_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 = 4 MS_PER_SECOND = 1000 TIMER_PAD_THRESHOLD = 10 SECONDS_PER_MINUTE = 60 VARIANT_STANDARD = 'standard' VARIANT_FANCY = 'fancy' VARIANT_SPEED = 'speed' VARIANT_STOPSTART = 'stopstart' VARIANT_LUCKY = 'lucky' SPEED_PHASE_1_BPS = 4 SPEED_PHASE_2_BPS = 4 SPEED_PHASE_3_BPS = 4 SPEED_PHASE_1_DURATION = 10 SPEED_PHASE_2_DURATION = 20 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 # ========================================================= # LANGUAGE HELPER # ========================================================= def get_language(player): return player.session.config.get('language', 'en') == 'en' class Player(BasePlayer): # ===================================================== # GAME VARIABLES # ===================================================== boxes_collected = models.IntegerField() bomb_position = models.IntegerField() bomb_exploded = models.BooleanField() payoff_boxes = models.IntegerField() bonus_boxes_used = models.IntegerField() variant = models.StringField() stopped_by_player = models.BooleanField() # ===================================================== # LUCKY NUMBERS # ===================================================== bought_lucky_numbers = models.BooleanField( label='' ) lucky_number_1 = models.IntegerField( label='', min=C.MIN_LUCKY_NUMBER, max=C.GRID_SIZE ) lucky_number_2 = models.IntegerField( label='', min=C.MIN_LUCKY_NUMBER, max=C.GRID_SIZE ) lucky_number_3 = models.IntegerField( label='', min=C.MIN_LUCKY_NUMBER, max=C.GRID_SIZE ) lucky_number_4 = models.IntegerField( label='', min=C.MIN_LUCKY_NUMBER, max=C.GRID_SIZE ) lucky_number_saved_player = models.BooleanField( initial=False ) # ===================================================== # PAYOFF TRACKING # ===================================================== paying_round_lucky_trial = models.IntegerField() paying_round_lucky_trial_payoff = models.CurrencyField() payoff_lucky_trial = models.CurrencyField( initial=0 ) def creating_session(subsession: Subsession): import random for player in subsession.get_players(): player.variant = 'lucky' player.bomb_position = random.randint(1, C.GRID_SIZE) player.boxes_collected = 0 player.bomb_exploded = False player.payoff_boxes = 0 player.bonus_boxes_used = 0 player.stopped_by_player = False if subsession.round_number == 1: player.participant.vars['round_to_pay_lucky_trial'] = random.randint( 1, C.NUM_ROUNDS_lucky_trial ) def bought_lucky_numbers_choices(player): if get_language(player): return [ [True, 'Yes, I want to choose Lucky Numbers'], [False, 'No, I do not want to choose Lucky Numbers'], ] return [ [True, 'Ya, Saya ingin memilih Angka Keberuntungan'], [False, 'Tidak, Saya tidak ingin memilih Angka Keberuntungan'], ] 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): @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def vars_for_template(player: Player): en = ( player.session.config.get('language', 'en') == 'en' ) return dict( en=en, total_rounds_lucky_trial= player.variant==C.NUM_ROUNDS_lucky_trial, 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 @staticmethod def vars_for_template(player: Player): en = ( player.session.config.get('language', 'en') == 'en' ) return dict( en=en, ) class SelectLuckyNumbers(Page): form_model = 'player' form_fields = [ 'lucky_number_1', 'lucky_number_2', 'lucky_number_3', 'lucky_number_4', ] @staticmethod def vars_for_template(player: Player): en = ( player.session.config.get('language', 'en') == 'en' ) return dict( en=en, ) @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 ' f'{C.MIN_LUCKY_NUMBER} and {C.GRID_SIZE}' if get_language(player) else f'Semua angka harus berada di antara ' f'{C.MIN_LUCKY_NUMBER} dan {C.GRID_SIZE}' ) if len(numbers) != len(set(numbers)): return ( 'All lucky numbers must be different' if get_language(player) else 'Semua angka keberuntungan harus berbeda' ) class Task(Page): form_model = 'player' @staticmethod def before_next_page(player: Player, timeout_happened): if player.bomb_exploded: player.payoff_boxes = 0 else: player.payoff_boxes = player.boxes_collected player.payoff_lucky_trial = ( player.payoff_boxes * C.POINTS_PER_BOX ) if player.bought_lucky_numbers: player.payoff_lucky_trial -= C.LUCKY_NUMBERS_COST @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_lucky_trial=C.NUM_ROUNDS_lucky_trial, points_per_box=C.POINTS_PER_BOX, ) @staticmethod def vars_for_template(player: Player): en = ( player.session.config.get('language', 'en') == 'en' ) return dict( en=en, total_rounds_lucky_trial=C.NUM_ROUNDS_lucky_trial, ) @staticmethod async def live_method(player: Player, data): if data['type'] == 'collect': boxes = data['boxes'] box_id = data.get('box_id') player.boxes_collected = boxes lucky_numbers = get_lucky_numbers(player) bomb_protected = ( player.bomb_position in lucky_numbers ) # Only process if bomb not already exploded 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_boxes = boxes else: player.bomb_exploded = True player.payoff_boxes = 0 player.lucky_number_saved_player = False else: player.payoff_boxes = boxes player.payoff_lucky_trial = ( player.payoff_boxes * C.POINTS_PER_BOX ) if player.bought_lucky_numbers: player.payoff_lucky_trial -= ( C.LUCKY_NUMBERS_COST ) response = dict( 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' ] player.boxes_collected = data[ 'final_boxes' ] player.payoff_boxes = ( player.boxes_collected ) player.payoff_lucky_trial = ( player.payoff_boxes * C.POINTS_PER_BOX ) if player.bought_lucky_numbers: player.payoff_lucky_trial -= ( C.LUCKY_NUMBERS_COST ) if player.bomb_exploded: player.payoff_lucky = cu(0) elif data['type'] == 'stop': player.stopped_by_player = True class Results(Page): @staticmethod def vars_for_template(player: Player): en = ( player.session.config.get('language', 'en') == 'en' ) return dict( en=en, points_per_box=C.POINTS_PER_BOX, boxes_collected=player.boxes_collected, payoff_lucky_trial=player.payoff_lucky_trial, total_rounds_lucky_trial=C.NUM_ROUNDS_lucky_trial, extra_boxes=C.EXTRA_BOXES, ) class MultiRoundResults(Page): @staticmethod def is_displayed(player: Player): return ( player.round_number == C.NUM_ROUNDS_lucky_trial ) @staticmethod def vars_for_template(player: Player): round_number = player.participant.vars[ 'round_to_pay_lucky_trial' ] paying_round = player.in_round(round_number) player.paying_round_lucky_trial = round_number player.paying_round_lucky_trial_payoff = ( paying_round.payoff_lucky_trial ) player.participant.vars[ 'final_payoff_lucky_trial' ] = paying_round.payoff_lucky_trial en = ( player.session.config.get('language', 'en') == 'en' ) return dict( en=en, player_in_all_rounds=player.in_all_rounds(), round_to_pay_lucky_trial=round_number, paying_round_lucky_trial_payoff=( paying_round.payoff_lucky_trial ), bomb=paying_round.bomb_exploded, total_rounds_lucky_trial=( C.NUM_ROUNDS_lucky_trial ), ) page_sequence = [ Instructions, LuckyNumbersChoice, SelectLuckyNumbers, Task, Results, MultiRoundResults, ]