from otree.api import * import json import os import random import time class C(BaseConstants): NAME_IN_URL = 'kenken' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 # Single round as the timer dictates the session # Dynamically load puzzles this_dir = os.path.dirname(__file__) puzzles_file_path = os.path.join(this_dir, 'puzzles.json') with open(puzzles_file_path, 'r', encoding='utf-8') as f: PUZZLES = json.load(f) class Subsession(BaseSubsession): def creating_session(self): for p in self.get_players(): if 'expiry_time' not in p.participant.vars: # Set the timer for 10 minutes (600 seconds) p.participant.vars['expiry_time'] = time.time() + 600 class Group(BaseGroup): pass class Player(BasePlayer): current_puzzle_index = models.IntegerField(initial=0) current_puzzle_number = models.IntegerField(initial=1) puzzleState = models.LongStringField(initial=json.dumps([])) total_points = models.IntegerField(initial=0) feedback = models.LongStringField(label=" ") # Survey Question 1: Familiarity with Sudoku puzzles familiarity_sudoku = models.IntegerField( choices=[ [1, 'I have never heard of Sudoku'], [2, 'I have heard of Sudoku but have never played it'], [3, 'I have played Sudoku a few times'], [4, 'I play Sudoku occasionally'], [5, 'I play Sudoku regularly'] ], widget=widgets.RadioSelect, label="How familiar are you with Sudoku puzzles?" ) # Survey Question 2: Familiarity with KenKen puzzles familiarity_kenken = models.IntegerField( choices=[ [1, 'I have never heard of KenKen'], [2, 'I have heard of KenKen but have never played it'], [3, 'I have played KenKen a few times'], [4, 'I play KenKen occasionally'], [5, 'I play KenKen regularly'] ], widget=widgets.RadioSelect, label="How familiar are you with KenKen puzzles?" ) # Survey Question 3: Enjoyment of the KenKen puzzle task enjoyment_kenken = models.IntegerField( choices=[ [1, 'Not at all'], [2, 'A little'], [3, 'Neutral'], [4, 'Somewhat enjoyed it'], [5, 'Very much enjoyed it'] ], widget=widgets.RadioSelect, label="How much did you enjoy completing the KenKen puzzle task today?" ) def calculate_scores(player: Player): puzzle = puzzle_data(player) solution = puzzle["solution"] submitted_state = json.loads(player.puzzleState) # List of submitted values puzzlepoints = 0 bonuspoints = 0 size = len(solution) feedback = [[None for _ in range(size)] for _ in range(size)] # Initialize feedback as None for row in range(size): for col in range(size): # Get the submitted value for the current cell submitted_value = next( (item['value'] for item in reversed(submitted_state) if item['row'] == row and item['col'] == col), None ) # Compare submitted value to the solution if str(submitted_value) == str(solution[row][col]): puzzlepoints += 5 feedback[row][col] = True # Correct else: feedback[row][col] = False # Incorrect # Add bonus points if the entire puzzle is correct if all(feedback[row][col] for row in range(size) for col in range(size)): bonuspoints += 20 # Update player fields player.total_points += puzzlepoints + bonuspoints if 'feedback' not in player.participant.vars: player.participant.vars['feedback'] = [] player.participant.vars['feedback'].append(feedback) return puzzlepoints, bonuspoints, feedback def puzzle_data(player: Player): """Retrieve the puzzle data for the current puzzle index of the player.""" try: return C.PUZZLES[player.current_puzzle_index] except IndexError: # Handle the case where the index is out of range return {} class PuzzleInstructions(Page): pass class Difficulty(Page): pass class PracticePuzzle1(Page): pass class PracticePuzzle2(Page): pass class TaskInstructions(Page): pass # class Puzzle(Page): # # @staticmethod # # def get_timeout_seconds(player): # # import time # # return max(0, player.participant.vars['expiry_time'] - time.time()) # # # # @staticmethod # # def is_displayed(player): # # import time # # if 'expiry_time' not in player.participant.vars: # # # Fallback: Set expiry_time if missing # # player.participant.vars['expiry_time'] = time.time() + 600 # # print("Expiry Time:", player.participant.vars.get('expiry_time')) # # # # return time.time() < player.participant.vars['expiry_time'] # # @staticmethod # def vars_for_template(player: Player): # puzzle = puzzle_data(player) # return {'puzzle_data': json.dumps(puzzle)} # # @staticmethod # def live_method(player, data): # if data['type'] == 'reset': # player.puzzleState = json.dumps([]) # return {player.id_in_group: {'state': []}} # # elif data['type'] == 'submit': # puzzle = C.PUZZLES[player.current_puzzle_index] # puzzlepoints, bonuspoints, feedback = calculate_scores(player) # # response = { # 'feedback': feedback, # 'points': puzzlepoints, # 'bonus_points': 20 if all( # feedback[row][col] for row in range(len(feedback)) for col in range(len(feedback[0])) # ) else 0, # 'total_points': player.total_points, # } # # if player.current_puzzle_index + 1 < len(C.PUZZLES): # # Move to the next puzzle # player.current_puzzle_index += 1 # next_puzzle = C.PUZZLES[player.current_puzzle_index] # player.puzzleState = json.dumps([]) # Reset the puzzle state # response['next_puzzle'] = next_puzzle # else: # # End the session if no more puzzles # response['end'] = True # # return {player.id_in_group: response} # # elif data['type'] == 'update': # state = json.loads(player.puzzleState) # state.append(data['update']) # player.puzzleState = json.dumps(state) # return {player.id_in_group: {'state': state}} # # @staticmethod # def before_next_page(player, timeout_happened): # if timeout_happened: # puzzlepoints, bonuspoints, feedback = calculate_scores(player) # # # 2. Possibly handle puzzle index # if player.current_puzzle_index + 1 < len(C.PUZZLES): # player.current_puzzle_index += 1 # player.puzzleState = json.dumps([]) # Reset state # else: # # We are done with all puzzles # pass class PuzzleTests(Page): # @staticmethod # def get_timeout_seconds(player): # import time # return max(0, player.participant.vars['expiry_time'] - time.time()) # # @staticmethod # def is_displayed(player): # import time # if 'expiry_time' not in player.participant.vars: # # Fallback: Set expiry_time if missing # player.participant.vars['expiry_time'] = time.time() + 600 # print("Expiry Time:", player.participant.vars.get('expiry_time')) # # return time.time() < player.participant.vars['expiry_time'] @staticmethod def vars_for_template(player: Player): puzzle = C.PUZZLES[player.current_puzzle_number - 1] # -1 since list is 0-indexed return { 'puzzle_data': json.dumps(puzzle), 'puzzle_number': player.current_puzzle_number, } @staticmethod def live_method(player, data): if data['type'] == 'reset': player.puzzleState = json.dumps([]) return {player.id_in_group: {'state': []}} elif data['type'] == 'submit': puzzle_index = player.current_puzzle_number - 1 puzzle = C.PUZZLES[puzzle_index] puzzlepoints, bonuspoints, feedback = calculate_scores(player) total = puzzlepoints + bonuspoints player.total_points += total # Accumulate points response = { 'feedback': feedback, 'points': puzzlepoints, 'bonus_points': bonuspoints, 'total_points': player.total_points, } if player.current_puzzle_number < len(C.PUZZLES): player.current_puzzle_number += 1 player.puzzleState = json.dumps([]) # Reset puzzle state response['next_puzzle'] = C.PUZZLES[player.current_puzzle_number - 1] response['puzzle_number'] = player.current_puzzle_number else: response['end'] = True return {player.id_in_group: response} elif data['type'] == 'update': state = json.loads(player.puzzleState) state.append(data['update']) player.puzzleState = json.dumps(state) return {player.id_in_group: {'state': state}} @staticmethod def before_next_page(player, timeout_happened): if timeout_happened: puzzle_index = player.current_puzzle_number - 1 puzzlepoints, bonuspoints, feedback = calculate_scores(player) total = puzzlepoints + bonuspoints player.total_points += total if player.current_puzzle_number < len(C.PUZZLES): player.current_puzzle_number += 1 player.puzzleState = json.dumps([]) # Reset state class Survey(Page): form_model = 'player' form_fields = ['familiarity_sudoku', 'familiarity_kenken', 'enjoyment_kenken'] class Results(Page): @staticmethod def vars_for_template(player: Player): return { 'feedback_all': player.participant.vars.get('feedback', []), 'total_points': player.total_points, } class Feedback(Page): form_model = 'player' form_fields = ['feedback'] class TestTaskInstructions(Page): pass class ThankYou(Page): pass page_sequence = [TestTaskInstructions, PuzzleInstructions, # Difficulty, # PracticePuzzle1, # PracticePuzzle2, # TaskInstructions, # Puzzle, PuzzleTests, # Survey, # Results, Feedback, ThankYou]