Experiment structure
Basic experiment script
The most basic PennController experiment script contains a single line:
PennController.newTrial()
When you run a PennController script, its commands are evaluated and executed in sequential order, from top to bottom. In this basic script, the only command is the PennController.newTrial
command, which creates and starts a trial.
Once a trial starts, it ends when all of the commands within the trial finish being executed. In this example, the trial does not have commands, meaning that the trial starts and then ends almost immediately.
Similarly, a PennController experiment ends when all of its commands finish being executed. In this example, the experiment ends after its only command, the PennController.newTrial
command, ends.
Trial structure
Trials have the following (simplified) structure, where:
- The optional first argument is a string and the trial’s label, or the trial has no label.
@// Option 1: Trial has the label "example-label" $PennController.newTrial("example-label", @ newX() @ .ELEMENT_COMMAND() @ , @ getX() @ .ELEMENT_COMMAND() @) @ @// Option 2: Trial does not have a label $PennController.newTrial( @ newX() @ .ELEMENT_COMMAND() @ , @ getX() @ .ELEMENT_COMMAND() @)
Trial labels are case-sensitive:
"helloWorld"
and"HelloWorld"
are two different labels.
Although trial labels are not strictly obligatory, we recommend giving every trial a label. Labeled trials make it easier to debug an experiment script, and some commands actually require that a trial have a label.
- Any other argument is an element-command block.
@PennController.newTrial("TRIAL_LABEL", $ newX() $ .ELEMENT_COMMAND() @ , $ getX() $ .ELEMENT_COMMAND() @)
- All arguments are separated by a comma (note the comma after the first argument, the trial label
"TRIAL_LABEL"
).$PennController.newTrial("TRIAL_LABEL", @ newX() @ .ELEMENT_COMMAND() $ , @ getX() @ .ELEMENT_COMMAND() @)
We’ll talk about elements and commands more in upcoming sections, but for now the important parts to know are that:
newX()
is a function that creates an element.getX()
is a function that refers back to an element that has been created..ELEMENT_COMMAND()
is a placeholder for an element command.- Elements contain content, and element commands manipulate that content.
The PennController.
prefix
PennController global commands are commands that used outside of a trial. By default, all global commands begin with the prefix PennController.
in order to avoid naming conflicts with other JavaScript modules. For example, PennController.newTrial
is a global command that creates a trial. In addition, the newX()
and getX()
functions have the prefix PennController.Elements.
The global command PennController.ResetPrefix(null)
removes the PennController.(Elements.)
prefix for all subsequent calls of a global command and instances of a newX
or getX()
function. You can also pass a string instead of null
, which resets the prefix to the given string.
- Before removing the prefix
PennController.newTrial("TRIAL_LABEL", PennController.Elements.newX() .ELEMENT_COMMAND() , PennController.Elements.getX() .ELEMENT_COMMAND() )
- After removing the prefix
PennController.ResetPrefix(null) newTrial("TRIAL_LABEL", newX() .ELEMENT_COMMAND() , getX() .ELEMENT_COMMAND() )
- After resetting the prefix to a string
PennController.ResetPrefix("kiwi") kiwi.newTrial("TRIAL_LABEL", kiwi.newX() .ELEMENT_COMMAND() , kiwi.getX() .ELEMENT_COMMAND() )
From this point forward, this guide assumes that PennController.ResetPrefix(null)
has been called, and the PennController.(Elements.)
prefix removed.
For example, in prose we’ll say “the newTrial
command” instead of “the PennController.newTrial
command”. Code examples will generally omit an explicit call to PennController.ResetPrefix
, but may include such a call for additional clarity.
However, the PennController.
prefix is always included when referring to the PennController.ResetPrefix
command itself, because the command must include the prefix when it is first called.
We recommend calling PennController.ResetPrefix(null)
at the start of every experiment script for readability and convenience. Not having to type the PennController.(Elements.)
prefix quickly adds up once you start writing a script with multiple trials and elements!
Syntax
The following special symbols are obligatory, and omitting one or more will cause an error.
(
and )
(parentheses)
When calling a function or command, the function or command name must be followed by a pair of matching parentheses. For example, to call the newTrial
command, type newTrial()
.
@// Incorrect: The newTrial command does not have a closing parenthesis
parenthesis
%newTrial("example-trial-one",
% newX)
% ,
% newX()
% ,
% getX()
@
@// Incorrect: The second call of the newX() function does not have parentheses
%newTrial("example-trial-two",
% newX()
% ,
% newX
% ,
% getX()
%)
@
@// Correct: All functions and commands have matching parentheses
~newTrial("example-trial-three",
~ newX()
~ ,
~ newX()
~ ,
~ getX()
~)
.
(period)
When calling an element command, the command name must be preceded by a period. For example, to call the print
command, type .print()
.
@// Incorrect: the 'print' element command does not have a preceding period
%newTrial("example-trial-one",
% newX()
% print()
%)
@
@// Correct: The 'print' element command has a preceding period
~newTrial("example-trial-two",
~ newX()
~ .print()
~)
@
@// Incorrect: The 'newTrial' command is a global command, not an element command.
@// It should not be preceded by a period.
%.newTrial("example-trial-three",
% newX()
% .print()
%)
,
(comma)
Trial arguments must be separated by a comma.
@// Incorrect: There is no comma after the trial label argument "example-trial"
%newTrial("example-trial"
% newX()
% ,
% newX()
%)
@
@// Correct: All three trial arguments are separated by a comma.
~newTrial("example-trial",
~ newX()
~ ,
~ newX()
~)
"
or '
(quote mark)
If there is a trial label argument, it is a string and must be surrounded by matching quote marks. You can use single quotes or double quotes, as long as they match.
@// Incorrect: The trial label "example-trial-one" does not have a closing
quote mark
%newTrial("example-trial-one,
% newX()
%)
@
@// Incorrect: The trial label "example-trial-two" does not have matching
quote marks
%newTrial('example-label",
% newX()
%)
@
@// Correct: The trial label "example-trial-three" has matching double quotes
~newTrial("example-trial-three",
~ newX()
~)
@
@// Correct: The trial label 'example-trial-four' has matching single quotes
~newTrial('example-trial-four',
~ newX()
~)
Comment syntax
PennController is a JavaScript module and follows the JavaScript comment style:
// This is a single-line comment
PennController.newTrial() // This is also a single-line comment
/*
This
is
a
multi-line
comment
*/
Style guidelines (optional)
PennController does not care about whitespace: all line breaks, tabs, and spaces are optional and purely for human readability.
The two scripts below are equivalent:
newTrial("example-trial",
newX()
.ELEMENT_COMMAND()
,
getX()
.ELEMENT_COMMAND()
)
newTrial("example-trial",newX().ELEMENT_COMMAND(),getX().ELEMENT_COMMAND())
Therefore, the following style guidelines are only suggestions. Feel free to adapt your scripts to your own style, and be aware that you may see scripts written in other styles.
Within a trial
- Place the opening parenthesis, trial label, and following comma of a
newTrial
command on the same line. Place the closing parenthesis on a new line.$newTrial("example-trial", $)
- Call every instance of a
newX()
orgetX()
function on an indented (4 spaces or 1 tab) new line. Except for the comma that follows the trial label argument, commas that separate trial arguments should be placed on an indented new line.@newTrial("example-trial", $ newX() $ , $ getX() @)
- Call an element command on an indented new line under the element it’s called on; or
@newTrial("example-trial", @ newX() $ .ELEMENT_COMMAND() $ .ELEMENT_COMMAND() @)
call an element command on the same line as the element it’s called on.
@newTrial("example-trial", $ newX().ELEMENT_COMMAND().ELEMENT_COMMAND() )
Global commands
Place every global command on an unindented new line, and separate global commands with an empty new line.
@newTrial("example-trial-one",
@ newX()
@)
$
@newTrial("example-trial-two",
@ newX()
@)
Special commands
We won’t learn about special commands until section 3. Commands, but here’s a preview: special commands are commands that are called within a trial but not on an element. Like a newX()
or getX()
function, call a special command on an indented new line.
@newTrial("example-trial",
@ newX()
@ ,
$ SPECIAL_COMMAND()
@ ,
@ getX()
@)
Trial labels
Trial labels are essential to control which trials should be part of your experiment, and in which order they should appear.
Trial labels are simple strings that may or may not be shared by multiple trials.
PennController provides multiple ways of specificying a trial’s label, the most common one consisting in passing it as the first argument of the newTrial
command
$newTrial("example-trial",
@ // ...
@)
Note that embedding the example above as is inside a Template
command will generate multiple trials that all share the label "example-trial"
:
+Template( row =>
$ newTrial("example-trial",
@ // ...
@ )
+)
Because cells from tables are strings, you can reference a column to provide a trial label. For example, if you have a column named Condition in your table that alternates between test and filler, the following will generate trials that will be labeled "test"
and trials that will be labeled "filler"
:
@Template( row =>
~ newTrial( row.Condition ,
@ // ...
@ )
@)
Another way of specifying a label is to use the label
command on the closing parenthesis of the trial:
@newTrial("example-trial",
@ // ...
@)
$.label("example-trial")
The label
command is most useful with commands that generate trials but do not accept a string argument to label the generated trial. For example, the command CheckPreloaded
generates a trial which, when run, will proceed only once the resources have preloaded, but you cannot give it a string argument to label the generated trial. The solution is to use the label
command:
@CheckPreloaded(
@ "test" ,
@ "filler"
@)
$.label("check-test-filler")
Once you have labeled your trials, you can control the order in which they will be executed using the command Sequence
.
Flow of evaluation
PennController scripts are evaluated and executed from top to bottom. As soon as one command is executed, the next command is immediately evaluated. When there are no more commands to evaluate and execute, the experiment sends its results to the server and ends.
Certain elements and certain commands can interact in a way that pauses experiment script evaluation. For example, you can create a button and use a command to pause the rest of the experiment until the participant clicks on the button.
If your experiment script doesn’t include any interactive elements and commands, PennController will straightforwardly evaluate and execute the script without ever giving the participant time to interact with the experiment, and the experiment will likely end within a few milliseconds. This is probably not what you want!