diff --git a/index.js b/index.js index 55d61de..2f6d351 100644 --- a/index.js +++ b/index.js @@ -45,46 +45,42 @@ debug = jsPsych.data.getURLVariable('debug') === 'true' || import.meta.env.VITE_DEBUG === 'true'; -console.log('debug:', debug); prolific_id = jsPsych.data.getURLVariable('PROLIFIC_PID'); const COND = Number(jsPsych.data.getURLVariable('C')); -const probe_preamble = `In this experiment, we told you that you would roll a die to determine the difficulty of the captcha task.\nHowever, `; -const probe_closing_text = `\n\nPlease share your thoughts and suspicions about this by indicating your agreement with the following statements.`; +const probe_closing_text = `
On the next page, you will be asked about your suspicions and thoughts about this aspect of the study.
+Press SPACE to continue
`; const probe_text_die = - probe_preamble + - 'the die roll was rigged, so that the number you received (and therefore the difficulty of the captcha task) was pre-determined. ' + + `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.
` + probe_closing_text; const probe_text_difficulty = - probe_preamble + - 'while the die roll was random, the difficulty of the captcha task was pre-determined and unrelated to the die roll. ' + + `In this study, the die roll to determine captcha difficulty was indeed random.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 {