fixed die

This commit is contained in:
2026-02-27 22:49:53 +01:00
parent 9b226d60da
commit d29e0e392b
4 changed files with 332 additions and 83 deletions

170
index.js
View File

@@ -45,46 +45,42 @@ debug =
jsPsych.data.getURLVariable('debug') === 'true' || jsPsych.data.getURLVariable('debug') === 'true' ||
import.meta.env.VITE_DEBUG === 'true'; import.meta.env.VITE_DEBUG === 'true';
console.log('debug:', debug);
prolific_id = jsPsych.data.getURLVariable('PROLIFIC_PID'); prolific_id = jsPsych.data.getURLVariable('PROLIFIC_PID');
const COND = Number(jsPsych.data.getURLVariable('C')); 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 = `<p class ="mt-6">On the next page, you will be asked about your suspicions and thoughts about this aspect of the study.</p>
const probe_closing_text = `\n\nPlease share your thoughts and suspicions about this by indicating your agreement with the following statements.`; <p class="mt-6">Press <span class = "font-semibold"> SPACE</span> to continue</p>`;
const probe_text_die = const probe_text_die =
probe_preamble + `<p> In this study, the die roll to determine captcha difficulty was rigged: the number you rolled was predetermined.</p><p class="mt-2"> Therefore, the difficulty of the captcha task was also predetermined.</p>` +
'the die roll was rigged, so that the number you received (and therefore the difficulty of the captcha task) was pre-determined. ' +
probe_closing_text; probe_closing_text;
const probe_text_difficulty = const probe_text_difficulty =
probe_preamble + `In this study, the die roll to determine captcha difficulty was indeed random. </p><p class="mt-2"> However, the difficulty of the captcha task was pre-determined. It was unrelated to the die roll.
'while the die roll was random, the difficulty of the captcha task was pre-determined and unrelated to the die roll. ' + </p>` +
probe_closing_text; 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) { switch (COND) {
case 0: case 0:
probe_condition = 'die'; probe_condition = 'die';
probe_order = 'die_first'; probe_order = 'die_first';
die_result = 4;
break; break;
case 1: case 1:
probe_condition = 'die'; probe_condition = 'die';
probe_order = 'die_first'; probe_order = 'die_first';
die_result = 4;
break; break;
case 2: case 2:
probe_condition = 'difficulty'; probe_condition = 'difficulty';
probe_order = 'die_first'; probe_order = 'die_first';
die_result = 4;
break; break;
case 3: case 3:
probe_condition = 'difficulty'; probe_condition = 'difficulty';
probe_order = 'die_first'; probe_order = 'die_first';
die_result = 4;
break; break;
case 4: case 4:
probe_condition = 'die'; probe_condition = 'die';
@@ -102,6 +98,22 @@ switch (COND) {
probe_condition = 'difficulty'; probe_condition = 'difficulty';
probe_order = 'difficulty_first'; probe_order = 'difficulty_first';
break; 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(); const stimulusMap = getStimulusMap();
@@ -174,6 +186,21 @@ const pre_survey_info = {
stimulus: stimulusMap.get('pre_survey_info'), stimulus: stimulusMap.get('pre_survey_info'),
}; };
const die_roll_practice_trial = {
type: JsPsychDieRoll,
prompt: html`
<p class="mt-2">Before the real die roll, you can practice here.</p>
<p class="mt-2">
Click the die once to start it rolling, then click it again to stop.
</p>
`,
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 = { const die_roll_trial = {
type: JsPsychDieRoll, type: JsPsychDieRoll,
prompt: html` prompt: html`
@@ -204,6 +231,13 @@ const create_captcha_trial = trial_index => {
}; };
}; };
const pre_die_roll_info = {
type: jsPsychHtmlKeyboardResponse,
choices: [' '],
stimulus: `<p>Practice complete. Now you will roll a die to determine the difficulty of the captcha task.</p>
<p class="mt-2">Press <span class ="font-bold">SPACE</span> to continue</p>`,
};
const captcha_trials = Array.from({ length: CAPTCHA_TRIAL_COUNT }, (_, index) => const captcha_trials = Array.from({ length: CAPTCHA_TRIAL_COUNT }, (_, index) =>
create_captcha_trial(index) create_captcha_trial(index)
); );
@@ -245,12 +279,12 @@ const survey_function = survey => {
}); });
}; };
const survey = { const survey_1 = {
type: jsPsychSurvey, type: jsPsychSurvey,
survey_function: survey_function, survey_function: survey_function,
survey_json: { survey_json: {
showQuestionNumbers: false, showQuestionNumbers: false,
completeText: 'Done!', completeText: 'Continue',
pageNextText: 'Continue', pageNextText: 'Continue',
pagePrevText: 'Previous', pagePrevText: 'Previous',
showPrevButton: false, showPrevButton: false,
@@ -266,10 +300,6 @@ const survey = {
isAllRowRequired: debug ? false : true, isAllRowRequired: debug ? false : true,
rowOrder: 'random', rowOrder: 'random',
rows: [ rows: [
{
text: `I found the captcha task difficult.`,
value: 'Difficulty',
},
{ {
text: `I felt lucky during the die roll.`, text: `I felt lucky during the die roll.`,
value: 'Luck', value: 'Luck',
@@ -278,6 +308,47 @@ const survey = {
text: `I felt unlucky during the die roll.`, text: `I felt unlucky during the die roll.`,
value: 'Bad_luck', 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.`, text: `I think I solved all the captchas correctly.`,
value: 'Accuracy', 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: [ elements: [
{ {
type: 'matrix', type: 'matrix',
name: name: "Please indicate your agreement with the following statements.",
probe_condition === 'die'
? probe_text_die
: probe_text_difficulty,
alternateRows: true, alternateRows: true,
isAllRowRequired: debug ? false : true, isAllRowRequired: debug ? false : true,
rowOrder: 'random', 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', name: 'page3',
@@ -403,22 +517,28 @@ const survey = {
const main_experiment_timeline = [ const main_experiment_timeline = [
instructions_1, instructions_1,
instructions_2, instructions_2,
die_roll_practice_trial,
pre_die_roll_info,
die_roll_trial, die_roll_trial,
instructions_3, instructions_3,
...captcha_trials, ...captcha_trials,
pre_survey_info, pre_survey_info,
survey, survey_1,
pre_manip_info,
survey_2,
debrief, debrief,
]; ];
if (debug) { if (debug) {
timeline.push(die_roll_trial);
timeline.push(...main_experiment_timeline); timeline.push(...main_experiment_timeline);
} }
if (!debug) { if (!debug) {
timeline.push(pre_consent_info); timeline.push(pre_consent_info);
timeline.push(consent_form);
timeline.push(enter_fullscreen); timeline.push(enter_fullscreen);
timeline.push(consent_form);
timeline.push(...main_experiment_timeline); timeline.push(...main_experiment_timeline);
} }
jsPsych.run(timeline); jsPsych.run(timeline);

View File

@@ -27,6 +27,14 @@ const info = {
type: ParameterType.STRING, type: ParameterType.STRING,
default: 'You rolled a {{value}}.', 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) { trial(display_element, trial) {
const start_time = performance.now(); const start_time = performance.now();
const sanitized_rigged_value = this.get_rigged_value(trial.rigged_value); const practice_enabled = Boolean(trial.practice_trial);
const final_value = const practice_roll_limit = practice_enabled
sanitized_rigged_value ?? ? this.get_practice_roll_limit(trial.practice_roll_limit)
this.jsPsych.randomization.sampleWithoutReplacement( : 1;
[1, 2, 3, 4, 5, 6], const sanitized_rigged_value = practice_enabled
1 ? null
)[0]; : 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( display_element.innerHTML = this.build_markup(
trial.prompt, trial.prompt,
@@ -60,21 +72,60 @@ class DieRollPlugin {
'.jspsych_die_roll_button' '.jspsych_die_roll_button'
); );
const PRACTICE_UNLOCK_DELAY_MS = 600;
const MIN_IDLE_CLICK_INTERVAL_MS = 350;
let is_rolling = false; let is_rolling = false;
let has_rolled = false; let has_rolled = false;
let roll_started_at = null; let roll_started_at = null;
let roll_stopped_at = null; let roll_stopped_at = null;
let die_click_count = 0; 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 = () => { const clear_timeouts = () => {
if (practice_enabled && practice_reenable_timeout !== null) {
window.clearTimeout(practice_reenable_timeout);
practice_reenable_timeout = null;
}
this.jsPsych.pluginAPI.clearAllTimeouts(); 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 = () => { const cycle_face = () => {
if (!is_rolling) { if (!is_rolling) {
return; return;
} }
const value = this.random_face(final_value); const value = get_next_face_value();
face_element.textContent = value; face_element.textContent = value;
this.jsPsych.pluginAPI.setTimeout(cycle_face, trial.animation_interval); this.jsPsych.pluginAPI.setTimeout(cycle_face, trial.animation_interval);
}; };
@@ -88,25 +139,55 @@ class DieRollPlugin {
die_click_count += 1; die_click_count += 1;
roll_stopped_at = performance.now(); roll_stopped_at = performance.now();
clear_timeouts(); clear_timeouts();
face_element.textContent = final_value; last_roll_value = pending_final_value;
face_element.classList.remove('jspsych_die_roll_face--active'); face_element.textContent = last_roll_value;
face_element.classList.remove('jspsych_die_roll_face--interactive'); lock_die_face();
face_element.classList.add('jspsych_die_roll_face--locked'); practice_rolls_completed += 1;
face_element.setAttribute('aria-disabled', 'true'); const rolls_remaining = Math.max(
face_element.removeAttribute('tabindex'); practice_roll_limit - practice_rolls_completed,
face_element.style.pointerEvents = 'none'; 0
result_element.textContent = trial.result_template.replace(
/\{\{value\}\}/gi,
final_value
); );
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} <p class = "mt-2"> You may roll again (${rolls_remaining} ` +
`practice ${rolls_remaining === 1 ? 'roll' : 'rolls'} remaining) or click 'Continue' to proceed.</p>`;
// 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.disabled = false;
if (!practice_enabled || rolls_remaining === 0) {
button_element.focus(); button_element.focus();
}
}; };
const begin_roll = () => { const begin_roll = () => {
if (is_rolling || has_rolled) { if (is_rolling || has_rolled) {
return; return;
} }
previous_face_value = null;
pending_final_value = get_next_result_value();
is_rolling = true; is_rolling = true;
die_click_count += 1; die_click_count += 1;
roll_started_at = performance.now(); roll_started_at = performance.now();
@@ -117,31 +198,59 @@ class DieRollPlugin {
cycle_face(); cycle_face();
}; };
const handle_face_interaction = () => { const handle_face_interaction = event => {
if (!is_rolling && !has_rolled) { const event_type = event?.type ?? 'unknown';
begin_roll();
} else if (is_rolling && !has_rolled) { 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(); finalize_roll();
return;
}
if (!has_rolled) {
begin_roll();
return;
} }
}; };
button_element.disabled = true; button_element.disabled = true;
face_element.textContent = ''; face_element.textContent = '';
result_element.textContent = result_element.innerHTML = practice_enabled
'Click the die to start, then click it again to lock in your number.'; ? `<p>Practice rolling the die. You can complete up to ${practice_roll_limit}</p>` +
face_element.classList.add('jspsych_die_roll_face--interactive'); `practice ${practice_roll_limit === 1 ? 'roll' : 'rolls'}. <p class = "mt-2"> Click the die to start, then click it again to stop. </p>`
face_element.setAttribute('tabindex', '0'); : '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('role', 'button');
face_element.setAttribute( face_element.setAttribute(
'aria-label', 'aria-label',
'Virtual die. Click to start and stop.' '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 => { face_element.addEventListener('keydown', event => {
if (event.key === 'Enter' || event.key === ' ') { if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault(); event.preventDefault();
handle_face_interaction(); handle_face_interaction(event);
} }
}); });
@@ -149,7 +258,7 @@ class DieRollPlugin {
is_rolling = false; is_rolling = false;
clear_timeouts(); clear_timeouts();
const trial_data = { const trial_data = {
roll: final_value, roll: last_roll_value,
rigged_value: sanitized_rigged_value, rigged_value: sanitized_rigged_value,
rigged: sanitized_rigged_value !== null, rigged: sanitized_rigged_value !== null,
die_click_count: die_click_count, die_click_count: die_click_count,
@@ -161,6 +270,14 @@ class DieRollPlugin {
? roll_stopped_at - roll_started_at ? roll_stopped_at - roll_started_at
: null, : null,
rt: performance.now() - start_time, 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 = ''; display_element.innerHTML = '';
this.jsPsych.finishTrial(trial_data); this.jsPsych.finishTrial(trial_data);
@@ -187,12 +304,11 @@ class DieRollPlugin {
`; `;
} }
random_face(exclude_value) { get_random_value() {
let value = Math.floor(Math.random() * 6) + 1; return this.jsPsych.randomization.sampleWithoutReplacement(
if (value === exclude_value) { [1, 2, 3, 4, 5, 6],
value = (value % 6) + 1; 1
} )[0];
return value;
} }
get_rigged_value(value) { get_rigged_value(value) {
@@ -202,6 +318,27 @@ class DieRollPlugin {
} }
return null; 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; DieRollPlugin.info = info;

View File

@@ -19,11 +19,11 @@ function getStimulusMap() {
</p> </p>
<p class="leading-relaxed mt-2""> <p class="leading-relaxed mt-2"">
You will now answer a few questions about your experience in the You will now answer a few questions about your experience in the
experiment. It is important that you read the questions carefully and experiment.
answer them honestly.
</p> </p>
<p class="leading-relaxed mt-2"> <p class="leading-relaxed mt-2">
Nonsense or random answers may lead to your submission being rejected. It is important that you read the questions carefully and
answer them honestly.
</p> </p>
<p class="mt-6"> <p class="mt-6">
Press Press
@@ -167,15 +167,14 @@ function getStimulusMap() {
stimulusMap.set( stimulusMap.set(
'instructions_1', 'instructions_1',
html` html`
<p>In this experiment, you will be solving some captchas.</p> <p>In this experiment, you will be solving captchas.</p>
<p class="mt-2"> <p class="mt-2">
This will involve identifying slightly distorted letters and numbers This will involve identifying slightly distorted letters and numbers
from an image. from an image.
</p> </p>
<p class="mt-2"> <p class="mt-6">
Your task is to solve the captchas as quickly and accurately as Your task is to solve the captchas as quickly and accurately as
possible. You will not be given feedback on your performance. possible. </p>
</p>
<p class="mt-6"> <p class="mt-6">
Press Press
<strong>SPACE</strong> <strong>SPACE</strong>
@@ -188,9 +187,9 @@ function getStimulusMap() {
'instructions_2', 'instructions_2',
html` html`
<p> <p>
To randomise the difficulty of the captchas you will be solving, we will In this study, we will randomise the difficulty of the captchas you will solve. </p>
first ask you to roll a virtual die. You will then be shown captchas of <p class="mt-2">
a difficulty corresponding to the number you rolled. You will roll a virtual die to determine the difficulty of the captchas you will see.
</p> </p>
<p class="mt-2"> <p class="mt-2">
The higher the number you roll, the more difficult the captchas will be. The higher the number you roll, the more difficult the captchas will be.
@@ -207,18 +206,9 @@ function getStimulusMap() {
'instructions_3', 'instructions_3',
html` html`
<p> <p>
You will now proceed to the captcha task, in which you will need to You will now proceed to the captcha task </p> <p class = "mt-2"> You will need to
solve 12 captchas in a row as quickly and accurately as possible. solve 12 captchas in a row as quickly and accurately as possible.
</p> </p>
<p class="mt-2">
Please ensure that you are in a quiet environment and that you will not
be interrupted for the next few minutes.
</p>
<p class="mt-2">
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.
</p>
<p class="mt-6 text-red-500"> <p class="mt-6 text-red-500">
Press Press
<strong>SPACE</strong> <strong>SPACE</strong>

View File

@@ -76,8 +76,10 @@
.jspsych_die_roll_face--locked { .jspsych_die_roll_face--locked {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.8; opacity: 0.5;
box-shadow: 0 15px 25px rgba(15, 23, 42, 0.15); box-shadow: 0 15px 25px rgba(15, 23, 42, 0.15);
background: linear-gradient(145deg, #d4d8de, #cbd2db);
color: #475569;
} }
@keyframes pulse { @keyframes pulse {