function showSlide(id) {
I’m implementing the experiment using a data structure that I call a sequence. The insight behind sequences is that many experiments consist of a sequence of largely homogeneous trials that vary based on a parameter. For instance, in this example experiment, a lot stays the same from trial to trial - we always have to present some number, the subject always has to make a response, and we always want to record that response. Of course, the trials do differ - we’re displaying a different number every time. The idea behind the sequence is to separate what stays the same from what differs - to separate code from data. This results in parametric code, which is much easier to maintain - it’s simple to add, remove, or change conditions, do randomization, and do testing.
Things happen in this order:
{{}}
slots that indicate which keys to press for even/odd, and show the instructions slide.experiment.next()
experiment.next()
checks if there are any trials left to do. If there aren’t, it calls experiment.end()
, which shows the finish slide, waits for 1.5 seconds, and then uses mmturkey to submit to Turk.experiment.next()
shows the next trial, records the current time for computing reaction time, and sets up a listener for a key press.experiment.data
array. Then we show a blank screen and wait 500 milliseconds before calling experiment.next()
again.Shows slides. We’re using jQuery here - the $ is the jQuery selector function, which takes as input either a DOM element or a CSS selector string.
function showSlide(id) {
Hide all slides
$(".slide").hide();
Show just the slide we want to show
$("#"+id).show();
}
Get a random integer less than n.
function randomInteger(n) {
return Math.floor(Math.random()*n);
}
Get a random element from an array (e.g., random_element([4,8,7])
could return 4, 8, or 7). This is useful for condition randomization.
function randomElement(array) {
return array[randomInteger(array.length)];
}
var allKeyBindings = [
{"p": "odd", "q": "even"},
{"p": "even", "q": "odd"} ],
allTrialOrders = [
[1,3,2,5,4,9,8,7,6],
[8,4,3,7,5,6,2,1,9] ],
myKeyBindings = randomElement(allKeyBindings),
myTrialOrder = randomElement(allTrialOrders),
pOdd = (myKeyBindings["p"] == "odd");
Fill in the instructions template using jQuery’s html()
method. In particular,
let the subject know which keys correspond to even/odd. Here, I’m using the so-called ternary operator, which is a shorthand for if (…) { … } else { … }
$("#odd-key").text(pOdd ? "P" : "Q");
$("#even-key").text(pOdd ? "Q" : "P");
Show the instructions slide — this is what we want subjects to see first.
showSlide("instructions");
I implement the sequence as an object with properties and methods. The benefit of encapsulating everything in an object is that it’s conceptually coherent (i.e. the data
variable belongs to this particular sequence and not any other) and allows you to compose sequences to build more complicated experiments. For instance, if you wanted an experiment with, say, a survey, a reaction time test, and a memory test presented in a number of different orders, you could easily do so by creating three separate sequences and dynamically setting the end()
function for each sequence so that it points to the next. More practically, you should stick everything in an object and submit that whole object so that you don’t lose data (e.g. randomization parameters, what condition the subject is in, etc). Don’t worry about the fact that some of the object properties are functions — mmturkey (the Turk submission library) will strip these out.
var experiment = {
Parameters for this sequence.
trials: myTrialOrder,
Experiment-specific parameters - which keys map to odd/even
keyBindings: myKeyBindings,
An array to store the data that we’re collecting.
data: [],
The function that gets called when the sequence is finished.
end: function() {
Show the finish slide.
showSlide("finished");
Wait 1.5 seconds and then submit the whole experiment object to Mechanical Turk (mmturkey filters out the functions so we know we’re just submitting properties [i.e. data])
setTimeout(function() { turk.submit(experiment) }, 1500);
},
The work horse of the sequence - what to do on every trial.
next: function() {
If the number of remaining trials is 0, we’re done, so call the end function.
if (experiment.trials.length == 0) {
experiment.end();
return;
}
Get the current trial - shift()
removes the first element of the array and returns it.
var n = experiment.trials.shift();
Compute the correct answer.
var realParity = (n % 2 == 0) ? "even" : "odd";
showSlide("stage");
Display the number stimulus.
$("#number").text(n);
Get the current time so we can compute reaction time later.
var startTime = (new Date()).getTime();
Set up a function to react to keyboard input. Functions that are used to react to user input are called event handlers. In addition to writing these event handlers, you have to bind them to particular events (i.e., tell the browser that you actually want the handler to run when the user performs an action). Note that the handler always takes an event
argument, which is an object that provides data about the user input (e.g., where they clicked, which button they pressed).
var keyPressHandler = function(event) {
A slight disadvantage of this code is that you have to test for numeric key values; instead of writing code that expresses “do X if ‘Q’ was pressed“, you have to do the more complicated “do X if the key with code 80 was pressed“. A library like Keymaster lets you write simpler code like key(‘a’, function(){ alert(‘you pressed a!’) })
, but I’ve omitted it here. Here, we get the numeric key code from the event object
var keyCode = event.which;
if (keyCode != 81 && keyCode != 80) {
If a key that we don’t care about is pressed, re-attach the handler (see the end of this script for more info)
$(document).one("keydown", keyPressHandler);
} else {
If a valid key is pressed (code 80 is p, 81 is q), record the reaction time (current time minus start time), which key was pressed, and what that means (even or odd).
var endTime = (new Date()).getTime(),
key = (keyCode == 80) ? "p" : "q",
userParity = experiment.keyBindings[key],
data = {
stimulus: n,
accuracy: realParity == userParity ? 1 : 0,
rt: endTime - startTime
};
experiment.data.push(data);
Temporarily clear the number.
$("#number").text("");
Wait 500 milliseconds before starting the next trial.
setTimeout(experiment.next, 500);
}
};
Here, we actually bind the handler. We’re using jQuery’s one()
function, which ensures that the handler can only run once. This is very important, because generally you only want the handler to run only once per trial. If you don’t bind with one()
, the handler might run multiple times per trial, which can be disastrous. For instance, if the user accidentally presses P twice, you’ll be recording an extra copy of the data for this trial and (even worse) you will be calling experiment.next
twice, which will cause trials to be skipped! That said, there are certainly cases where you do want to run an event handler multiple times per trial. In this case, you want to use the bind()
and unbind()
functions, but you have to be extra careful about properly unbinding.
$(document).one("keydown", keyPressHandler);
}
}