Ontology

Elements

The basic units of a PennController trial are elements: every command relates to an element.

There are no clear-cut types of elements: one element can have visual content (e.g., Text, Image elements) or audio content (e.g., Audio elements), it can stand as an interface to collect responses (e.g., Key elements). More often than not, an element shares several of those properties: for instance, Scale elements both show buttons and collect scores, and VoiceRecorder elements show buttons to record audio and play back the recordings to be collected.

There are two ways to manipulate elements: either by creating a new element, or by referring back to an existing element. In this documentation, you will see scripts with blocks of lines separated by commas: the first lines of the blocks always start with new or get. All the lines below the first one that start with . relate to the element in the first line. To give an illustration, consider the bit of script below: its first line creates an Audio element, its second line starts playing it, and its third line tells the script to wait until the end of the playback before proceeding (more about this point later).

newAudio("myAudio", "test.mp3")
    .play()
    .wait()

Commands

In the code above, play and wait are commands that relate to the newly created element named myAudio. As just mentioned, commands start with . and refer to an element (with Clear command exceptions as of PennConroller beta 0.2). The list of commands for a given element are specific to its category. For instance, there is no command wait for Text elements. There is a command wait both for Audio and for Key elements, though its meaning is element-dependent: in the case of Audio elements it means “wait until the audio has ended,” whereas in the case of Key elements it means “wait until the key has been pressed.” That being said, you can find a list of commands shared by all elements on the page Standard Element Commands (note that some of those commands simply have no effect on some elements—e.g., aesthetic commands have no effect on Key elements).

Flow of evaluation and execution

To design your PennController trials, you write scripts that consist of series of blocks of commands like the one given above as an illustration, separated by commas and contained within the parentheses of PennController( ). When a trial is run, it starts by reading the very first commands of the very first block. Let us walk through a basic example:

newButton("myButton", "Click")
    .print()
,
newAudio("myAudio", "test.mp3")
    .play()
    .wait()
    .print()
,
getButton("myButton")
    .wait()

The script starts by reading the first block, which itself starts with newButton("myButton", "Click"): the script creates a Button element named myButton, which will read Click when displayed. This block will be about this Button element. Immediately after creating the element, the script reads what comes next, .print(), and therefore adds the button to the screen. So far, the screen only contains a button that says Click. Immediately after adding the button to the screen, the script encounters a comma, and it starts reading the second block. Note that the script keeps on reading: it does not wait for a click on the button, as it was not commanded to do so. It now reads newAudio("myAudio", "test.mp3") and therefore creates a new Audio element named myAudio, using the file test.mp3: this block will be about this Audio element. Immediately after creating the element, the script reads what comes next, .play(), and therefore starts playing the audio. Immediately after starting playing the audio, the script reads .wait() and therefore knows that it has to wait until the audio has ended playing before continuing reading. While the audio is playing, the Click button is still displayed on the screen, but clicks have no effect (this is just a button that says Click, nothing more). Then, after the audio has ended playing (and regardless of whether the button was clicked), the script continues and reads .print(), which means (in the context of an Audio element) it should add buttons to the page to provide control over the playback of the audio. So far, the script has already added a button to the page, so the controls appear below that button. Immediately after adding those controls, the script encounters a comma and starts reading the third block, which starts with getButton("myButton"): the third block will be about the Button element that was created in the first block. The script immediately reads .wait() and therefore knows that is has to wait until the button is clicked. So far, the page displays a button that says Click, above controls for the audio. Until the Click button is clicked, the controls can be used to replay the audio, pause it, change its volume, …: the script is on hold until the Click button is clicked. Immediately after the Click button is clicked, the script resumes and reaches the end: this is the end of the trial, the page is refreshed and the experiment proceeds to the next screen.

PLEASE NOTE that this walk-through did not talk about lines or about spaces, for the simple reason that you could remove all the linebreaks and all the spaces from the script above and it would still work the same: it only cares about . and , to determine what bits of text to evaluate (and, to some extent, ( and )). The linebreaks and spaces are there for us humans: without them, the script becomes much less readable (to us). By convention, the scripts in this documentation follow spacing and linebreak conventions thought to make things clearer, but do not be surprised to find scripts that do not follow the same conventions, and feel free to adapt your scripts to your own style.

Types of commands

Commands are divided in three types, and you will accordingly find them listed in types in this documentation.

Actions

Action commands are usually active verbs, like play or wait above. Actions usually do at least one of three things: they add visual content to page (e.g., print), they trigger an event (e.g., play) or they pause the script (wait). Other commands can have similar effects but are still better conceived of as settings rather than actions (see below).

Actions tend to be element-specific and to have element-depdendent meanings (though see Standard Element Commands). They also tend to correspond to what you would intuitively use the elements for: printing some text, playing audios, starting a timer

Settings

Setting commands are always preceded with .settings, even when they come after another setting command in the block:

newText("myText", "Hello world.")
    .settings.color("green")
    .settings.bold()
    .settings.italic()
    .print()

In the code above, you cannot replace .settings.bold() with simply .bold() or .settings.italic() with simply .italic() (even if you cleverly add spaces/tabs to horizontally align them with .color() in the second line—see PLEASE NOTE at the end of the section Flow of evaluation and execution).

Settings are usually not active verbs and can be participles, adjectives or nouns (though there are some exceptions). They tend to affect attributes or properties of elements, such as aesthetics (as illustrated above) but they can also act on future behaviors of the elements: for instance, using .settings.once() on a Button element will tell it that it should become disabled after (if) it is clicked. Another way of thinking about this is in terms of what happens when, or in terms of listening to events.

Even though settings tend to not have the punctuality of actions, they still are evaluated and executed in sequence (see the section Flow of evaluation and execution above). In the code below, the text of the first button does not appear boldfaced until it is clicked, because wait is executed before .settings.bold is evaluated. By contrast, the text of the second button appears boldfaced from the moment it is displayed on, because it is evaluated and executed before print gets evaluated.

newButton("first", "Click me!")
    .print()
    .wait()
    .settings.bold()
,
newButton("second", "Button 2")
    .settings.bold()
    .print()
    .wait()

Tests

Test commands are always preceded with .test and are of a very different nature from actions and settings. They can mostly be used in two contexts: as a new block to define conditional branching, or within certain commands (such as wait) to define conditions of success.

You can always associate a block of command(s) to be evaluated and executed upon success of the test, and a block of command(s) to be evaluated and executed upon failure of the test.

newAudio("yarrel", "test.mp3")
    .play()
    .wait()
,
newTextInput("heard", "")
    .settings.before( newText("left", "It said ") )
    .print()
    .wait()
,
getTextInput("heard")
    .test.text( "laurel" )
    .success(
        newText("laurel", "Yes, you are right, any other response would be wrong.")
            .print()
    )
    .failure(
        newText("yanny", "You should pay more attention next time...")
            .print()
    )

This script plays the audio file test.mp3 and, after it is done playing it, invites the participant to enter what they heard. If what they have typed in the input box when they press enter/return is laurel, what is inside the parentheses of success is evaluated and executed: a text appears below the input box saying Yes, your are right, any other response would be wrong. If the content of the input box is different from laurel, what is inside the parentheses of failure is evaluated and executed: a text appears below the input box saying You should pay more attention next time.

Note that tests do not pause the flow of evaluation and execution. That is, if you removed the wait command from the second block above, the command .test.text would then be evaluated and executed immediately after the input is added to the page, and therefore the content of success would never be evaluated and executed, because the text of the input box is empty (and so, different from laurel) when it is added to the page. Instead, the content of failure would automatically be evaluated and executed.

To know what happens when you use a test command within another command, read the documentation about that other command (only wait commands accept tests as of PennController 1.1).

testNot, and, or

Test commands in PennController are compositional to a certain extent. You can express the negation of any test by replacing test by testNot. The code below, where we replaced test with testNot and switched success and failure, has exactly the same effect as the code above:

newAudio("yarrel", "test.mp3")
    .play()
    .wait()
,
newTextInput("heard", "")
    .settings.before( newText("left", "It said ") )
    .print()
    .wait()
,
getTextInput("heard")
    .testNot.text( "laurel" )
    .success(
        newText("yanny", "You should pay more attention next time...")
            .print()
    )
    .failure(
        newText("laurel", "Yes, you are right, any other response would be wrong.")
            .print()
    )

Since PennController 1.1, you can test whether several tests are successful using the special keyword and, for instance:

newAudio("yarrel", "test.mp3")
    .play()
    .wait()
,
newTextInput("heard", "")
    .settings.before( newText("left", "It said ") )
    .print()
    .wait()
,
getTextInput("heard")
    .testNot.text( "laurel" )
    .and( getTextInput("heard").testNot.text( "yanny" ) )
    .success(                                              // Success means "neither laurel nor yanny"
        newText("other", "You follow the Middle Path.")
            .print()
    )
    .failure(                                              // Failure means "either laurel or yanny"
        newText("laurelOrYanny", "You come from a bimodal distribution.")
            .print()
    )

Besides and, PennController 1.1 introduces the keyword or. The code above can therefore be reformulated as:

newAudio("yarrel", "test.mp3")
    .play()
    .wait()
,
newTextInput("heard", "")
    .settings.before( newText("left", "It said ") )
    .print()
    .wait()
,
getTextInput("heard")
    .test.text( "laurel" )
    .or( getTextInput("heard").test.text( "yanny" ) )
    .success(                                              // Success means "either laurel or yanny"
        newText("laurelOrYanny", "You come from a bimodal distribution.")
            .print()
    )
    .failure(                                              // Failure means "neither laurel nor yanny"
        newText("other", "You follow the Middle Path.")
            .print()
    )

Note that series of and and or‘s are evaluated linearly. As a result, a test of the form A and B or C and D will be successful only if D is successful, and either C or A and B is successful, i.e. ( (A and B) or C ) and D. If you want to express (A and B) or (C and D) you should call the last and on C itself:

newAudio("yarrel", "test.mp3")
    .print()
,
newTextInput("heard", "")
    .settings.before( newText("left", "It said ") )
    .print()
    .wait()
,
getAudio("yarrel")
    .test.hasPlayed()
    .and( getTextInput("heard").testNot.text("") )
    .or ( 
        getAudio("yarrel")
            .testNot.hasPlayed()
            .and( getTextInput("heard").test.text("") )
    )
    .success(                                              // (Played & input) or (neither played nor input)
        newText("consistent", "You are a reasonable person.")
            .print()
    )
    .failure(                                              // Played but no input, or input bot not played
        newText("inconsistent", "You missed one step.")
            .print()
    )

The code above prints “You are a reasonable person.” after a press on return in the text input box in two conditions: audio has not been played back and the box is empty, or the audio has been played and the input box is not empty. In all other conditions, it prints “You missed one step.”

Defaults

This page starts with a lie: some commands actually do not relate to an element. First, there are two special cases. But what is more, there are default commands. Say your trial contains lots of italic text lines: you need to create as many Text elements (though you may also consider using an Html element). But it could quickly feel repetitive to call print on every single Text element that you create. To spare you such unnecessary efforts, you can use defaults, like this:

defaultText
    .settings.italic()
    .print()

Now whenever you add a line starting with newText to your script, it is as if you actually added two lines right below it: a first line saying .settings.italic() and a second line saying .print().

As another illustration, note that this:

defaultButton
    .print()
    .wait()
,
newButton("first", "Button 1")
    .remove()
,
newButton("second", "Button 2")

is equivalent to:

newButton("first", "Button 1")
    .print()
    .wait()
    .remove()
,
newButton("second", "Button 2")
    .print()
    .wait()

You use blocks starting with default the same way that you would use blocks starting with new or get. Elements created before a default is encountered are not affected by it (see the section Flow of evaluation and execution above to understand why).