From d29e0e392b4d7d10602f3c8c4be0f6ba534ee5b7 Mon Sep 17 00:00:00 2001
From: Shaheed Azaad On the next page, you will be asked about your suspicions and thoughts about this aspect of the study. Press SPACE to continue In this study, the die roll to determine captcha difficulty was rigged: the number you rolled was predetermined. Therefore, the difficulty of the captcha task was also predetermined.
However, the difficulty of the captcha task was pre-determined. It was unrelated to the die roll. +
` + probe_closing_text; -let die_result = 3; +const probe_text_baseline = probe_closing_text + +const die_result = Math.random() < 0.5 ? 3 : 4; switch (COND) { case 0: probe_condition = 'die'; probe_order = 'die_first'; - die_result = 4; break; case 1: probe_condition = 'die'; probe_order = 'die_first'; - die_result = 4; break; case 2: probe_condition = 'difficulty'; probe_order = 'die_first'; - die_result = 4; break; case 3: probe_condition = 'difficulty'; probe_order = 'die_first'; - die_result = 4; break; case 4: probe_condition = 'die'; @@ -102,6 +98,22 @@ switch (COND) { probe_condition = 'difficulty'; probe_order = 'difficulty_first'; break; + case 8: + probe_condition = 'baseline'; + probe_order = 'die_first'; + break; + case 9: + probe_condition = 'baseline'; + probe_order = 'die_first'; + break; + case 10: + probe_condition = 'baseline'; + probe_order = 'difficulty_first'; + break; + case 11: + probe_condition = 'baseline'; + probe_order = 'difficulty_first'; + break; } const stimulusMap = getStimulusMap(); @@ -174,6 +186,21 @@ const pre_survey_info = { stimulus: stimulusMap.get('pre_survey_info'), }; +const die_roll_practice_trial = { + type: JsPsychDieRoll, + prompt: html` +Before the real die roll, you can practice here.
++ Click the die once to start it rolling, then click it again to stop. +
+ `, + practice_trial: true, + practice_roll_limit: 3, + roll_duration: 2200, + result_template: + 'Practice roll {{roll_number}}/{{roll_limit}}: You rolled a {{value}}.', +}; + const die_roll_trial = { type: JsPsychDieRoll, prompt: html` @@ -204,6 +231,13 @@ const create_captcha_trial = trial_index => { }; }; +const pre_die_roll_info = { + type: jsPsychHtmlKeyboardResponse, + choices: [' '], + stimulus: `Practice complete. Now you will roll a die to determine the difficulty of the captcha task.
+Press SPACE to continue
`, +}; + const captcha_trials = Array.from({ length: CAPTCHA_TRIAL_COUNT }, (_, index) => create_captcha_trial(index) ); @@ -245,12 +279,12 @@ const survey_function = survey => { }); }; -const survey = { +const survey_1 = { type: jsPsychSurvey, survey_function: survey_function, survey_json: { showQuestionNumbers: false, - completeText: 'Done!', + completeText: 'Continue', pageNextText: 'Continue', pagePrevText: 'Previous', showPrevButton: false, @@ -266,10 +300,6 @@ const survey = { isAllRowRequired: debug ? false : true, rowOrder: 'random', rows: [ - { - text: `I found the captcha task difficult.`, - value: 'Difficulty', - }, { text: `I felt lucky during the die roll.`, value: 'Luck', @@ -278,6 +308,47 @@ const survey = { text: `I felt unlucky during the die roll.`, value: 'Bad_luck', }, + ], + columns: [ + { + value: 5, + text: 'Strongly agree', + }, + { + value: 4, + text: 'Agree', + }, + { + value: 3, + text: 'Neutral', + }, + { + value: 2, + text: 'Disagree', + }, + { + value: 1, + text: 'Strongly disagree', + }, + ], + }, + ], + }, + { + name: 'page2', + elements: [ + { + type: 'matrix', + name: + 'Please answer the following questions about your experience in the captcha task.', + alternateRows: true, + isAllRowRequired: debug ? false : true, + rowOrder: 'random', + rows: [ + { + text: `I found the captcha task difficult.`, + value: 'Difficulty', + }, { text: `I think I solved all the captchas correctly.`, value: 'Accuracy', @@ -316,15 +387,43 @@ const survey = { }, ], }, + ], + }, +}; + +const pre_manip_info = { + type: jsPsychHtmlKeyboardResponse, + choices: [' '], + stimulus: function () { + switch (probe_condition) { + case 'die': + return probe_text_die; + case 'difficulty': + return probe_text_difficulty; + case 'baseline': + return probe_text_baseline; + default: + return 'ERROR: probe condition not recognized'; + } + }, +}; + +const survey_2 = { + type: jsPsychSurvey, + survey_function: survey_function, + survey_json: { + showQuestionNumbers: false, + completeText: 'Done!', + pageNextText: 'Continue', + pagePrevText: 'Previous', + showPrevButton: false, + pages: [ { - name: 'page2', + name: 'page1', elements: [ { type: 'matrix', - name: - probe_condition === 'die' - ? probe_text_die - : probe_text_difficulty, + name: "Please indicate your agreement with the following statements.", alternateRows: true, isAllRowRequired: debug ? false : true, rowOrder: 'random', @@ -364,6 +463,21 @@ const survey = { ], }, ], + }, + { + name: 'page2', + elements: [ + { + type: 'comment', + title: `Please write a sentence or two on what you thought the study was about.`, + isRequired: debug == true ? false : true, + }, + { + type: 'comment', + title: ` Indicate any other thoughts or suspicions you had about the study. \n`, + isRequired: debug == true ? false : true, + } + ], }, { name: 'page3', @@ -403,22 +517,28 @@ const survey = { const main_experiment_timeline = [ instructions_1, instructions_2, + die_roll_practice_trial, + pre_die_roll_info, die_roll_trial, instructions_3, ...captcha_trials, pre_survey_info, - survey, + survey_1, + pre_manip_info, + survey_2, debrief, ]; + if (debug) { + timeline.push(die_roll_trial); timeline.push(...main_experiment_timeline); } if (!debug) { timeline.push(pre_consent_info); - timeline.push(consent_form); timeline.push(enter_fullscreen); + timeline.push(consent_form); timeline.push(...main_experiment_timeline); } jsPsych.run(timeline); diff --git a/plugins/jspsych-die-roll.js b/plugins/jspsych-die-roll.js index d6202fa..433d56c 100644 --- a/plugins/jspsych-die-roll.js +++ b/plugins/jspsych-die-roll.js @@ -27,6 +27,14 @@ const info = { type: ParameterType.STRING, default: 'You rolled a {{value}}.', }, + practice_trial: { + type: ParameterType.BOOL, + default: false, + }, + practice_roll_limit: { + type: ParameterType.INT, + default: 5, + }, }, }; @@ -37,13 +45,17 @@ class DieRollPlugin { trial(display_element, trial) { const start_time = performance.now(); - const sanitized_rigged_value = this.get_rigged_value(trial.rigged_value); - const final_value = - sanitized_rigged_value ?? - this.jsPsych.randomization.sampleWithoutReplacement( - [1, 2, 3, 4, 5, 6], - 1 - )[0]; + const practice_enabled = Boolean(trial.practice_trial); + const practice_roll_limit = practice_enabled + ? this.get_practice_roll_limit(trial.practice_roll_limit) + : 1; + const sanitized_rigged_value = practice_enabled + ? null + : this.get_rigged_value(trial.rigged_value); + const get_next_result_value = () => + sanitized_rigged_value ?? this.get_random_value(); + let pending_final_value = get_next_result_value(); + let last_roll_value = null; display_element.innerHTML = this.build_markup( trial.prompt, @@ -60,21 +72,60 @@ class DieRollPlugin { '.jspsych_die_roll_button' ); + const PRACTICE_UNLOCK_DELAY_MS = 600; + const MIN_IDLE_CLICK_INTERVAL_MS = 350; let is_rolling = false; let has_rolled = false; let roll_started_at = null; let roll_stopped_at = null; let die_click_count = 0; + let practice_rolls_completed = 0; + let practice_reenable_timeout = null; + let previous_face_value = null; + let last_face_interaction_at = null; const clear_timeouts = () => { + if (practice_enabled && practice_reenable_timeout !== null) { + window.clearTimeout(practice_reenable_timeout); + practice_reenable_timeout = null; + } this.jsPsych.pluginAPI.clearAllTimeouts(); }; + const lock_die_face = () => { + face_element.classList.remove('jspsych_die_roll_face--interactive'); + face_element.classList.remove('jspsych_die_roll_face--active'); + face_element.classList.add('jspsych_die_roll_face--locked'); + face_element.setAttribute('aria-disabled', 'true'); + face_element.removeAttribute('tabindex'); + face_element.style.pointerEvents = 'none'; + }; + + const make_die_interactive = () => { + face_element.classList.remove('jspsych_die_roll_face--locked'); + face_element.classList.add('jspsych_die_roll_face--interactive'); + face_element.setAttribute('tabindex', '0'); + face_element.removeAttribute('aria-disabled'); + face_element.style.pointerEvents = ''; + }; + + const get_next_face_value = () => { + let value; + do { + value = Math.floor(Math.random() * 6) + 1; + } while ( + value === previous_face_value || + (sanitized_rigged_value !== null && value === sanitized_rigged_value) + ); + previous_face_value = value; + return value; + }; + const cycle_face = () => { if (!is_rolling) { return; } - const value = this.random_face(final_value); + const value = get_next_face_value(); face_element.textContent = value; this.jsPsych.pluginAPI.setTimeout(cycle_face, trial.animation_interval); }; @@ -88,25 +139,55 @@ class DieRollPlugin { die_click_count += 1; roll_stopped_at = performance.now(); clear_timeouts(); - face_element.textContent = final_value; - face_element.classList.remove('jspsych_die_roll_face--active'); - face_element.classList.remove('jspsych_die_roll_face--interactive'); - face_element.classList.add('jspsych_die_roll_face--locked'); - face_element.setAttribute('aria-disabled', 'true'); - face_element.removeAttribute('tabindex'); - face_element.style.pointerEvents = 'none'; - result_element.textContent = trial.result_template.replace( - /\{\{value\}\}/gi, - final_value + last_roll_value = pending_final_value; + face_element.textContent = last_roll_value; + lock_die_face(); + practice_rolls_completed += 1; + const rolls_remaining = Math.max( + practice_roll_limit - practice_rolls_completed, + 0 ); + const formatted_result = this.render_result_text( + trial.result_template, + { + value: last_roll_value, + roll_number: practice_rolls_completed, + roll_limit: practice_enabled ? practice_roll_limit : null, + rolls_remaining: practice_enabled ? rolls_remaining : null, + } + ); + + if (practice_enabled) { + if (rolls_remaining > 0) { + result_element.innerHTML = + `${formatted_result}You may roll again (${rolls_remaining} ` + + `practice ${rolls_remaining === 1 ? 'roll' : 'rolls'} remaining) or click 'Continue' to proceed.
`; + // Brief delay prevents a double-click from immediately starting a new roll. + practice_reenable_timeout = window.setTimeout(() => { + make_die_interactive(); + has_rolled = false; + practice_reenable_timeout = null; + }, PRACTICE_UNLOCK_DELAY_MS); + } else { + result_element.textContent = + `${formatted_result} Practice complete. Click 'Continue' to proceed.`; + } + } else { + result_element.textContent = formatted_result; + } + button_element.disabled = false; - button_element.focus(); + if (!practice_enabled || rolls_remaining === 0) { + button_element.focus(); + } }; const begin_roll = () => { if (is_rolling || has_rolled) { return; } + previous_face_value = null; + pending_final_value = get_next_result_value(); is_rolling = true; die_click_count += 1; roll_started_at = performance.now(); @@ -117,31 +198,59 @@ class DieRollPlugin { cycle_face(); }; - const handle_face_interaction = () => { - if (!is_rolling && !has_rolled) { - begin_roll(); - } else if (is_rolling && !has_rolled) { + const handle_face_interaction = event => { + const event_type = event?.type ?? 'unknown'; + + if ( + (event_type === 'pointerdown' || event_type === 'click') && + typeof event?.button === 'number' && + event.button !== 0 + ) { + return; + } + + if (practice_enabled) { + const now = performance.now(); + if (!is_rolling && last_face_interaction_at !== null) { + const elapsed = now - last_face_interaction_at; + if (elapsed < MIN_IDLE_CLICK_INTERVAL_MS) { + event?.preventDefault(); + return; + } + } + last_face_interaction_at = now; + } + + if (is_rolling) { finalize_roll(); + return; + } + if (!has_rolled) { + begin_roll(); + return; } }; button_element.disabled = true; face_element.textContent = ''; - result_element.textContent = - 'Click the die to start, then click it again to lock in your number.'; - face_element.classList.add('jspsych_die_roll_face--interactive'); - face_element.setAttribute('tabindex', '0'); + result_element.innerHTML = practice_enabled + ? `Practice rolling the die. You can complete up to ${practice_roll_limit}
` + + `practice ${practice_roll_limit === 1 ? 'roll' : 'rolls'}.Click the die to start, then click it again to stop.
` + : 'Click the die to start, then click it again to lock in your number.'; + make_die_interactive(); face_element.setAttribute('role', 'button'); face_element.setAttribute( 'aria-label', 'Virtual die. Click to start and stop.' ); - face_element.addEventListener('click', handle_face_interaction); + const face_interaction_event = + typeof window.PointerEvent === 'function' ? 'pointerdown' : 'click'; + face_element.addEventListener(face_interaction_event, handle_face_interaction); face_element.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); - handle_face_interaction(); + handle_face_interaction(event); } }); @@ -149,7 +258,7 @@ class DieRollPlugin { is_rolling = false; clear_timeouts(); const trial_data = { - roll: final_value, + roll: last_roll_value, rigged_value: sanitized_rigged_value, rigged: sanitized_rigged_value !== null, die_click_count: die_click_count, @@ -161,6 +270,14 @@ class DieRollPlugin { ? roll_stopped_at - roll_started_at : null, rt: performance.now() - start_time, + practice_trial: practice_enabled, + practice_roll_limit: practice_enabled ? practice_roll_limit : null, + practice_rolls_completed: practice_enabled + ? practice_rolls_completed + : null, + practice_rolls_remaining: practice_enabled + ? Math.max(practice_roll_limit - practice_rolls_completed, 0) + : null, }; display_element.innerHTML = ''; this.jsPsych.finishTrial(trial_data); @@ -187,12 +304,11 @@ class DieRollPlugin { `; } - random_face(exclude_value) { - let value = Math.floor(Math.random() * 6) + 1; - if (value === exclude_value) { - value = (value % 6) + 1; - } - return value; + get_random_value() { + return this.jsPsych.randomization.sampleWithoutReplacement( + [1, 2, 3, 4, 5, 6], + 1 + )[0]; } get_rigged_value(value) { @@ -202,6 +318,27 @@ class DieRollPlugin { } return null; } + + get_practice_roll_limit(value) { + const numeric = Number(value); + if (Number.isFinite(numeric) && numeric >= 1) { + return Math.round(numeric); + } + return 5; + } + + render_result_text(template, replacements) { + if (typeof template !== 'string' || template.length === 0) { + return ''; + } + return Object.entries(replacements).reduce((text, [key, value]) => { + if (value === null || value === undefined) { + return text; + } + const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'gi'); + return text.replace(pattern, value); + }, template); + } } DieRollPlugin.info = info; diff --git a/scripts/text-stimuli.js b/scripts/text-stimuli.js index 22f3b26..b812741 100644 --- a/scripts/text-stimuli.js +++ b/scripts/text-stimuli.js @@ -19,11 +19,11 @@ function getStimulusMap() {You will now answer a few questions about your experience in the - experiment. It is important that you read the questions carefully and - answer them honestly. + experiment.
- Nonsense or random answers may lead to your submission being rejected. + It is important that you read the questions carefully and + answer them honestly.
Press @@ -167,15 +167,14 @@ function getStimulusMap() { stimulusMap.set( 'instructions_1', html` -
In this experiment, you will be solving some captchas.
+In this experiment, you will be solving captchas.
This will involve identifying slightly distorted letters and numbers from an image.
-+
Your task is to solve the captchas as quickly and accurately as - possible. You will not be given feedback on your performance. -
+ possible.Press SPACE @@ -188,11 +187,11 @@ function getStimulusMap() { 'instructions_2', html`
- To randomise the difficulty of the captchas you will be solving, we will - first ask you to roll a virtual die. You will then be shown captchas of - a difficulty corresponding to the number you rolled. + In this study, we will randomise the difficulty of the captchas you will solve.
++ You will roll a virtual die to determine the difficulty of the captchas you will see.
-+
The higher the number you roll, the more difficult the captchas will be.
@@ -207,18 +206,9 @@ function getStimulusMap() { 'instructions_3', html`
- You will now proceed to the captcha task, in which you will need to + You will now proceed to the captcha task
You will need to solve 12 captchas in a row as quickly and accurately as possible.
-- Please ensure that you are in a quiet environment and that you will not - be interrupted for the next few minutes. -
-- Please also ensure that you solve these captchas to the best of your - ability. Nonsense or random answers may lead to your submission being - rejected. -
Press SPACE diff --git a/styles.css b/styles.css index bbc53c7..967eae4 100644 --- a/styles.css +++ b/styles.css @@ -76,8 +76,10 @@ .jspsych_die_roll_face--locked { cursor: not-allowed; - opacity: 0.8; + opacity: 0.5; box-shadow: 0 15px 25px rgba(15, 23, 42, 0.15); + background: linear-gradient(145deg, #d4d8de, #cbd2db); + color: #475569; } @keyframes pulse {