9 Whack-a-Mole

Today you will create your first video game Whack-a-Mole. The game itself is very much a reaction time experiment: moles/targets appear after a random delay at one of the predefined locations, the player’s task is to whack (press a corresponding button) the mole/target before it disappears. Your final game should look approximately like the one in the video: Circles (moles) turn white, if I hit the correct button in time.

Grab the exercise notebook before we start!

9.1 Chapter concepts

9.2 Lists

So far, we were using variables to store single values: computer’s pick, player’s guess, number of attempts, PsychoPy window object, etc. But sometimes we need to handle more than one value. We already had this problem in the computer-based Guess-the-Number game when we needed to store the remaining number range. We got away by using two variables, one for the lower and one for the upper limit. However, this approach clearly does not scale well and, sometimes, we might not even know how many values we will need to store. Python’s lists are the solution to the problem.

A list is a mutable38 sequence of items where individual elements can be accessed via their zero-based index. Extending the idea of variable-as-a-box, you can think about lists as a box with numbered slots. To store and retrieve a particular piece you will need to know both the variable name and the index of the item you are interested in within that box. Then, you work with a variable-plus-index in exactly the same way you work with a normal variable, accessing or changing its value via the same syntax as before.

A list is defined via square brackets <variable> = [<value1>, <value2>, ... <valueN>]. An individual slot within a list is also accessed via square brackets <variable>[<index>] where index is, again, zero-based39. This means that the first item is variable[0] and, if there are N items in the list, the last one is variable[N-1]. You can figure out the total number of items in a list by getting its length via a special len() function. Thus, you can access the last item via variable[len(variable)-1]40. Note the -1: If your list has 3 items, the index of the last one is 2, if it has 100, then 99, etc. I am spending so much time on this because it is a fairly common source of confusion.

Do exercise #1 see how lists are defined and indexed.

Lists also allow you access more than one slot/index at a time via slicing. You can specify index of elements via <start>:<stop> notation. For example, x[1:3] will give you access to two items with indexes 1 and 2. Yes, two items: Slicing index goes from the start up to but not including the stop. Thus, if you want to get all the items of a list, you will need to write x[0:length(x)] and, yet, to get the last item alone you still write x[len(x)-1]. Confusing? I think so! I understand the logic but I find this stop-is-not-included to be counterintuitive and I still have to consciously remind myself about this. Unfortunately, this is a standard way to define sequences of numbers in Python, so you need to memorize this.

Do exercise #2 to build the intuition.

When slicing, you can omit either start or stop. In this case, Python will assume that a missing start means 0 (the index of the first element) and missing stop means len(<list>) (so, last item is included). If you omit both, e.g., my_pretty_numbers[:] it will return all values, as this is equivalent to my_pretty_numbers[0:len(my_pretty_numbers)].41

Do exercise #3.

You can also use negative indexes that are computed relative to length of the list42. For example, if you want to get the last element of the list, you can say my_pretty_numbers[len(my_pretty_numbers)-1] or just my_pretty_numbers[-1]. The last-but-one element would be my_pretty_numbers[-2], etc. You can use negative indexes for slicing but keep in mind the including-the-start-but-excluding-the-stop catch: my_pretty_numbers[:-1] will return all but last element of the list not the entire list!

Do exercise #4.

Slicing can be extended by specifying a step via start:stop:step notation. step can be negative, allowing you to build indexes in the reverse order:

my_pretty_numbers = [1, 2, 3, 4, 5, 6, 7]
my_pretty_numbers[4:0:-1]
#> [5, 4, 3, 2]

However, you must pay attention to the sign of the step. If it goes in the wrong direction then stop cannot be reached, Python will return an empty list.

my_pretty_numbers = [1, 2, 3, 4, 5, 6, 7]
my_pretty_numbers[4:0:1]
#> []

Steps can be combined with omitted and negative indexes. To get every odd element of the list, you write my_pretty_numbers[::2]:

my_pretty_numbers = [1, 2, 3, 4, 5, 6, 7]
my_pretty_numbers[::2]
#> [1, 3, 5, 7]

Do exercise #5.

If you try to to access indexes outside of a valid range, Python will raise an IndexError43. Thus, trying to get 6th element (index 5) of a five-element-long list will generate a simple and straightforward error. However, if your slice is larger than the range, it will be truncated without an extra warning or an error. So, for a five-element list my_pretty_numbers[:6] or my_pretty_numbers[:600] will both return all numbers (effectively, this is equivalent to my_pretty_numbers[:]). Moreover, if the slice is empty (2:2, cannot include 2 because it is a stop value, even though it starts from 2 as well) or the entire slice is outside of the range, Python will return an empty list, again, neither warning or error is generated.

Do exercise #6.

In Python lists are dynamic, so you can always add or remove elements to it, see the list of methods. You can add a new item to the of the end of the list via .append(<new_value>) method

my_pretty_numbers = [1, 2, 3, 4, 5, 6, 7]
my_pretty_numbers.append(10)
my_pretty_numbers
#> [1, 2, 3, 4, 5, 6, 7, 10]

Or, you can insert(<index>, <new_value>) before an element with that index. Unfortunately, this means that you can use an arbitrary large index and it will insert a new value as a last element without generating an error.

my_pretty_numbers = [1, 2, 3, 4, 5, 6, 7]
my_pretty_numbers.insert(2, 10)
my_pretty_numbers.insert(500, 20)
my_pretty_numbers
#> [1, 2, 10, 3, 4, 5, 6, 7, 20]

You can remove an item using its index via pop(<index>), note that the item is returned as well. If you omit the index, pop() removes the last element of the list. Here, you can only use valid indexes.

my_pretty_numbers = [1, 2, 3, 4, 5, 6, 7]
my_pretty_numbers.pop(-1)
#> 7
my_pretty_numbers.pop(3)
#> 4
my_pretty_numbers
#> [1, 2, 3, 5, 6]

Do exercise #7.

9.3 Basic game scaffolding

Phew that was a lot about lists44. However, All work and no play makes Jack a dull boy! So let us start with a basic PsychoPy scaffolding. Here the code structure:

import libraries from [psychopy]
create the PsychoPy window (visual.Window())
flip the window (.flip())
wait for a player to press the escape key (event.waitKeys())
close the window (.close())

Try doing it from scratch. I have left hints to help you with this and you can always consult the online documentation. Do not forget to document the file and to split your code into meaningful chunks with comments (if needed).

Put your code into code01.py.

9.4 Three moles

Let us create three moles that will be represented by circles. Create a new list variable moles and put three circles into it. One should go to the left, one dead center, and one to the right. Watch a video above to see what I mean. Think of a reasonable size (which units make keeping circle a circle easier?) and position. You can also use different colors for them, as I did.

You can either create an empty list and then .append() circles one at a time or you can use square brackets to put all three of them into the list in one go. Then draw() circles before you flip the window and wait for a key press. Note that you have to draw them one at a time. Therefore, you will need to add three lines for this but the next section will show you an easier way.

Put your code into code02.py.

9.5 For loop

In the code above, we needed to iterate over three moles (circles) that we had in a list. Python has a tool just for that: a for loop that iterates over the items in any sequence (our list is a sequence!). Here is an example:

numbers = [2, 4, 42]
for a_number in numbers:
    print("Value of a_number variable on this iteration is %d"%(a_number))
    a_number = a_number + 3
    print("  Now we incremented it by 3: %d"%(a_number))
    print("  Now we use in a formula a_number / 10: %g"%(a_number / 10))
#> Value of a_number variable on this iteration is 2
#>   Now we incremented it by 3: 5
#>   Now we use in a formula a_number / 10: 0.5
#> Value of a_number variable on this iteration is 4
#>   Now we incremented it by 3: 7
#>   Now we use in a formula a_number / 10: 0.7
#> Value of a_number variable on this iteration is 42
#>   Now we incremented it by 3: 45
#>   Now we use in a formula a_number / 10: 4.5

Here, the code inside the for loop is repeated three times because there are three items in the list. On each iteration, next value from the list gets assigned to a temporary variable a_number (see the output). Once the value is assigned to a variable, you can use it just like any variable. You can print it out (first print), you can modify it (second line within the loop), use its value for when calling other functions, etc. To better appreciate this, copy-paste this code into a temporary file (call it test01.py), put a breakpoint onto the first print statement and then use F10 to step through the loop and see how value of a_number variable changes on each iteration and then it gets modified in the second line within the loop.

Note that you can use the same break statement as for the while loop.

Do exercise #8.

9.6 Drawing in a loop

Now that you have learned about the for loop, it is easy to draw the moles. Just iterate over the list (come up with a good temporary variable name) and draw() a current item (which is in your temporary variable).

Put your code into code03.py.

9.7 range() function: Repeating code N times

Sometimes, you might need to repeat the code several times. For example, imagine that you have 40 trials in an experiment. Thus, you need to repeat a trial-related code 40 times. You can, of course, build a list 40 items long by hand and iterate over it but Python has a handy range() function for that. range(N) yields N integers from 0 to N-1 (same up-to-but-not-including rule as for slicing) that you can iterate over in a for loop.

for x in range(3):
    print("Value of x is %d"%(x))
#> Value of x is 0
#> Value of x is 1
#> Value of x is 2

You can modify range() function behavior by providing a starting value and a step size. But in its simplest form range(N) is a handy tool to repeat the code that many times. Note that while you always need to have a temporary variable in a for loop, sometimes you may not use it at all. In cases like this, you should use _ (underscore symbol) as a variable name to indicate the lack of use.

for _ in range(2):
    print("I will be repeated twice!")
#> I will be repeated twice!
#> I will be repeated twice!

Alternatively, you can use range() to loop through indexes of a list (remember, you can always access an individual list item via var[index]). Do exactly that45! Modify your code to use range() function in the for loop (how can you compute the number of iterations you need from the length of the list?), use temporary variable as an index for the list to draw each item46. When in doubt, put a breakpoint inside (or just before) the loop and step through your code to understand what values a temporary loop variable gets and how it is used.

Put your modified code into code04.py.

9.8 A random mole

Drawing all three moles served as a practical exercise with loops but in a real game we need to shown only one random target at a time. We could create the three targets as before and draw one of them. However, later on we would like to change the color of the target to indicate that the player did hit it, so it is simpler (if a bit wasteful) to create a single mole every time we need one.

For this, define one CONSTANT with a list of three colors that you used and another one with three horizontal locations (the vertical location is the same, so we do not need to worry about it). Next, randomly pick which target out of three you want to create, i.e., we need to generate an index of the target and use that index to figure out target’s location and color. You can do it either via random.randrange() or via random.choice() building the range yourself via the function with the same name you have just learned about (remember to organize your imports alphabetically). Store the index in a variable with a meaningful name47 and use it with constants to create the target of the corresponding color at a corresponding location. Then, you need to draw that single target before waiting for a key press.

Once you have the code, put a breakpoint and check that the value of the index variable matches what is shown on a screen48.

Put your modified code into code05.py.

9.9 Random time

What makes Whack-a-Mole game fun is not only that you do not know which mole will appear but you also do not know when it will appear and how much time you have to whack it. Thus, we need to modify our presentation schedule. We need a blank period of a random duration (I would suggest between 0.75 s to 1.5 s) and limited presentation duration (between 0.5 to 0.75 s). First, you need to define these ranges as constants. Now that you know lists you can use a single variable to hold both ends of the range. Then, you need to generate two numbers (one for the blank another for the presentation) coming from a uniform distrubition within that range.

Here, a CONSTANT will hold values for two parameters of random.uniform() function and there are two ways of using them. First, you can use an index 0 to get the value for the first parameter and 1 for the second parameter:

import random

TIME_UNTIL_BEEP = [0.1, 0.3]
random.uniform(TIME_UNTIL_BEEP[0], TIME_UNTIL_BEEP[1])

However, Python has a nifty trick called [Unpacking Argument Lists]: You can pass a list of arguments prepended by an asterisk and Python will unpack the list into arguments in the same order they are in the list: first value goes to the parameter, second value to the second parameter, etc. So, in our case, the code can be simplified to

random.uniform(*TIME_UNTIL_BEEP)

Note that it is on you to make sure that the number and the order of elements in the list match function parameters!

def single_parameter_function(x):
  """Do nothing but require a single paramter
  """
  pass

TWO_VALUES = [1, 3]

single_parameter_function(*TWO_VALUES)
#> single_parameter_function() takes 1 positional argument but
#> 2 were given

Back to the game, use random.uniform() function to generate random blank and presentation times, store them into variables of your choice, and time your blank and presentation using the wait() function from the clock module.

Now is time to update and structure you code. Here is a approximate outline (note that I have dropped the wait for keys):

"""Document your file
"""
import all libaries you need in an alphabetical order

define CONSTANTS

create window

# generating random parameters for the trial
pick random index for the mole
create the mole
generate random durations for blank and presentation interval

# blank
clear window (win.flip() alone)
wait for "blank duration" seconds

# presentation
draw the mole
wait for "presentation duration" seconds

close the window

Note that it has no response processing at the moment and that window should close right after the stimulus is presented.

Put your code into code06.py.

9.10 Repeating trials

You already know how to repeat the same code many times. Decide on number of trials / rounds (define this as a CONSTANT) and repeat the single round that many times. Think about what code goes inside the loop and what should stay outside for the randomization to work properly.

Put your code into code07.py.

9.11 Exit strategy

I hope that you used a small number of trials because (on my advice, yes!) we did not program a possibility to exit the game via the escape key. To put it in, we will replace both wait() calls with waitKeys() function. It has maxWait parameter that by default is set to infinity but can be set to the duration we require. If a player does not press a key, it will work just like wait() did. If a player presses a key (allow only "escape" for now), it means that they want to abort the game (the only possible action at the moment). Thus, assign the returned value to a temporary variable (keys?) and check whether it is equal to None49. If it is not equal to None, break out of the loop!

Put your code into code08.py.

9.12 Whacking that mole

We have moles that appear at a random location after a random delay for a random period of time. Now we just need to add an ability to whack ’em! You whack a mole only when it is present. Thus, we only need to modify and handle the waitKeys() call for the presentation interval.

First, create a new constant with three keys that correspond to three locations. I would suggest using ["left", "down", "right"], which are cursor keys50. Next, you need to use them for the keyList parameter. However, we cannot use this list directly, as we also need the escape key. The simplest way is to put “escape” into its own list and concatenate the two lists via +: ["escape"] + YOUR_CONSTANT_WITH_KEYS. Do this concatenation directly when you set a value to the keyList in the function call. Before we continue, run the code and test that you can abort the program during the presentation (but not during the blank interval) by pressing any of these three keys. Also check that escape still works!

Now that we have keys to press, we need more sophisticated processing (we gonna have quite a few nested conditional statements). We still need to check whether waitKeys() returned None first. If it did not, it must have returned a list of pressed keys. Actually, it will be a list with just a single item51, so we can work with it directly via keys[0]. Use conditional if-else statement to break out of the loop if the player pressed escape. Otherwise, it was one of the three “whack” keys.

Our next step is to establish which index the key corresponds to. Python makes it extremely easy as lists have .index(value) method that returns the index of the value within the list. You have the (CONSTANT) list with the keys and you have the pressed key: Figure out the index and check whether it matches the index of the target (imole variable in my code). If it does, let us provide a visual feedback of success: change mole (circle) fillColor to white, draw it, and wait for 300 ms (setup a constant for feedback duration). This way, the mole will turn white and remain briefly on the screen when hit but will disappear immediately, if you missed.

Put your code into code09.py.

9.13 You did it!

Congratulations on your first video game! It could use some bells-and-whistles like having a score, combos would be cool, proper mole images instead of a circle, etc. but it works and it is fun (if you do not feel challenged, reduce the presentation time)! Submit your files and next time we will ditch the keyboard and learn how to handle the mouse in the Memory game.