The most basic PennController experiment script contains a single line:
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.
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"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
$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.
PennController global commands are commands that used outside of a trial. By default, all global commands begin with the prefix
PennController.newTrial is a global command that creates a trial. In addition, the
getX() functions have the prefix
The global command
PennController.ResetPrefix(null) removes the
PennController.(Elements.) prefix for all subsequent calls of a global command and instances of a
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.
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!
The following special symbols are obligatory, and omitting one or more will cause an error.
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
@// 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() ~)
When calling an element command, the command name must be preceded by a period. For example, to call the
@// 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() %)
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() ~)
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() ~)
// This is a single-line comment PennController.newTrial() // This is also a single-line comment /* This is a multi-line comment */
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() )
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.
- Place the opening parenthesis, trial label, and following comma of a
newTrialcommand on the same line. Place the closing parenthesis on a new line.
- Call every instance of a
getX()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() )
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() @)
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
getX() function, call a special command on an indented new line.
@newTrial("example-trial", @ newX() @ , $ SPECIAL_COMMAND() @ , @ getX() @)
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("example-trial", @ // ... @)
Note that embedding the example above as is inside a
Template command will generate multiple trials that all share the label
+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
@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")
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
@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
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!