Wackadot

In this assignment you will create the game Wackadot. You may not be familiar with the name, but you have probably seen the game mechanic in some form. Note that this project is split up into steps that have chunks of code associated with each. I am starting a few conventions here that we will continue to use throughout the course. New code is in red, code from past step(s) is in gray, and chunks of code that have been removed for brevity's sake because they have not changed from the last step are marked by purple ellipses. Where chunks of code have been removed, there will be an ellipses, some "old code" so you can find the place in the code, the new red code, and possibly more "old code" afterwards, again as an aid for finding the right spot. What you want to do is paste the red new code into the appropriate place in your program like it shows in the step, so your program code ends up looking like the code here on the website for each step. Finally, make sure you understand and fully complete this project, because we will come back to it later in the course!

Gameplay: Wackadot is a game, in its simplest form, where there are three dots on screen of two colors. Two dots are of opposing colors and the third follows the mouse cursor and changes color based on the following rules. The goal of the game is to hit the dot of the same color as your cursor, e.g. if your cursor is red then you want to hit the red dot on the screen to score a point. If you hit the wrong color you will lose a point. When you hit the dot that is the same color, the cursor swaps color and the other dot randomly relocates on the screen.

Design: Before looking at the below list of steps first consider how you would break down this problem and in what stages you would implement it. A huge part of being a good programmer or computer scientist is being able to break a problem down into its elementary parts to make implementation easier. It also aids in testing and debugging your code because incremental steps with small code additions are easy to fix rather than looking through a final project that is hundreds or thousands of lines of code. This breaking-down and debugging usually lets you refine your code and ideas while you are making a program rather than getting in a rut, completing your code, and finding it too much of a mess to add to at a later time.

Thoughts: Below are the steps that I chose to implement the program, which you will follow. Place the order and ideas you came up with in your journal and discuss how they are different from mine and the pros and cons of each way of doing it. After you get the code from each step working review what happened in the stage and try to understand how the code made that happen. Write these reflections for each step in your journal also. When game is complete make sure to write about what you thought of this project as a whole and what you learned. At each step you need to answer what the added code did and how you think this is accomplished. Once you are finished, below are some questions you should answer when thinking about the process as a whole.

  • Some steps have code added in multiple places. Why is this? Why does the code to do one thing require additions in multiple places?
  • What are some things you did not understand? What do you think "from pg_helpers import *" did? What do you think the CircleSprite and TextSprites do? What about the drawing groups? Why are there two (and then three) different groups by the end?
  • Finally, any other reflections, comments, or questions you have.

Step 0: Create a new project in Eclipse

Make sure to name it Wackadot. Go here for our tutorial on creating new projects in Eclipse. Also download pg_helpers.py from the Materials page and place it in your src folder before you begin.

Step 1: Create a pygame display

#!/usr/bin/env python

import pygame, random
from pygame.locals import *
from sys import exit

# initialize pygame
pygame.init( )

# setup the screen
scr_size = ( 800, 600 )  # first is width, second height
screen = pygame.display.set_mode( scr_size, 0, 32 )
background = pygame.Surface( scr_size )
background.fill( ( 0, 0, 0 ) )
pygame.display.set_caption( "Wackadot" )

# seed random number generator
random.seed( )

# the game loop
while True:
  
  # check if the user has quit the program and if so exit
  for event in pygame.event.get( ):
    if event.type == QUIT:
      exit( )

# end while (game loop)

Step 2: Display a dot

from pygame.locals import *
from pg_helpers import *
...
# seed random number generator
random.seed( )

# global information
color1 = ( 255, 0, 0 ) # red
radius = 35
cursor = CircleSprite( radius, ( 0, 0 ), color1 )

# create groups for screen updates/drawing
drawgrp = pygame.sprite.RenderUpdates( )
drawgrp.add( cursor )

# the game loop
while True:
...
      exit( )

  # draw the dots, cursor 
  rectlist = drawgrp.draw( screen )
  pygame.display.update( rectlist )
  drawgrp.clear( screen, background )

# end while (game loop)

Step 3: Display the cursor as a dot

# the game loop
while True:
...
      exit( )

  # adjust the cursor
  cursor.set_cposition( pygame.mouse.get_pos( ) ) # move the middle of circle to cursor position

  # draw the dots, cursor 
  rectlist = drawgrp.draw( screen )

Step 3.1: How would you remove the cursor pointer so all you can see is the dot?

Hint: The pygame documentation may help - http://www.pygame.org/docs/ref/mouse.html

Step 4: Create the other two dots

background.fill( ( 0, 0, 0 ) )
pygame.display.set_caption( "Wackadot" )

# remove the pointer for the cursor (we're using a dot for the cursor)
pygame.mouse.set_visible( False )

# seed random number generator
random.seed( )
...
# global information
color1 = ( 255, 0, 0 ) # red
color2 = ( 0, 0, 255 ) # blue
radius = 35
cursor = CircleSprite( radius, ( 0, 0 ), color1 )
dot1 = CircleSprite( radius, ( random.randint( 0, scr_size[ 0 ] ), random.randint( 0, scr_size[ 1 ] ) ), color1 )
dot2 = CircleSprite( radius, ( random.randint( 0, scr_size[ 0 ] ), random.randint( 0, scr_size[ 1 ] ) ), color2 )

# create groups for screen updates/drawing
drawgrp = pygame.sprite.RenderUpdates( )
drawgrp.add( cursor )
drawgrp.add( dot1 )
drawgrp.add( dot2 )

Step 4.1: Drawing Order

Move the cursor around, notice that it goes under the other dots. Why is this? Let's try an experiment! Re-arrange how the three dots are added to the drawgrp Group to the following:

# create groups for screen updates/drawing
drawgrp = pygame.sprite.RenderUpdates( )
drawgrp.add( dot1 )
drawgrp.add( dot2 )
drawgrp.add( cursor )
Now run again and see what happens. The cursor *probably* will now go over the dots, but there is no way to tell. Try running the program several times to see if you can get both over and under behaviors (note: it may never swap, like I said, there is no way to tell). Why do you think this is? Write your thoughts in your journal. Let's switch from using RenderUpdates to OrderedUpdates.
# create groups for screen updates/drawing
drawgrp = pygame.sprite.OrderedUpdates( )
drawgrp.add( dot1 )
drawgrp.add( dot2 )
drawgrp.add( cursor )
The cursor now will always go over the dots! So how do you think RenderUpdates works versus OrderedUpdates? Write your thoughts in your journal.

Step 5: Collision handling

Note that in this step in the second chunk of new code the comment has been updated, so make appropriate changes to that line or overwrite it.

dot1 = CircleSprite( radius, ( random.randint( 0, scr_size[ 0 ] ), random.randint( 0, scr_size[ 1 ] ) ), color1 )
dot2 = CircleSprite( radius, ( random.randint( 0, scr_size[ 0 ] ), random.randint( 0, scr_size[ 1 ] ) ), color2 )

# function to move sprite to a new random position
def new_position( csprite ):
  csprite.set_left( random.randint( 0, scr_size[ 0 ] )-radius )
  csprite.set_top( random.randint( 0, scr_size[ 1 ] )-radius )

# create groups for the collision handling and screen updates/drawing
dots = pygame.sprite.Group( )
dots.add( dot1 )
dots.add( dot2 )

drawgrp = pygame.sprite.OrderedUpdates( )
drawgrp.add( dot1 )
drawgrp.add( dot2 )
drawgrp.add( cursor )

...
  # adjust the cursor
  cursor.set_cposition( pygame.mouse.get_pos( ) ) # move the middle of circle to cursor position

  # collision handling
  collide = pygame.sprite.spritecollide( cursor, dots, False )
  for d in collide:
    new_position( d )

Step 6: Swapping colors

Note the use of if statements here. You test if the cursor is equal to one color and set it to one thing accordingly, if not you set it to the other color. This is a common pattern that is worth remembering.

  # collision handling
  collide = pygame.sprite.spritecollide( cursor, dots, False )
  for d in collide:
    new_position( d )
    if cursor.color == d.color:
      if cursor.color == color1:
        cursor.set_color( color2 )
      else:
        cursor.set_color( color1 )
    

Step 7: Keeping score

Note that in this step, the fourth chunk of new code is again an update to a comment; replace or edit the existing one rather.

# global information
...
cursor = CircleSprite( radius, ( 0, 0 ), color1 )
dot1 = CircleSprite( radius, ( random.randint( 0, scr_size[ 0 ] ), random.randint( 0, scr_size[ 1 ] ) ), color1 )
dot2 = CircleSprite( radius, ( random.randint( 0, scr_size[ 0 ] ), random.randint( 0, scr_size[ 1 ] ) ), color2 )
score = 0
scoreSprite = TextSprite( "Score: " + str( score ), 32, ( 10, 10 ), ( 0, 255, 0 ) )

# create groups for the collision handling and screen updates/drawing
dots = pygame.sprite.Group( )
dots.add( dot1 )
dots.add( dot2 )
drawgrp = pygame.sprite.OrderedUpdates( )
drawgrp.add( dot1 )
drawgrp.add( dot2 )
drawgrp.add( cursor )
drawgrp.add( scoreSprite )
...
  # adjust the cursor
  cursor.change_position( pygame.mouse.get_pos( ) )
  
  # draw the dots, cursor, and score
  rectlist = drawgrp.draw( screen )
  pygame.display.update( rectlist )
  drawgrp.clear( screen, background )

  # collision handling
  collide = pygame.sprite.spritecollide( cursor, dots, False )
  for d in collide:
    new_position( d )
    if cursor.color == d.color:
      score = score + 1
      if cursor.color == color1:
        cursor.set_color( color2 )
      else:
        cursor.set_color( color1 )
    else:
      score = score - 1
    scoreSprite.set_text( "Score: " + str( score ) )

# end while (game loop)

Possible Improvements

  • If you wish, change the dot and cursor colors so they are no longer red and blue.
  • Add obstacles that cause the cursor to randomly relocate and subtract 1 point from the score. Hints:
    • Define a new color besides color1 and color2 (call it obscolor).
    • Define a new CircleSprite called obstacle (similar to how do1 and dot2 are created).
    • Make another group to go with the existing draw and collision ones. It will contain the obstacles and these obstacles will also need to be added to the draw group.
    • Add another chunk in the "#collision handling" section after the "scoreSprite.change_text( "Score: " + str( score ) )" statement. The indentation for your new code should be even with "collide = pygame.sp...."
    • In this chunk we want "collide2 = ..." like the already existing "collide =...." line, but we want to check against something else other than the dots group. Which group are we worried about now?
    • Finally, we need another chunk of code like the for loop from "for dot in collide:" down to "scoreSprite.change_text( "Score: " + str( score ) )". Except we want a different variable name besides dot (what are we hitting exactly? try an abbrevation of it) and we will not use collide in the equivalent "collide = " part, instead... what? (remember the last step!)
    • From here keep following the existing for loop line by line to see what you need to copy down to your new loop and/or adjust (or leave out entirely)
  • Add another obstacle type which will be stationary and subtract 2 from the player's score. Stationary means it will not move when hit and specifying its starting location instead of having it random like the obstacle from the above step (the first obstacle's location was random because we were copying off of dot1 and dot2). To specify the staring location, make obstacle2's assignment statement similar to cursor's assignment statement instead of dot1, dot2, and obstacle. Getting this "subtract score but do not move" thing to work will require creating yet another group for stationary obstacles and another "collision3 = ..." with following for loop chunk to handle the collisions properly. What happens when you leave the cursor over it? Ask Dr. Kuhl if you are getting the proper results and then journal about why this happens.
  • Now that you know how to make both types, add enough new obstacles to have a couple of each type to make the game a bit more interesting. Maybe make a small pattern of manually placed obstacles and one or two random ones.
  • Add another dot and color to the mix
  • Advanced: have multiple shapes (RectangleSprite will come in handy) as well as multiple colors.