Lesson 8 - Line by Line Code
Like coding in R, the beginning section of the code is importing essential functions. These are all things that are useful or essential to running the monkey programs. Things like “time” and “random” allow us to use certain functions within the programs. Then we also have an initialization statement which tells the program, “Hey we need to use pygame, boot it up!”
import sys # Import the 'system' library
import random # Import the 'random' library which gives cool functions for randomizing numbers
from random import choice
import math # Import the 'math' library for more advanced math operations
import time # Import the 'time' library for functions of keeping track of time (ITIs, IBIs etc.)
import datetime
import os # Import the operating system (OS)
import glob # Import the glob function
import pygame # Import Pygame to have access to all those cool functions
import Matts_Toolbox # Import Matt's Toolbox with LRC specific functions
pygame.init() # This initializes all pygame modules
Read files and Set up Variables
In the next part, we start giving the program some local variables. For example, we want the program to read the name in “monkey.txt” and store that name as a variable called “monkey”. Now, whenever we need to know which monkey this is, we can simply reference the variable “monkey”
We also set up local variables like colors, the names of sounds, the screen size (800×600), and the frames per second of the program (which is typically 60 for the monkey programs). The purpose of all this set up is to allow for shortcuts later on in the coding process. Instead of creating a red cursor by passing it (250, 0, 0), we can simply tell it to be “red”.
# Read the monkey name from monkey.txt which should be located inside your program's folder
with open("monkey.txt") as f:
monkey = f.read()
# Set Current Date
today = time.strftime('%Y-%m-%d')
"""SET UP LOCAL VARIABLES ---------------------------------------------------------------------------------------------"""
white = (255, 255, 255) # This sets up colors you might need
blue = (0, 191, 255)
black = (0, 0, 0) # Format is (Red, Green, Blue, Alpha)
green = (0, 200, 0) # 0 is the minimum & 260 is the maximum
red = (250, 0, 0) # Alpha is the transparency of a color
transparent = (0, 0, 0, 0)
"""Put your sounds here"""
sound_chime = pygame.mixer.Sound("chime.wav") # This sets your trial initiation sound
sound_correct = pygame.mixer.Sound("correct.wav") # This sets your correct pellet dispensing sound
sound_incorrect = pygame.mixer.Sound("Incorrect.wav") # This sets your incorrect sound
"""Put your Screen Parameters here"""
scrSize = (800, 600) # Standard Resolution of Monkey Computers is 800 x 600. This changes for the joint computers.
scrRect = pygame.Rect((0, 0), scrSize) # Sets the shape of the screen to be a rectangle
fps = 60 # Frames Per Second. Typically = 60. Changing this changes the cursor speed.
Import all the functions from Matts _Toolbox.py
The next chunk of code is simply importing the functions we went over in Lesson 6. Use the following syntax:
If you don’t import them, your program will not be able to use them.
from Matts_Toolbox import pellet
Create the ICON Class through INHERITANCE
I am not going to dive too deep into the concept of inheritance, but just know it is a mechanism that allows a class to inherit properties and behaviors from another class. In this case, we will be creating a class called “ICON” that will inherit all the characteristics of the “BOX” class from Matts_Toolbox.py. This ICON class is going to be used to create ICON objects on the screen for the monkeys to interact with.
from Matts_Toolbox import Box
class Icon(Box):
def __init__(self, PNG, position, scale):
super(Icon, self).__init__()
image = pygame.image.load(PNG).convert_alpha() # image = image you passed in arguments
self.size = image.get_size() # Get the size of the image
self.image = pygame.transform.smoothscale(image, scale) # Scale the image = scale inputted
self.rect = self.image.get_rect() # Get rectangle around the image
self.rect.center = self.position = position # Set rectangle and center at position
self.mask = pygame.mask.from_surface(self.image) # Creates a mask object
"""Objects of the Icon class have this .mv2pos function"""
"""This function allows you to move the Icon anywhere on the screen"""
def mv2pos(self, position):
self.rect = self.image.get_rect()
self.rect.center = self.position = position
To create an object of the “Icon” class, we need the following: the PNG file of the icon, its (x,y) position on the screen, and its size (AKA scale). We know this by the following code:
class Icon(Box):
def __init__(self, PNG, position, scale):
super(Icon, self).__init__()
Keep in mind, we can always change their position and scale, but they must be defined when the icon object is first created.
Create the Trial Class
Next we are going to create a “Trial” Class. Think of a trial class an abstract representation of each trial the monkey will complete. Every trial needs all of its characteristics defined. For example, what is this trial’s number? What block is this trial in? What type of trial is it? etc. You will ALSO need to keep track of where the monkey is WITHIN each trial. I do this by tracking the monkey’s “phase”. For example, startphase is usually the beginning of the trial where there is just a cursor and a start button
-Phase 1 might be the presentation of the target
-Phase 2 might be the presentation of the monkey’s options, and so on…
Your trials can have as many phases as you want, and the phases can differ by the type of trial. So if you wanted to test how good a monkeys MTS is after completing 100 different tests, you could do it. I wouldn’t… but you could… You can ALSO keep track of what happens inside of each trial. For example, did the monkey press the hint button? I call these “events”.
class Trial(object):
def __init__(self):
super(Trial, self).__init__()
self.train_or_test = train_or_test # Determine if this is a Training or a Testing Condition by pulling the value of train_or_test from parameters.txt
self.trial_number = 0 # Trial Number {1 - x}
self.trial_within_block = -1 # Trial Within the current block {0 - x}
self.block = 1 # Block number {1 - x}
self.block_length = trials_per_block # Number of trials per block = stored in parameters.txt
self.blocks_per_session = blocks_per_session # Number of blocks per session = stored in parameters.txt
self.start_time = 0
# Keep Track of Phases
self.startphase = True # Start button
self.phase1 = False # Phase 1: Stimuli flashes for 5 sec
self.phase2 = False # Phase 2: Blank Screen
self.phase3 = False # Phase 3: Display Stimuli
# Keep Track of events that occur inside the program
self.event1 = False
self.event2 = False
self.event3 = False
I also like to set up other variables, like the program’s stimuli (if you have a lot of stimuli), the trial types, the icon’s (x,y) positions on the screen etc. Here are some examples of additional things to set up:
Make a list of PNGs pulled from the stimuli folder
self.pngs = glob.glob('stimuli/*.png')
self.stimuli_idx = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
random.shuffle(self.stimuli_idx)
self.pngs = glob.glob takes all the files in your stimuli folder and creates a list of them.
Make an array of your trial types
self.trial_type = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
In this program, we only have on type of trial, but here you could label different types of trials with different numbers.
Give the program the standard (x,y) positions of the icons
# If you want 4 options on the screen at once:
self.icon_position = [(150,150), (650,450), (150, 450), (650, 150)]
# If you want only 2 options on left & right sides:
self.icon_position = [(125, 300), (675, 300)]
This helps keep your icons in the exact same place on every trial.
Keep track of the monkey's performance
self.num_correct = 0
self.correct_pct = 0.00
self.consecutive = 0
This is useful if you want to advance a monkey to a different condition after they achieve a specific percentage of success.
Create the Program and Game Loop
Before we give the the trials some functions, we need to first create the program and the main game loop. This is best done at the end of your code, so skip down to line 337 in the code, and we will come back to the trial functions later.
This first chunk of code uploads your task’s parameters. It reads the parameters.txt file from the task’s folder and assigns the value from the .txt file to a variable within the program. For example, if you open parameters.txt you will see that the ITI is set to 1. Now, by running this code, the variable called ITI is equal to 1. This is helpful as it allows you to change the task’s settings without having to change any code. If you want to increase the monkey’s ITI to 20 seconds, you can simply change the parameters.txt file without touching a single line of code.
params = getParams(varNames)
globals().update(params)
full_screen = params['full_screen']
train_or_test = params['train_or_test']
icon_condition = params['icon_condition']
trials_per_block = params['trials_per_block']
blocks_per_session = params['blocks_per_session']
ITI = params['ITI']
duration = params['duration']
run_time = params['run_time']
time_out = params['time_out']
Create the clock, screen, and cursor<b
These next three chunks of code establish a clock system in the program, create the task screen, and creates the cursor
1. Clock: This allows you to keep track of timing related items and is very useful for more advanced programs with specific time requirements.
# START THE CLOCK
clock = pygame.time.Clock()
start_time = (pygame.time.get_ticks() / 1000)
stop_after = run_time * 60 * 1000
2. Task Screen: This creates the screen of the task. Notice that it pulls the variable ‘full_screen’ which was just uploaded from the parameters.txt file. If ‘full_screen’ = True, then the task will fill the entire screen. If ‘full_screen’ = False, then it will create a 600×800 task window.
pygame.display.set_caption just labels your window with whatever you pass into the quotations
display_icon is a .png file that displays in the upper left corner of the task window. I usually just use a cute monkey, but you can use whatever you like!
screen.fill(white) sets the screen to the color of white. You can easily change this if you need a different color background, but by default it is white.
# CREATE THE TASK WINDOW
screen = setScreen(full_screen)
pygame.display.set_caption("System Check")
display_icon = pygame.image.load("Monkey_Icon.png")
pygame.display.set_icon(display_icon)
screen.fill(white)
3. Create the Cursor: Now its time to create the monkey’s cursor! The monkey’s cursor is simply an object of the Box class, which we call ‘cursor’. This means it inherits all the characteristics of a Box object and can move around the screen and interact with other Box and Icon objects.
speed = 8 is usually what I use for my tasks, but you can change the speed to make the cursor move faster (high values) or slower (low values)
# CREATE THE CURSOR
cursor = Box(color = red, speed = 8, circle = True)
Create the Main Game Loop
The main game loop, if you remember from Lesson X is the heart and soul of the task. Believe it or not, when you run a python script, this chunk of code is the only part of the code that is actually being run by the computer. That is clearly an oversimplification because you will see the game loop causes a million other things to run, but essentially the computer jumps right here to run your task. I like keeping it at the bottom of the code so that as I am working on the different components of a task, I can quickly scroll to the bottom to edit the game loop. Let’s run through what it is doing.
1. Create an Object called ‘trial’ that is over the Trial Class. Then have that newly created object, called ‘trial’ run the function .new() <– don’t worry we haven’t created this yet but we will soon.
2. Set the variable ‘running’ equal to True followed by a ‘while’ loop that runs continuously WHILE the variable running is equal to True. For a refresher on while loops click here.
3. Run the function quitEscQ() which allows us to close the program at any time by pressing the “esc” or “Q” keys.
4. Set up the timer
5. Fill the screen with the color white
6. Draw your cursor on the scree
"""MAIN GAME LOOP -------------------------------------------------------------------------------------------"""
trial = Trial() # Initialize a new Trial
trial.new() # Have the newly initialized trial run .new() function to begin ;)
running = True
while running == True:
quitEscQ()
timer = (pygame.time.get_ticks() / 1000)
if timer > run_time:
pygame.quit()
sys.exit()
screen.fill(white)
cursor.draw(screen)
7. Next we are going to create a variable that will give us constant feedback about what the cursor is touching. This variable is called “SELECT” and at any point you can ask for the value of SELECT to determine what the cursor is actively touching. As you can imagine, this is super helpful for colliding with stimuli.
8. Now we run the task through a series of if-else statements. Each if statement corresponds to a phase of the trial and as you can see, once one phase becomes false, it begins the next one. For this example, we are going to have only 2 phases.
startphase == True when we want the start button to be displayed
phase1 == True when we want the monkeys to touch a green box for a pellet.
SELECT = cursor.collides_with_list(trial.stimuli) # While the program is running, the variable called "SELECT"
clock.tick(fps) # is equal to the number of the stimuli in stimuli[]
if trial.startphase == True:
trial.start()
elif trial.startphase == False:
if trial.phase1 == True:
trial.run_trial()
refresh(screen)
Give your Trials Functions
Now that the trial class has been created and the game loop is ready to run, we need to give the trial class some functions. This is essentially how your program is going to run. It is going to create a trial object, and then within that object it is going to run all these functions. For example, each trial needs a function that tells it when a new trial is starting. Once again, we will stick with the same syntax as before.
new() function
This function is real simple. It makes the SELECT variable equal to -1. SELECT has not yet been established, but it is a variable that represents what the monkey’s cursor is touching. It is very useful for tasks with hundreds or thousands of stimuli. Next, it increases the trial number and the trial within block by 1. Using += 1 increases any number by 1. The opposite is also true, using -= 1 will decrease any number by 1. Then finally, it plays the chime sound to indicate a new trial has begun.
def new(self):
global start_time
global SELECT
SELECT = -1
self.trial_number += 1 # Increment trial number by 1
self.trial_within_block += 1 # Increment trial within block by 1
sound_chime.play()
If you have blocks within your program, this is also the time where you need to check if its the last trial in the block. If it is, you need to reset a couple things. We can do this by using a simple if loop to run the function .newBlock() which we will define next.
if self.trial_within_block == self.block_length: # If this is the last trial in the block
self.trial_within_block = 0 # Reset this to 0
self.newBlock() # Run the function .newBlock()
Next you need to reset all your phases to False, except for your start phase and all of your events to False.
Lastly, create your stimuli, because this is a new trial and they need to be recreated each time, and then move your cursor to the start position.
newBlock() function
This function is rather simple, just advances the program to the next block of trials and checks to see if this is the last block in the session. If it is, then it quits the program out.
def newBlock(self):
"""Moves program to the next block and randomizes the trial types"""
self.block += 1 # Increment block by 1
# Reset all events to False
self.event1 = False
self.event2 = False
self.event3 = False
if self.block > self.blocks_per_session: # Check if this is the last block in the session
print("Session Complete!") # If it is, then quit!
pygame.quit()
sys.exit()
create_stimuli() function
This function creates the stimuli for your trial. In this case we only have three stimuli: (1) a start button, (2) a correct button, and (3) an incorrect button, which we will use in the next lesson.
To make these stimuli, first we are going to create a list of Icon Objects with various .png files and call the list Icons. This list called Icons should contain every stimuli you are going to use throughout the entire program.
After Icons is made, we are going to create a second list called self.stimuli[].
def create_stimuli(self):
"""Create the stimuli based on the trial type"""
"""Use choice() to randomly select a stimuli within a range of indices"""
global icon_condition # Use icon_condition from parameters.txt to counterbalance stimuli between monkeys
# Create a list called "Icons" that is made up of a list of Objects with the "Icon" Class
Icons = [Icon("start.png", (150, 200), (140, 140)),
Icon("correct.png", (0, 0), (100, 100)),
Icon("incorrect.png", (0, 0), (100, 100))
]
# Icons[] determined which pngs are taken in, self.stimuli[] determines which stimuli can be selected for this specific trial
self.stimuli = [Icons[0], Icons[1], Icons[2]]
You will notice that for this program, Icons and self.stimuli are identical, but in more complex programs, you may want to vary self.stimuli based on what kind of trial is being run. Here is an example of that from the next lesson:
if self.trial_type[trial_within_block] == 1:
self.stimuli = [Icons[0], Icons[1], Icons[2]]
elif self.trial_Type[trial_within_block] == 2:
self.stimuli = [Icons[3], Icons[4], Icons[5]]
create_start() and draw_stimuli()
These functions draw the stimuli on the screen at the (x,y) position you specify. These functions are super helpful for quickly drawing the start button or the stimuli.
def draw_start(self):
"""Draw the start button at center of the screen"""
self.stimuli[0].mv2pos((400, 400))
self.stimuli[0].draw(screen)
def draw_stimuli(self):
"""Draw the stimuli at their positions after start button is selected"""
global icon_positions
self.stimuli[1].mv2pos(self.icon_position[0])
self.stimuli[1].draw(screen)
start() function
Here is the first complex function that tells the program to actually do something that you can see on screen. This function draws the start button and the cursor on the screen. Then it allows the cursor to move in the up direction.
If the cursor collides with the start button, it wipes the screen clear, starts an internal timer, and advances the program to Phase 1. Once Phase 1 is set to True, we can check our main game loop and see that this causes the program to run the function run_trial() which we will code next.
def start(self):
global SELECT
global timer
global start_time
self.draw_start()
cursor.draw(screen)
moveCursor(cursor, only = 'up')
if cursor.collides_with(self.stimuli[0]):
screen.fill(white)
self.start_time = pygame.time.get_ticks()
self.startphase = False
self.phase1 = True
run_trial() function
This function runs the actual trial and based on the main game loop is activated once Phase 1 == True.
First we have to redraw the cursor on the screen. You have to do this after each trial because every time you change phases, the screen is erased. Then we change the cursor’s movement to any direction by removing the ‘up’ argument from the moveCursor() function.
Next, we get rid of the start button and draw the stimuli using draw_stimuli()
If the cursor touches the stimuli, then we play the true sound, dispense a pellet, erase the screen and give the monkey a delay equal to the variable ITI in seconds (that is why it is multiplied by 1000). Once all of that is done we write our data into a .csv file using .write() and jump back up to a new trial.
def run_trial(self):
global SELECT
global timer
global start_time
global button_positions
global duration
cursor.draw(screen)
moveCursor(cursor)
self.stimuli[0].mv2pos((-50, -50))
self.stimuli[0].size = 0
self.draw_stimuli()
if cursor.collides_with_list(self.stimuli) == 1:
sound(True)
pellet()
self.num_correct += 1
screen.fill(white)
refresh(screen)
pygame.time.delay(ITI * 1000)
self.write(data_file, 1)
self.new()
write() function
This function, when called, writes information down into a .csv file. This function is critical to actually collecting data. After all, these are experiments not just fun little games. Full disclosure, this is one of the most important functions, but I typically wait until the very end of coding to write this function. I have found it is easiest that way.
def write(self, file, correct):
"""This function writes whatever you want to an excel file. When you call it in the program it will write one row of excel"""
global icon_condition
now = time.strftime('%H:%M:%S')
data = [monkey, today, now, self.train_or_test, self.block, self.trial_number, self.trial_type[self.trial_within_block], correct]
writeLn(file, data)