Skip to content

Commit ac4e9d7

Browse files
authored
Fall back to downex instructions during older kid tasks (#421)
* add fallback to downex trials for mental rotation and matrix reasoning * add fallback logic to memory game * standardize button feedback for correct/incorrect non-practice trials
1 parent f54dd3b commit ac4e9d7

9 files changed

Lines changed: 155 additions & 123 deletions

File tree

task-launcher/patches/@jspsych-contrib/plugin-corsi-blocks/dist/index.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ class CorsiBlocksPlugin {
283283
.querySelector(`.jspsych-corsi-block[data-id="${id}"]`)
284284
.animate(correct_animation, animation_timing);
285285
}
286-
else if (!correct) {
286+
else if (!correct && !trial.disable_animation) {
287287
display_element
288288
.querySelector(`.jspsych-corsi-block[data-id="${id}"]`)
289289
.animate(incorrect_animation, animation_timing);

task-launcher/patches/@jspsych-contrib/plugin-corsi-blocks/dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

task-launcher/src/tasks/matrix-reasoning/timeline.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
createPreloadTrials,
77
getRealTrials,
88
batchTrials,
9-
batchMediaAssets,
9+
batchMediaAssets,
10+
checkFallbackCriteria,
1011
} from '../shared/helpers';
1112
import { downexInstructions1, downexInstructions2, downexInstructions3, downexInstructions4, downexInstructions5, instructions } from './trials/instructions';
1213
import { downexStimulus } from './trials/downexStimulus';
@@ -130,12 +131,31 @@ export default function buildMatrixTimeline(config: Record<string, any>, mediaAs
130131
}
131132
}
132133

133-
const repeatInstructions = {
134-
timeline: [repeatInstructionsMessage, ...instructions],
134+
const secondPhaseIndex = 5;
135+
let fellBack = false;
136+
137+
// give older kids the downex items if they meet fall back criteria
138+
const fallbackBlock = {
139+
timeline: [
140+
repeatInstructionsMessage,
141+
downexInstructions1,
142+
...downexCorpus.slice(0, secondPhaseIndex).map((trial) => [{...fixationOnly, stimulus: ''}, downexStimulus(layoutConfigMap, true, trial), ifRealTrialResponse]).flat(),
143+
downexInstructions2,
144+
downexInstructions3,
145+
practiceTransition(undefined, true),
146+
...downexCorpus.slice(secondPhaseIndex).map((trial) => [{...fixationOnly, stimulus: ''}, downexStimulus(layoutConfigMap, false, trial), ifRealTrialResponse]).flat(),
147+
downexInstructions4,
148+
downexInstructions5,
149+
],
135150
conditional_function: () => {
136-
return taskStore().numIncorrect >= 2;
151+
const run = checkFallbackCriteria() && !fellBack && !heavyInstructions;
152+
if (run) {
153+
fellBack = true;
154+
}
155+
156+
return run;
137157
},
138-
};
158+
}
139159

140160
function preloadBatch() {
141161
timeline.push(createPreloadTrials(batchedMediaAssets[currPreloadBatch]).default);
@@ -181,7 +201,6 @@ export default function buildMatrixTimeline(config: Record<string, any>, mediaAs
181201
timeline.push(unnormedBlock);
182202
} else {
183203
const numOfDownexTrials = taskStore().totalDownexTrials;
184-
const secondPhaseIndex = 5; // stop animation after 5 trials
185204

186205
if (heavyInstructions) {
187206
for (let i = 0; i < numOfDownexTrials; i++) {
@@ -203,12 +222,14 @@ export default function buildMatrixTimeline(config: Record<string, any>, mediaAs
203222
const numOfTrials = taskStore().totalTrials;
204223
taskStore('totalTestTrials', heavyInstructions ? getRealTrials(defaultCorpus) + getRealTrials(downexCorpus) : getRealTrials(defaultCorpus));
205224

225+
const numOfInitialPracticeTrials = defaultCorpus.filter((trial) => trial.assessmentStage === 'practice_response').length;
226+
const fallbackIndex = numOfInitialPracticeTrials + 4;
206227
for (let i = 0; i < numOfTrials; i += 1) {
207228
if (i % batchSize === 0) {
208229
preloadBatch();
209230
}
210-
if (i === 4) {
211-
timeline.push(repeatInstructions);
231+
if (i <= fallbackIndex) {
232+
timeline.push(fallbackBlock);
212233
}
213234
timeline.push(stimulusBlock);
214235
}

task-launcher/src/tasks/matrix-reasoning/trials/downexStimulus.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ let practiceResponses = []
1818
let startTime: number;
1919
let audioEnabled = false; // disable audio if the trial has changed since the loop started - prevent overlapping audio
2020

21-
export const downexStimulus = (layoutConfigMap: Record<string, LayoutConfigType>, animate: boolean) => {
21+
export const downexStimulus = (layoutConfigMap: Record<string, LayoutConfigType>, animate: boolean, trial?: StimulusType) => {
2222
return {
2323
type: jsPsychHtmlMultiResponse,
2424
data: () => {
25-
const stim = taskStore().nextStimulus;
25+
const stim = trial || taskStore().nextStimulus;
2626
let isPracticeTrial = stim.assessmentStage === 'practice_response';
2727
return {
2828
// not camelCase because firekit
@@ -33,7 +33,7 @@ export const downexStimulus = (layoutConfigMap: Record<string, LayoutConfigType>
3333
};
3434
},
3535
stimulus: () => {
36-
const stim = taskStore().nextStimulus;
36+
const stim = trial || taskStore().nextStimulus;
3737
const t = taskStore().translations;
3838
const imageSrc = mediaAssets.images[camelize(stim.item)];
3939

@@ -66,7 +66,7 @@ export const downexStimulus = (layoutConfigMap: Record<string, LayoutConfigType>
6666
},
6767
prompt_above_buttons: true,
6868
button_choices: () => {
69-
const stim = taskStore().nextStimulus;
69+
const stim = trial || taskStore().nextStimulus;
7070
const itemLayoutConfig = layoutConfigMap?.[stim.itemId];
7171
const choices = itemLayoutConfig.response.displayValues;
7272

@@ -78,7 +78,7 @@ export const downexStimulus = (layoutConfigMap: Record<string, LayoutConfigType>
7878
},
7979
keyboard_choices: () => 'NO_KEYS',
8080
button_html: () => {
81-
const stim = taskStore().nextStimulus;
81+
const stim = trial || taskStore().nextStimulus;
8282
const itemLayoutConfig = layoutConfigMap?.[stim.itemId];
8383
const classList = [...itemLayoutConfig.classOverrides.buttonClassList];
8484
if (stim.assessmentStage === 'practice_response') {
@@ -92,7 +92,7 @@ export const downexStimulus = (layoutConfigMap: Record<string, LayoutConfigType>
9292
on_load: async () => {
9393
startTime = performance.now();
9494

95-
const stim = taskStore().nextStimulus;
95+
const stim = trial || taskStore().nextStimulus;
9696

9797
// set up replay audio with animations
9898
const trialAudio = stim.audioFile;
@@ -214,7 +214,7 @@ export const downexStimulus = (layoutConfigMap: Record<string, LayoutConfigType>
214214
PageAudioHandler.stopAndDisconnectNode();
215215
audioEnabled = false;
216216

217-
const stimulus = taskStore().nextStimulus;
217+
const stimulus = trial || taskStore().nextStimulus;
218218
const itemLayoutConfig = layoutConfigMap?.[stimulus.itemId];
219219
const { corpus } = taskStore();
220220

@@ -291,7 +291,7 @@ export const downexStimulus = (layoutConfigMap: Record<string, LayoutConfigType>
291291
}
292292
},
293293
response_ends_trial: () => {
294-
const stim = taskStore().nextStimulus;
294+
const stim = trial || taskStore().nextStimulus;
295295

296296
return stim.assessmentStage !== 'practice_response';
297297
},

task-launcher/src/tasks/memory-game/timeline.ts

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { initTimeline, initTrialSaving, createPreloadTrials, PageAudioHandler } from '../shared/helpers';
1+
import { initTimeline, initTrialSaving, createPreloadTrials, checkFallbackCriteria, PageAudioHandler } from '../shared/helpers';
22
// setup
33
import { jsPsych } from '../taskSetup';
44
import { initializeCat } from '../taskSetup';
55
// trials
6-
import { enterFullscreen, exitFullscreen, feedback, finishExperiment, taskFinished } from '../shared/trials';
6+
import { enterFullscreen, exitFullscreen, feedback, finishExperiment, repeatInstructionsMessage, taskFinished } from '../shared/trials';
77
import { getCorsiBlocks } from './trials/stimulus';
88
import { readyToPlay, reverseOrderPrompt, reverseOrderInstructions, defaultInstructions, downexInstructions } from './trials/instructions';
99
import { taskStore } from '../../taskStore';
@@ -69,7 +69,7 @@ export default function buildMemoryTimeline(config: Record<string, any>) {
6969

7070
const corsiBlocksStimulus = {
7171
timeline: [forwardTrial()],
72-
repetitions: 20,
72+
repetitions: 16,
7373
};
7474

7575
// last forward trial by itself in order to reset sequence length back to 2 for backward phase
@@ -101,11 +101,11 @@ export default function buildMemoryTimeline(config: Record<string, any>) {
101101
},
102102
}
103103

104-
const downexFeedbackIncorrect = (reverse: boolean, seqlength: number, prompt: string) => {
104+
const downexFeedbackIncorrect = (reverse: boolean, prompt: string) => {
105105
return {
106106
timeline: [
107107
getCorsiBlocks(
108-
{ reverse, mode: 'input', isPractice: true, customSeqLength: seqlength, animation: 'pulse', prompt }
108+
{ reverse, mode: 'input', isPractice: true, animation: 'pulse', prompt }
109109
),
110110
],
111111
conditional_function: () => {
@@ -115,41 +115,68 @@ export default function buildMemoryTimeline(config: Record<string, any>) {
115115
}
116116

117117
const downexPracticeTrial = (
118-
reverse: boolean,
119-
currentSeqlength: number,
120-
setNextSeqLength: number,
118+
reverse: boolean,
119+
seqlength: number,
121120
animation?: 'pulse' | 'cursor',
122121
) => {
123122
return {
124123
timeline: [
125-
getCorsiBlocks({ reverse, mode: 'display', isPractice: true, customSeqLength: currentSeqlength }),
126-
getCorsiBlocks({ reverse, mode: 'input', isPractice: true, customSeqLength: setNextSeqLength, animation }),
124+
getCorsiBlocks({ reverse, mode: 'display', isPractice: true, customSeqLength: seqlength }),
125+
getCorsiBlocks({ reverse, mode: 'input', isPractice: true, animation }),
127126
downexFeedbackCorrect,
128-
downexFeedbackIncorrect(reverse, setNextSeqLength, reverse ? 'memoryGameInstruct11Downex' : 'memoryGameFeedbackIncorrectDownex'),
127+
downexFeedbackIncorrect(reverse, reverse ? 'memoryGameInstruct11Downex' : 'memoryGameFeedbackIncorrectDownex'),
129128
]
130129
}
131130
}
132131

133132
const downexInstructionsTimeline = {
134133
timeline: [
135134
downexInstructions[0],
136-
downexPracticeTrial(false, 1, 1, 'cursor'),
137-
downexPracticeTrial(false, 1, 2),
135+
downexPracticeTrial(false, 1, 'cursor'),
136+
downexPracticeTrial(false, 1),
138137
downexInstructions[1],
139-
downexPracticeTrial(false, 2, 2, 'cursor'),
140-
downexPracticeTrial(false, 2, 2),
141-
downexPracticeTrial(false, 2, 2),
138+
downexPracticeTrial(false, 2, 'cursor'),
139+
downexPracticeTrial(false, 2),
140+
downexPracticeTrial(false, 2),
142141
downexInstructions[2],
143142
downexInstructions[3],
144143
downexInstructions[4],
145144
]
146145
}
147146

147+
let fellBack = false;
148+
const fallbackBlock = {
149+
timeline: [
150+
repeatInstructionsMessage,
151+
...downexInstructionsTimeline.timeline.slice(1)
152+
],
153+
conditional_function: () => {
154+
const run = checkFallbackCriteria(true) && !fellBack;
155+
if (run) {
156+
fellBack = true;
157+
taskStore('heavyInstructions', true);
158+
taskStore('gridSize', 2);
159+
taskStore('numOfBlocks', 4);
160+
taskStore('blockSize', 50);
161+
}
162+
163+
return run;
164+
},
165+
}
166+
167+
const firstFourTestTrials = {
168+
timeline: [
169+
forwardTrial(),
170+
fallbackBlock,
171+
],
172+
repetitions: 4,
173+
}
174+
148175
const downexCorsiBlocksPracticeReverse = {
149176
timeline: [
150-
downexPracticeTrial(true, 2, 2, 'cursor'),
151-
downexPracticeTrial(true, 2, 2),
152-
downexPracticeTrial(true, 2, 2),
177+
downexPracticeTrial(true, 2, 'cursor'),
178+
downexPracticeTrial(true, 2),
179+
downexPracticeTrial(true, 2),
153180
]
154181
}
155182

@@ -174,6 +201,7 @@ export default function buildMemoryTimeline(config: Record<string, any>) {
174201
preloadTrials,
175202
initialTimeline,
176203
heavyInstructions ? downexInstructionsTimeline : defaultInstructionsTimeline,
204+
firstFourTestTrials, // check for fallback criteria during first 4 trials
177205
corsiBlocksStimulus,
178206
forwardTrialResetSeq,
179207
reverseOrderInstructions,

task-launcher/src/tasks/memory-game/trials/stimulus.ts

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -94,21 +94,34 @@ export function getCorsiBlocks(
9494
type: jsPsychCorsiBlocks,
9595
sequence: () => {
9696
// On very first trial, generate initial sequence
97-
if (!generatedSequence) {
97+
if (mode === 'display') {
9898
const numOfBlocks: number = Number(taskStore().numOfBlocks);
99-
generatedSequence = generateRandomSequence(
100-
{ numOfBlocks, sequenceLength: customSeqLength || sequenceLength, previousSequence: null }
101-
);
99+
// Avoid generating the same sequence twice in a row
100+
let newSequence = generateRandomSequence({
101+
numOfBlocks,
102+
sequenceLength: customSeqLength || sequenceLength,
103+
previousSequence: generatedSequence,
104+
});
105+
106+
while (_isEqual(newSequence, generatedSequence)) {
107+
newSequence = generateRandomSequence({
108+
numOfBlocks,
109+
sequenceLength: customSeqLength || sequenceLength,
110+
previousSequence: generatedSequence,
111+
});
112+
}
113+
114+
generatedSequence = newSequence;
102115
}
103116

104-
if (mode === 'input' && reverse) {
117+
if (generatedSequence && mode === 'input' && reverse) {
105118
return [...generatedSequence].reverse(); // Create a copy before reversing
106119
} else {
107120
return generatedSequence;
108121
}
109122
},
110123
blocks: () => {
111-
if (!grid) {
124+
if (mode === 'display') {
112125
const { numOfBlocks, blockSize, gridSize } = taskStore();
113126
grid = createGrid({ x, y, numOfBlocks, blockSize, gridSize, blockSpacing });
114127
}
@@ -214,26 +227,6 @@ export function getCorsiBlocks(
214227

215228
const numOfBlocks = taskStore().numOfBlocks;
216229

217-
// resuse the same sequence for incorrect downward extension trials
218-
if (data.correct || !isPractice || !heavyInstructions) {
219-
// Avoid generating the same sequence twice in a row
220-
let newSequence = generateRandomSequence({
221-
numOfBlocks,
222-
sequenceLength: customSeqLength || sequenceLength,
223-
previousSequence: generatedSequence,
224-
});
225-
226-
while (_isEqual(newSequence, generatedSequence)) {
227-
newSequence = generateRandomSequence({
228-
numOfBlocks,
229-
sequenceLength: customSeqLength || sequenceLength,
230-
previousSequence: generatedSequence,
231-
});
232-
}
233-
234-
generatedSequence = newSequence;
235-
}
236-
237230
if (!isPractice) {
238231
timeoutIDs.forEach((id) => clearTimeout(id));
239232
timeoutIDs = [];
@@ -356,9 +349,8 @@ function doOnLoad(
356349
if (inputSequence !== null) {
357350
const nextBlockIndex = inputSequence[clickCount];
358351

359-
if (i === nextBlockIndex) {
360-
(event.target as HTMLDivElement).style.backgroundColor = HIGHLIGHT_COLOR
361-
}
352+
const color = isPractice && i !== nextBlockIndex ? INCORRECT_COLOR : HIGHLIGHT_COLOR;
353+
(event.target as HTMLDivElement).style.backgroundColor = color;
362354

363355
Array.from(blocks).forEach((element, j) => {
364356
if (i !== j) {

0 commit comments

Comments
 (0)