PEBL Experiment Tutorial
How to write an experiment in PEBL
Shane T. Mueller, Ph.D.
ABSTRACT
This document walks a beginner step-by-step through creating a simple experiment with PEBL.
INTRODUCTION
First, you must install PEBL on your computer. Precompiled versions are available for Windows and OSX, and the source code is available for compiling and using on Linux. Assuming you have compiled and/or installed PEBL on your computer, there are a few basic things you should know:

- PEBL is a compiler/interpreter. It reads a text file full of commands that define an experiment (often referred to as the experiment script, or experiment program). When PEBL is launched, it reads the text program into an intermediate node tree by through a process called a parser. Once parsed, it executes the operations in this tree. This translation to a new form is called compiling, which makes PEBL a compiler. It does not compile to machine code, or perform any optimization steps, but it intermediate code structure makes the execution very efficient.
- PEBL is designed to involve a minimum of knowledge in order to design an experiment. It eschews many powerful and complex computing paradigms in favor of being simple enough for a psychology student to use.
- There are, however, a few things you must learn to create an experiment.

BASICS
A basic PEBL script file requires just a few things. First, you need a primary function which the experiment starts with. This must be called Start.
Functions are defined using the define keyword:
`
define Start(parameter)
{
}`
This defines a basic function. All functions must start with a Capital letter, and may contain a list of parameters that need to be called when the function is executed. Start is unique in that it requires a single parameter, which gets bound to a list of command-line arguments passed in. Note that the variable parameter starts with a lowercase letter: all variables begin with lowercase letters. After the first letter in a function or variable, PEBL is case insensitive, meaning that parameter and paraMeter refer to the same thing.
Functions are interesting because they execute a set of PEBL operations when called. These operations go in between the `{` and the `}`.
One of the simplest things you might want to do is to compute some mathematical expression. PEBL uses fairly typical math expression symbols, and shouldn't be difficult to figure out. For example:

    define
    Start(parameter)
    {
      (33 + 2425)/ 325
    }

Computes the value of a complicated expression. You can run this program, but nothing will happen. PEBL will compute the expression on the first line, and then exit when it comes to the end of the program. The simplest way to display the results is to use the Print() function. Print writes a value to a special stream called 'standard out'. On Linux and Mac, this gets displayed directly on the command line. On windows, it gets sent to a file called stdout.txt that will appear in the working directory when a script is run (usually the directory that the launcher starts in). So:

    define
    Start(parameter)
    {
      Print((33 + 2425)/ 325)
    }

Remember, Print is a function, and so must start with a capital letter. This will print out the value '`7.56308`'.
MULTIPLE COMMANDS
What if you want your program to do more than one thing? Well, just follow the first command with another one on a new line. They will be executed in sequence. There is no need for a line-end character--PEBL automatically ends a complete statement when the line ends. When a new line happens in the middle of an incomplete statement, PEBL ignores the line break. However, if you want to put multiple statements on a line, you can use the `;` character as a virtual line break.

    define
    Start(parameter)
    {
      Print((33 + 2425)/
       325)
        Print("Hello World"); Print("Goodbye World")
    }

FUNCTIONS
As discussed earlier, you can define and call your own functions, as well as the ones pre-defined by PEBL. In fact, many of the functions available in PEBL are written in PEBL itself. Functions can be placed in the same text file you put your Start function, or can be placed in other files, and loaded by the interpreter. To review, a function is defined with the define keyword, and must start with an uppercase letter. But, functions can return values, using the return keyword:

    define
    Start(parameter)
    {
      Print(Average(3526,3))
    }

    define Average(a, b)
    {
      return (a + b) / 2
    }

This prints out 1764.5
VARIABLES
Often, you want to save the results of a command for use later. This can be done by assigning the result to a variable. There are two types of variables: local and global. Local variables are available only in the current function, and disappear afterward. Global variables are available to all functions. Every variable beginning with a '`g`' is treated as a global variable, and all others are local. To assign a value to a variable, use the two-character assignment symbol: '`<-`'

    define
    Start(parameter)
    {
        gMessage <- "Global Text"
        mu <- Average(3526, 3)
        Print(mu)
    }

    define Average(a, b)
    {
        Print(gMessage)
        return (a + b) / 2
    }

This prints out:
`Global Text
1764.5`
LISTS
So far, we have seen how to deal with a single data object, like a number or a text string. PEBL relies on a lists to represent sequences of data. The primary aspects of a list are that it is very cost-efficient to add things to and manipulate. You do not need to know the length of the list before you create it--you can simply add all the items you need to it. The drawback is that it is inefficient to access arbitrary indexes in the middle of the list--you need to walk through the whole list to get to it. In the future, other sequences will be available, but so far, lists are quite sufficient for nearly all experimental tasks.
Lists are defined with the `[]` brackets:
`data <- [23454, 21352, 253213]
`
Anything can appear in a list, even variables and things of different types:
`data2 <- [data, "second item", 33526.6]
`
List items can be individually accessed with functions `First()`, `Last()`, and `Nth()`. They can be shuffled and used to design experiments with a large range of other functions. To access each item in a list iteratively and efficiently, use the loop construct.

    define
    Start(parameter)
    {
        list <- Shuffle(["A", "B", "C", "D", "E"])
        loop(i, list)
        {
          Print(i)
        }
    }

Which produces output like:
`
D
A
E
C
B`
DESIGNING AN EXPERIMENT
These are nice demonstrations, but they don't appear to have a lot to do with creating an experiment. How does one go about that?
PEBL has a well-developed and easy-to-use set of objects that can be created and manipulated with function, and together with the above basics, can be used to build experiments. A full reference of the functions is available in the PEBL manual.
The first important object necessary is a window. Creating a window allows you to display graphical objects, and can be set to be fullscreen (for final experiments) or in a window (for design). Windows are created with the function `MakeWindow()`. Windows have a base background color, and this can be set in the function: `MakeWindow("black")` creates a window with a black background. A blank window is not of much use, however--you need things to display on it. The function `MakeLabel()` creates a one-line text object which would be a good stimulus, but it needs to font to be defined, using the `MakeFont()` function. Similarly, MakeFont needs to know what the foreground and background colors need to be, which are defined with the `MakeColor()` function.
Once an graphical object like a label is created, it must be added to the window, and should be moved to an interesting place. In order for a set of drawing commands to show up on the screen, you must issue the `Draw()` function which does the final rendering. Finally, if you want to look at what is happening, you might want to wait a while, which can be done with the `Wait()` function.
So, an annoted script for all these things is:

    define Start (par)
    {

        ## Colors can be defined by color names.  There are hundreds of valid
        ## color names.  They may also be defined by RGB colors.
        red <- MakeColor("red")
        bg <- MakeColor("grey")

        #Fonts are defined by a series of arguments:
        # Vera.ttf is a font that comes with PEBL.
        # 0 is the style, meaning plain
        # 22 is the font size, in points.
        # red is the foreground color
        #bg is the background color, which should match the window.
        # 1 tells PEBL to render this text anti-aliased
       font <- MakeFont("Vera.ttf",0,22,red, bg,1)
       label <- MakeLabel("My First Label", font)
       gWin <- MakeWindow("grey")

       #To display a label, it must be added to the window.
       AddObject(label,gWin)
       ##Move it to an interesting place
       Move(label, 250,100)

        ##Draw everything
        Draw()

        ##Wait 2 seconds to admire our work
        Wait(2000)
    }

This program produces something like this:
and stays up for 2 seconds before disappearing. Now this is getting closer to an experiment!
THE TASK
Suppose we want to create a simple experiment where some text is displayed on the screen, and the subject must decide whether the text is a word or not (This is called the lexical decision task). For this, we need to define a list of words and a list of nonwords, create a function that displays the stimuli and collect the response, and then records the data.
TRIALS, BLOCKS, DESIGN
To begin, lets assume that the function `Trial()` defines everything that needs to be done during a trial. All it needs to know is what stimulus should be presented, and whether it is a word or nonword. Then, all we need is a `Start()` function that sets up the display device, defines the stimuli, and calls trial for each stimulus.

    define Start(par)
    {

    #############################
    ## First, set up the basic  display stuff.
    #############################
        red <- MakeColor("red")
        bg <- MakeColor("grey")

       font <- MakeFont("Vera.ttf",0,22,red, bg,0)
       #define gLabel and gWin global so that Trial can use them.
       gLabel <- MakeLabel("+", font)
       gWin <- MakeWindow("grey")

        AddObject(gLabel, gWin)
        Move(gLabel,300,200)

    #############################
    ## Now, create the stimuli
    #############################
      words <- ["every","good","boy","does","fine",
                       "esery","rood","loy","noes","kine"]
       cond <- [1,1,1,1,1,2,2,2,2,2]

       ##combine them into a single list with sublists containing a word and a condition
       stim <-Transpose([words,cond])

       ## Shuffle the stimuli into a random order
       stim <- Shuffle(stim)

       ## Go through the stimuli one-by-one
        loop(s,stim)
        {
            Trial(First(s), Last(s))
         }
    }

    define Trial(stimulus, condition)
    {
      #To be defined later.
      Print(stimulus + " " + condition)
    }

Running this script will produce something like this:
`every 1
good 1
rood 2
fine 1
does 1
kine 2
esery 2
loy 2
noes 2
boy 1
`
Now, we need to define Trial to do the task. Since `gLabel` already exists and has been added to `gWin`, all we need to do is change the text of `gLabel`.

    define Trial(stimulus, condition)
    {
       SetText(gLabel,stimulus)
       Draw()
    }

This will display each of the ten stimuli very briefly (probably just a single refresh cycle) and then finish. So far, so good. We need a way to allow the participant to generate a response. We can use the function `WaitForListKeyPress()` for that. `WaitForListKeyPress()` allows you to specify a list of 'legal' keypresses, and waits until one of those keys is pressed before returning. It returns the value of the key pressed. The following modification:

    define Trial(stimulus, condition)
    {
       SetText(gLabel,stimulus)
      Draw()
      response <- WaitForListKeyPress(["Y","N"])
      Print(stimulus + " " + condition + " " + response )
    }

Produces output like this:
`
esery 2 y
rood 2 n
every 1 y
kine 2 n
fine 1 y
does 1 y
boy 1 y
good 1 y
noes 2 n
loy 2 n
`
This is useful, because it is close to what we want to save as data. Now, how about the response times? Use `GetTime()` to find out when things happen.

    define Trial(stimulus, condition)
    {
      SetText(gLabel,stimulus)
      Draw()
      start <- GetTime()
      response <- WaitForListKeyPress(["Y","N"])
      end <- GetTime()
      rt <- end - start
      Print(stimulus + " " + condition + " " + response + " " + rt )
    }

This is nice, but suppose we have a subject number we want saved in the data file, and a trial number? We can pass these into the Trial function, or just write this stuff out in the outer loop:

    define Start(par)
    {
      ## Execute pebl with the -v command-line to specify subject number
      ## If no -v option produced, the code below sets subnum arbitrarily to 0
      if(IsList(par))
       {
          subnum <- First(par)
       } else {

        subnum <- 0
       }

    #############################
    ## First, set up the basic  display stuff.
    #############################
        red <- MakeColor("red")
        bg <- MakeColor("grey")

       font <- MakeFont("Vera.ttf",0,22,red, bg,0)
       #define gLabel and gWin global so that Trial can use them.
       gLabel <- MakeLabel("+", font)
       gWin <- MakeWindow("grey")

        AddObject(gLabel, gWin)
        Move(gLabel,300,200)

    #############################
    ## Now, create the stimuli
    #############################
      words <- ["every","good","boy","does","fine",

    "esery","rood","loy","noes","kine"]
       cond <- [1,1,1,1,1,2,2,2,2,2]

       ##combine them into a single list with sublists containing a word and a condition
       stim <-Transpose([words,cond])

       ## Shuffle the stimuli into a random order
       stim <- Shuffle(stim)

        trial <- 1
       ## Go through the stimuli one-by-one
        loop(s,stim)
        {
            Print_(subnum + " " + trial + " ")
            Trial(First(s), Last(s))
            trial <- trial + 1
         }
    }

This produces output like:
`0 1 boy 1 y 1401
0 2 fine 1 n 848
0 3 good 1 n 392
0 4 rood 2 y 293
0 5 does 1 n 272
0 6 esery 2 y 166
0 7 loy 2 n 237
0 8 kine 2 y 359
0 9 noes 2 n 307
0 10 every 1 y 387
`
Finally, suppose we want to save this text into a file, rather than just outputting it. We can open a file using the `FileOpen()` command, and write to it using the `FilePrint()` and `FilePrint_()` commands. I've also added a few other tweaks to make it a nicer experiment.

    define Start(par)
    {
      ## Execute pebl with the -v command-line to specify subject number
      ## If no -v option produced, the code below sets subnum arbitrarily to 0
      if(IsList(par))
       {
          subnum <- First(par)
       } else {

        subnum <- 0
       }

    #############################
    ## First, set up the basic  display stuff.
    #############################
        red <- MakeColor("red")
        bg <- MakeColor("grey")

       font <- MakeFont("Vera.ttf",0,22,red, bg,0)
       #define gLabel and gWin global so that Trial can use them.
       gLabel <- MakeLabel("Press 'y' or 'n' keys for words and nonwords", font)
       gWin <- MakeWindow("grey")

        AddObject(gLabel, gWin)
        Move(gLabel,300,200)
        Draw()
        Wait(2000)

        gFileOut <- FileOpenWrite("data.txt")

    #############################
    ## Now, create the stimuli
    #############################
      words <- ["every","good","boy","does","fine",
                "esery","rood","loy","noes","kine"]
       cond <- [1,1,1,1,1,2,2,2,2,2]

       ##combine them into a single list with sublists containing a word and a condition
       stim <-Transpose([words,cond])

       ## Shuffle the stimuli into a random order
       stim <- Shuffle(stim)

        trial <- 1
       ## Go through the stimuli one-by-one
        loop(s,stim)
        {
            FilePrint_(gFileOut,subnum + " " + trial + " ")
            Trial(First(s), Last(s))
            trial <- trial + 1
         }
      FileClose(gFileOut)
      ##Thank them
      SetText(gLabel, "Thanks!  Press any key to finish.")
      Draw()
      WaitForAnyKeyPress()

    }

    define Trial(stimulus, condition)
    {
      ##At the beginning of the trial, make a fixation cross
      SetText(gLabel, "+")
      Draw()

      ##wait randomly between  500 and 800 ms
      Wait(RandomUniform(300) + 500)

      SetText(gLabel,stimulus)
      Draw()
      start <- GetTime()
      response <- WaitForListKeyPress(["Y","N"])
      end <- GetTime()
      rt <- end - start
      FilePrint(gFileOut,stimulus + " " + condition + " " + response + " " + rt )
    }

This will save the data into a file called 'data.txt'.
SUMMARY
This document has walked you through the basics of creating a simple experiment in PEBL. Many more complex functions exist, but if you understand what was done here, you should be able to make sense of them from their description in the manual.
Without even consulting the manual, you should be able to modify the script to:

- Use black and white is used instead of grey and red
- Change the position of the stimulus on each trial
- Change the keys used to make responses
- Display the text in a different color on each trial
- Make this a Stroop task, with stimuli named red and green, either in consistent or inconsistent colors
- Compute and report mean accuracy and mean response times to participant during debriefing.
- Give correct/incorrect feedback
- Present both the word and non-word version of each word simultaneously and require a forced-choice response.

More advanced modifications that can be done by consulting the manual might include:

- Repeat incorrect trials at end.
- Repeat the stimuli arbitrary some number of times, specified via the command line.
- Specify the stimuli in a text file that gets read in by `FileReadTable()`
- Display detailed instructions and debriefing using `MakeTextBox()`

`
`