CS 152: Project 9

Title image Project 9
Spring 2020

Project 9: Obstacle Course

The focus on this project is to provide you with experience writing classes with inheritance. We will also be using a dictionary to make working with collisions easier.


Tasks

  1. Create a Block class

    The first task is to redo the Block child class using inheritance. The parent class should be Thing.

    1. Define the Block __init__ method

      The __init__ method should have the required arguments: win, width, and height. You may add additional optional arguments for position, velocity, acceleration, color, and elasticity.

      def __init__(self, win, width, height, ... other optional parameters):

      As with the Ball class, the first action in the __init__ method is to call the parent Thing.__init__ method. Specify the type as the string "block". Any parameters you don't pass to the Thing.__init__ method should then be assigned to their proper field.

      Assign width and height to fields of your choice. Finish the constructor by calling self.render().

    2. Create the render method

      def render(self): # rebuilds the graphics objects that make the visualization

      The render method should undraw the graphics objects if they are drawn. Then it should define the self.shapes list of graphics objects using the current fields. Then it should draw the graphics objects if they were drawn to begin with. Note that if you use the undraw and draw methods, they affect the value of self.drawn. Therefore, it's best to make a local copy of self.drawn and use that to test if the object needs to be undrawn/drawn.

      When creating the visualization for the Block, put the anchor point in the center of the block. One corner should be at (x0-width/2, y0-height/2) and the other corner should be at (x0+width/2, y0+height/2). Remember to subtract the y-coordinate from self.win.getHeight() when creating Zelle graphics objects.

    3. Write getWidth and getHeight

      def getWidth(self): # returns the width of the block

      def getHeight(self): # returns the height of the block

    4. Write setWidth and setHeight

      def setWidth(self, width): # updates the width field and calls the render method.

      def setHeight(self, height): # updates the height field and calls the render method.

    5. Write a setColor method (optional)

      def setColor(self, color): # updates the color field and updates the color of one or more of the shapes in self.shapes using its setFill method.

      You will need to do this if you don't want all of the graphics shapes to be the same color.

    Test your basic block class methods test9-5.py

  2. Make two more shape classes of your choice

    Choose a shape. It can be a single polygon (e.g. a triangle, a pentagon, or an odd shape) or it could be a multi-object shape like a snowman. The new shape class should inherit Thing. The __init__ method should first call the Thing.__init__ method and make sure all the necessary information is provided. Then is should create any fields unique to the object and (e.g. radius for the Ball class) and finish by calling render.

    For the type field, give each shape a unique name like "triangle" or "snowmonster".

    The render method should define the graphics objects that define the simulated object. It's ok if this class is just a single graphics object. When you define the object's visualization, define it so that the object's position corresponds to the center of the object, just like it did for the Circle and Block classes.

    If the shape is best modeled as a block (for the purpose of collisions) then it will need getHeight, getWidth, setHeight, and setWidth methods. If the shape is best modeled as a circle, then it will need getRadius and setRadius methods. You may add other set and get methods as appropriate.

    If the class has a complicated color scheme, you will want to write a setColor for the child class.

    Write a program that draws each of your objects (including the ball and the block) into a window. Include a picture of the window in your handin directory. You may want to use this as test code for your other new methods.

  3. Collisions

    Download the following file.

    collision.py

    This file implements more sophisticated collisions. It predicts the trajectory of a ball a small time into the future and will (optionally) reflect the ball's velocity. The file implements three functions.

    1. collision_ball_ball( ball1, ball2, dt, bounce=True ) - Tests if ball1 collides with ball2 within time dt, assuming ball2 is stationary.

      If bounce is True, ball1 and ball2 will reflect their velocities around the normal to the collision point. If bounce is False, the velocities of ball1 and ball2 will not be modified. The motion of ball1 will be updated through the time period dt including after the collision as if its update method were called. The motion of ball2 will not be updated, but its velocity will be modified if bounce is equal to True.

    2. collision_ball_block( ball, block, dt, bounce=True ) - Tests if ball collides with block within time dt, assuming block is stationary.

      If bounce is True, ball1 will reflect its velocity around the normal to the collision point. The block's velocity is not affected. If bounce is False, the velocity of ball will not be modified. The motion of the ball updated through the time period dt, including after the collision, as if its update method were called. The motion of the block will not be updated.

    3. getBlockSideHit() - Returns which side of the block was most recently hit the last time collision_ball_block was called.

      0: hit on left side
      1: hit on right side
      2: hit on top side
      3: hit on bottom side

      If there was no collision the last time ball_block was called, the value is undefined (not meaningful)

    The next task is to modify the collision.py file to make it simpler to call the proper collision function no matter what types of objects are involved. (Note, the collision functions all work with a ball and either a ball or a block. Block-block collisions are not handled).

    In order to make your other shapes collide, you have to choose whether they are better modeled by a block or a ball and call the appropriate collision function. For example, a triangle is well-modeled by a ball with a radius that half the length of one side.

    Instead of using a big set of if/elif/else statements, we can use a dictionary with the two ypes involved in the collision as the key, making use of the type field in the Thing class. For example, if we have two objects, item1 and item2, we can create a key by writing:

    key = (item1.getType(), item2.getType())

    The key is a tuple that has two strings in it. Then, we can generate a dictionary that contains all the possible key combintions and stores the proper function to call in each case. For example, the following creates an entry in the dictionary collision_router with the key ('ball', 'ball') and sets its value to the function reference collision_ball_ball.

    collision_router[ ('ball', 'ball') ] = collision_ball_ball

    The above line creates a new entry in the collision_router dictionary and its value is a function reference to the collision_ball_ball function (note there are no parentheses after collision_ball_ball). This entry in the dictionary can be used to execute the collision_ball_ball function using the following syntax.

    collision_router[ ('ball', 'ball') ](thing1, thing2, dt, bounce)

    Your task is to write a function called collision that calls the right function for any combination of types by using a dictionary.

    1. At the bottom of the collision.py file, at the top level (meaning totally unindented), create an empty dictionary called collision_router. Then add an entry to the dictionary (as above) for each possible combination of types you want to handle.

      Remember, if you want your shape to act like a ball, it needs to have a getRadius method. If you want your shape to act like a block, it needs to have getWidth and getHeight methods.

    2. Create a function collision and uses the collision_router dictionary to call the right function.
      def collision(ball, thing, dt, bounce=True):
          # return the result of calling the proper collision function

      Assume that the ball is always the first parameter and the other object (ball, block, or other) is the second parameter. Use a tuple with the type of the two objects (use getType) as the keys. Use the dictionary entry as a function, passing in the two objects, dt, and bounce as parameters. Make sure you return the result.

    3. Test your code with the following functions.

      test9-6.py - collision test
      test9-7.py - collision test 2
      test9-8.py - bounce two balls
      test9-10.py - bouncy stuff

      In test9-7.py, try changing the last argument to the collision function to True instead of False and see what happens.

      Modify the test code to test your other shapes as well (i.e. replace a ball or a block with one of your shapes).

  4. Create an animated scene with obstacles

    Create a scene that is like a pinball table. It should have overall boundaries made of long thin blocks and then some obstacles inside the bounding box that are all stationary. It does not need to be completely enclosed. The default behavior should be for the program to launch a single object into the scene and have it bounce around. You are free to make it interactive, launch multiple balls, or otherwise modify the scene as you wish.

    + (more detail)

    To create the scene, the following is a suggested structure for your code to get started.

    def buildObstacles(win):
        # Create all of the obstacles in the scene and put them in a list
        # Each obstacle should be a Thing (e.g. Ball, Block, other)
        # You might want to give one or more the obstacles an elasticity > 1
        # Return the list of Things
    
    def main():
        # create a GraphWin
        # call buildObstacles, storing the return list in a variable (e.g. shapes)
        # loop over the shapes list and have each Thing call its draw method
    
        # assign to dt the value 0.02
    
        # create an object (e.g. ball), give it an initial velocity and acceleration, and draw it
    
        # start an infinite loop
    
            # using checKey, if the user typed a 'q' then break
    
            # if the ball is out of bounds, re-launch/re-position it
    
            # assign to collided the value False
            # for each item in the shapes list
                # if the result of calling the collision function with the ball and the item is True
                    # set collided to True
    
            # if collided is equal to False
                # call the update method of the ball with dt as the time step
    
            # call win.update()
    
        # close the window
    
    if __name__ == "__main__":
        main()
    						

    You can adjust the time step to change the visual behavior of the simulation.

    Create a video of your obstacle course in action and include a link to it on your wiki.


Follow-up Questions

These are questions you should be able to answer and may be similar to questions on a quiz or final exam. If you have any questions about them, send your answer to the lab instructor along with any questions in return for some feedback.

  1. What is inheritance?
  2. What does it mean for a child class to override a method?
  3. What is a class variable or class global variable?
  4. What is a field of an object?

Extensions

Extensions are your opportunity to customize your project, learn something else of interest to you, and improve your grade. The following are some suggested extensions, but you are free to choose your own.

Due to the switch to remote learning, extensions are no longer a graded part of CS 152 for the rest of the semester. They are things you can do on your own to learn more and become better at CS, especially if you are interested in taking more CS courses.


Submit your code

Turn in your code (all files ending with .py) by putting it in a directory in the Courses server. On the Courses server, you should have access to a directory called CS152, and within that, a directory with your user name. Within this directory is a directory named Private. Files that you put into that private directory you can edit, read, and write, and the professor can edit, read, and write, but no one else. To hand in your code and other materials, create a new directory, such as project9, and then copy your code into the project directory for that week. Please submit only code that you want to be graded.

When submitting your code, double check the following.

  1. Is your name at the top of each code file?
  2. Does every function have a comment or docstring specifying what it does?
  3. Is your handin project directory inside your Private folder on Courses?