Tutorial: N-Arm Bandit Task
===========================
This tutorial will show how you can build a variable N-arm bandit task from scratch using Psychex. In particular, we'll demonstrate:
- How to setup Psychex
- How to load static content
- How to use the Psychex *NArmBandit* class
- How to extend Psychex classes
- How to listen for user clicks
- How to store and store user data
Setup
-----
If you haven't already, download Psychex from Github. We'll set up a standard directory structure for a vanilla web-project,
which will look like this:
::
Bandit_Tutorial_Project
|--- static/
|--- lib/
|--- lodash
|--- lodash.core.js
|--- p5
|--- p5.js
|--- psychex
|--- psychex.js
|--- js
|--- main.js
|--- index.html
HTML Setup
^^^^^^^^^^
We can start by populating our HTML entrypoint, `index.html`. ::
MyTestProject
Let's break down the contents of this file and look at what isn't boilerplate HTML.
First, we import our library files: ::
Next, we'll set use an inline style to block x and y-scrolling on our page. ::
<...>
We want the experiment to feel like a single page,
but if you want scrollable content, then feel free to remove this. Yo can also add `margin: 0; padding: 0;` to remove any whitespace between the canvas and page body.
Importantly, we define a div that will hold our canvas. ::
We've named it `gameCanvas` - you can name this whatever you want, but it will be referenced when we initialise Psychex in the next steps.
Finally, we import our entrypoint javascript: ::
Now we can set up our main Psychex file.
JS Setup
^^^^^^^^
Open `js/main.js` in your code editor of choice, and paste the following code in: ::
// Global settings
var assets = {"imgs" : {}, "fonts" : {}};
function preload(){
};
function setup(){
};
function draw() {
};
Let's go through each of these sections and explain what they do. ::
var assets = {"imgs" : {}, "fonts" : {}}
The first variable, `assets` is an object that will store static content for use later on. Psychex comes with specific methods for
loading assets (images, fonts, gifs) in a *blocking* way - which means it requires all static assets to be loaded before the game can start.
We'll demonstrate how to use this shortly.
After defining these parameters, we create 3 empty functions:
- `preload()` loads static content in advance.
- `setup()` contains code to be run once at the start of the experiment
- `draw()` contains renderable content that can be run many times per second.
Drawing text
------------
Let's start by displaying some header text. We'll create a global variable that can hold our renderable objects: ::
var assets = {"imgs" : {}, "fonts" : {}};
// -- Global variables -- //
var canvas;
var gameContent = {};
Before we can define anything, we need to create a canvas. Add the following to `setup()` ::
function setup(){
var canvas = createCanvas(windowWidth, windowHeight);
canvas.parent("gameCanvas");
}
`windowWidth` and `windowHeight` are global variables that store the available width and height of the browser window.
Note that the variables `width` and `height` are also available, and store the dimensions of the entire screen, not just the window.
The canvas variable references the `gameCanvas` div we made before in `main.html` - so make sure the IDs match. Now we can add our first text component.
Psychex renderable classes have a lower case `p` as a prefix. The text class is called `pText`. If you want more details about each of the classes described
here, check the individual pages within these docs.
`pText` is instatiated with 3 parameters: `text`, `x`, `y`: ::
function setup(){
var canvas = createCanvas(window.screen.width, window.screen.height);
canvas.parent("gameCanvas");
// -- New code -- //
gameContent.title = new pText("My Bandit Task", 50, 10)
}
Finally, we need to add this to the draw loop: ::
function draw(){
clear();
gameContent.title.draw();
}
All Psychex objects have a `draw` method that tells the main draw loop what they should look like. We've also added `clear()` at the start of the loop.
This removes all rendered content from the screen at the start of the function, before drawing it again. If we didn't do this, our content would be
overlayed ontop of itself 30 (or more) times per second, which can create some weird effects, especially if we add animations!
We may wish to change the design of the header text. To do this, we use an `aesthetics` object. This is a mapping of
style-keywords to their values that's passed to an object when it's instantiated. This is similar to ordinary HTML, where you
would attach CSS styles to an HTML object. To make our heading bigger and in bold typeface, edit `gameContent.title` as follows:::
gameContent.title = new pText("My Bandit Task", 50, 50, {textSize: 48, textStyle : "bold"});
You can check what the style keywords are within the :doc:`../tutorial/aesthetics` section of the docs.
The NArmBandit Class
--------------------
Psychex offers some base classes for building experiments. One of these is the `NArmBandit` class. Psychex classes are designed to be extended
so that custom functionality can be built, while having commonly used base components avoid effort duplication.
We'll start by defining our own class, and extending the base class: ::
class BanditTask extends NArmBandit {
constructor(x, y, nArms, probs){
super(x, y, nArms, probs);
}
}
`NArmBandit` expects 4 parameters: *x*, *y*, *nArms* and *probs*. The first two are coordinates, which can be useful as an anchor point for adding renderables
later (such as images, as we'll see shortly). If you don't need these, simply use 0, 0 as inputs. nArms describes the number of arms in the task, and probs
is an array of probabilities for each arm. See :doc:`../code_docs/paradigms` for details on the class.
We can immediately use this created class for a bandit task. For instance the parent method `pullArm` will pull a specific arm and return a *boolean*
based on the assigned probabilities, e.g.: ::
gameContent.myBanditTask = new BanditTask(0, 0, 2, [0.5, 0.5])
let banditResult = gameContent.myBanditTask.pullArm(1);
console.log(banditResult);
Which will either print out `true` or `false`.
Let's build upon our class by adding some visuals. We'll add 2 slot machine images to represent the 2 arms in our task. First, let's load the slot machine img: ::
preload() {
assets.imgs.slotMachine = loadImage("https://raw.githubusercontent.com/agrogan97/psychex/dev/docs/build/html/_static/slotMachine.png")
}
To load this image, we're using the function `loadImage()`. This preloads the image, caching it before the experiment starts.
Doing this ensures all your stimuli are loaded and ready before the participant can begin playing. See :doc:`../tutorial/aesthetics`.
Now we'll add copies of this image to our new class: ::
constructor(x, y, nArms, probs){
super(x, y, nArms, probs);
// -- New code -- //
this.slotMachines = []
for (let i=0; i {
sm.draw();
})
}
Let's instantiate the class and render our images! If you didn't previously, create a reference within `gameContent`, which we made earlier: ::
gameContent.myBanditTask = new BanditTask(0, 0, 2, [50, 50]);
and add call its draw method inside the draw loop: ::
function draw(){
clear();
gameContent.title.draw();
// -- Draw bandit task here -- //
gameContent.myBanditTask.draw();
}
Refresh the page, and behold your slot machines. Now, we want a player to be able to choose a slot machine to sample from. To do this, we'll use a *click listener*.
Click Listeners
---------------
All *Psychex* elements have click listeners attached to them by default, but they need to be turned on before they can be used.
Let's turn them on: ::
// Define an array to store slot machine referencesq
this.slotMachines = [];
for (let i=0; i {
sm.toggleClickable();
})
You can double-check that an object is clickable in a console by typing `